Skip to content
This repository has been archived by the owner on Aug 11, 2024. It is now read-only.

Render transclude blocks in MemoDetailViewer #574

Merged
merged 23 commits into from
Jun 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions xcode/Subconscious/Shared/Components/AppView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1958,6 +1958,7 @@ struct AppEnvironment {
var database: DatabaseService
var data: DataService
var feed: FeedService
var transclude: TranscludeService

var recoveryPhrase: RecoveryPhraseEnvironment = RecoveryPhraseEnvironment()

Expand Down Expand Up @@ -2045,6 +2046,7 @@ struct AppEnvironment {

self.feed = FeedService()
self.gatewayProvisioningService = GatewayProvisioningService()
self.transclude = TranscludeService(database: database, noosphere: noosphere)
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ extension UserProfile {
self.nickname,
self.address.petname?.leaf
) {
case (.you, _, .some(let selfNickname), _):
case (.ourself, _, .some(let selfNickname), _):
return NameVariant.petname(Slashlink.ourProfile, selfNickname)
case (_, .following(let petname), _, _):
return NameVariant.petname(self.address, petname)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ struct EditProfileSheet: View {
address: user.address,
pfp: pfp,
bio: UserProfileBio(state.bioField.validated?.text ?? ""),
category: .you,
category: .ourself,
resolutionStatus: .resolved(Cid("fake-for-preview")),
ourFollowStatus: .notFollowing
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ struct UserProfileHeaderView: View {
Button(
action: {
switch (user.category, user.ourFollowStatus) {
case (.you, _):
case (.ourself, _):
action(.editOwnProfile)
case (_, .following(_)):
action(.requestUnfollow)
Expand All @@ -50,7 +50,7 @@ struct UserProfileHeaderView: View {
},
label: {
switch (user.category, user.ourFollowStatus) {
case (.you, _):
case (.ourself, _):
Label("Edit Profile", systemImage: AppIcon.edit.systemName)
case (_, .following(_)):
Label("Following", systemImage: AppIcon.following.systemName)
Expand All @@ -60,7 +60,7 @@ struct UserProfileHeaderView: View {
}
)
.buttonStyle(GhostPillButtonStyle(size: .small))
.frame(maxWidth: user.category == .you ? 120 : 100)
.frame(maxWidth: user.category == .ourself ? 120 : 100)
}
}

Expand Down Expand Up @@ -123,7 +123,7 @@ struct BylineLgView_Previews: PreviewProvider {
address: Slashlink.ourProfile,
pfp: .image("pfp-dog"),
bio: UserProfileBio("Ploofy snooflewhumps burbled, outflonking the zibber-zabber in a traddlewaddle."),
category: .you,
category: .ourself,
resolutionStatus: .resolved(Cid("ok")),
ourFollowStatus: .notFollowing
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ struct UserProfileView: View {
status: state.loadingState
)
if let user = state.user,
user.category == .you {
user.category == .ourself {
ToolbarItem(placement: .confirmationAction) {
Button(
action: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ struct StoryUserView: View {
case (.following(_), _):
Image.from(appIcon: .following)
.foregroundColor(.secondary)
case (_, .you):
case (_, .ourself):
Image.from(appIcon: .you(colorScheme))
.foregroundColor(.secondary)
case (_, _):
Expand Down Expand Up @@ -113,7 +113,7 @@ struct StoryUserView: View {
.background(.background)
.foregroundColor(.secondary)
}
).disabled(story.user.category == .you)
).disabled(story.user.category == .ourself)
}
.padding(AppTheme.tightPadding)
.frame(height: AppTheme.unit * 13)
Expand Down Expand Up @@ -181,7 +181,7 @@ struct StoryUserView_Previews: PreviewProvider {
address: Slashlink(petname: Petname("ben.gordon.chris.bob")!),
pfp: .image("pfp-dog"),
bio: UserProfileBio("Ploofy snooflewhumps burbled, outflonking the zibber-zabber."),
category: .you,
category: .ourself,
resolutionStatus: .resolved(Cid("ok")),
ourFollowStatus: .notFollowing
)
Expand All @@ -196,7 +196,7 @@ struct StoryUserView_Previews: PreviewProvider {
address: Slashlink(petname: Petname("ben.gordon.chris.bob")!),
pfp: .image("pfp-dog"),
bio: UserProfileBio.empty,
category: .you,
category: .ourself,
resolutionStatus: .pending,
ourFollowStatus: .notFollowing
)
Expand Down
36 changes: 34 additions & 2 deletions xcode/Subconscious/Shared/Components/Common/SubtextView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,37 @@ import SwiftUI
struct SubtextView: View {
private static var renderer = SubtextAttributedStringRenderer()
var subtext: Subtext
var transcludePreviews: [Slashlink: EntryStub]
var onViewTransclude: (Slashlink) -> Void

private func entries(for block: Subtext.Block) -> [EntryStub] {
block.slashlinks
.compactMap { link in
guard let slashlink = link.toSlashlink() else {
return nil
}

return transcludePreviews[slashlink]
}
.filter { entry in
// Avoid empty transclude blocks
entry.excerpt.count > 0
}
}

var body: some View {
VStack(alignment: .leading) {
ForEach(subtext.blocks, id: \.self) { block in
Text(Self.renderer.render(block.description))
ForEach(self.entries(for: block), id: \.self) { entry in
Transclude2View(
address: entry.address,
excerpt: entry.excerpt,
action: {
onViewTransclude(entry.address)
}
)
}
}
}
.expandAlignedLeading()
Expand Down Expand Up @@ -45,13 +71,19 @@ struct SubtextView_Previews: PreviewProvider {

Brief were my days among you, and briefer still the words I have spoken.

But should my voice fade in your ears, and my love vanish in your memory, then I will come again,
But should my /voice fade in your ears, and my love vanish in your /memory, then I will come again,

And with a richer heart and lips more yielding to the spirit will I speak.

Yea, I shall return with the tide,
"""
)
),
transcludePreviews: [
Slashlink("/wanderer-your-footsteps-are-the-road")!: EntryStub(address: Slashlink("/wanderer-your-footsteps-are-the-road")!, excerpt: "hello mother", modified: Date.now),
Slashlink("/voice")!: EntryStub(address: Slashlink("/voice")!, excerpt: "hello father", modified: Date.now),
Slashlink("/memory")!: EntryStub(address: Slashlink("/memory")!, excerpt: "hello world", modified: Date.now)
],
onViewTransclude: { _ in }
)
}
}
Expand Down
139 changes: 135 additions & 4 deletions xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ struct MemoViewerDetailView: View {
MemoViewerDetailLoadedView(
title: store.state.title,
dom: store.state.dom,
transcludePreviews: store.state.transcludePreviews,
address: description.address,
backlinks: store.state.backlinks,
send: store.send,
Expand Down Expand Up @@ -127,6 +128,7 @@ struct MemoViewerDetailLoadingView: View {
struct MemoViewerDetailLoadedView: View {
var title: String
var dom: Subtext
var transcludePreviews: [Slashlink: EntryStub]
var address: Slashlink
var backlinks: [EntryStub]
var send: (MemoViewerDetailAction) -> Void
Expand Down Expand Up @@ -158,14 +160,30 @@ struct MemoViewerDetailLoadedView: View {
)
return .handled
}

private func onViewTransclude(
address: Slashlink
) {
notify(
.requestDetail(
MemoDetailDescription.from(
address: address,
fallback: address.description
)
)
)
}


var body: some View {
GeometryReader { geometry in
ScrollView {
VStack {
VStack {
SubtextView(
subtext: dom
subtext: dom,
transcludePreviews: transcludePreviews,
onViewTransclude: self.onViewTransclude
).textSelection(
.enabled
).environment(\.openURL, OpenURLAction { url in
Expand Down Expand Up @@ -216,6 +234,14 @@ enum MemoViewerDetailAction: Hashable {
case failLoadDetail(_ message: String)
case presentMetaSheet(_ isPresented: Bool)

case fetchTranscludePreviews
case succeedFetchTranscludePreviews([Slashlink: EntryStub])
case failFetchTranscludePreviews(_ error: String)

case fetchOwnerProfile
case succeedFetchOwnerProfile(UserProfile)
case failFetchOwnerProfile(_ error: String)

/// Synonym for `.metaSheet(.setAddress(_))`
static func setMetaSheetAddress(_ address: Slashlink) -> Self {
.metaSheet(.setAddress(address))
Expand Down Expand Up @@ -245,6 +271,7 @@ struct MemoViewerDetailModel: ModelProtocol {

var loadingState = LoadingState.loading

var owner: UserProfile?
var address: Slashlink?
var defaultAudience = Audience.local
var title = ""
Expand All @@ -255,6 +282,8 @@ struct MemoViewerDetailModel: ModelProtocol {
var isMetaSheetPresented = false
var metaSheet = MemoViewerDetailMetaSheetModel()

var transcludePreviews: [Slashlink: EntryStub] = [:]

static func update(
state: Self,
action: Action,
Expand Down Expand Up @@ -294,6 +323,36 @@ struct MemoViewerDetailModel: ModelProtocol {
environment: environment,
isPresented: isPresented
)
case .fetchTranscludePreviews:
return fetchTranscludePreviews(
state: state,
environment: environment
)
case .succeedFetchTranscludePreviews(let transcludes):
var model = state
model.transcludePreviews = transcludes
return Update(state: model)

case .failFetchTranscludePreviews(let error):
logger.error("Failed to fetch transcludes: \(error)")
return Update(state: state)

case .fetchOwnerProfile:
return fetchOwnerProfile(
state: state,
environment: environment
)
case .succeedFetchOwnerProfile(let profile):
var model = state
model.owner = profile
return update(
state: model,
action: .fetchTranscludePreviews,
environment: environment
)
case .failFetchOwnerProfile(let error):
logger.error("Failed to fetch owner: \(error)")
return Update(state: state)
}
}

Expand All @@ -314,7 +373,10 @@ struct MemoViewerDetailModel: ModelProtocol {
return update(
state: model,
// Set meta sheet address as well
action: .setMetaSheetAddress(description.address),
actions: [
.setMetaSheetAddress(description.address),
.fetchOwnerProfile
],
environment: environment
).mergeFx(fx)
}
Expand Down Expand Up @@ -354,7 +416,67 @@ struct MemoViewerDetailModel: ModelProtocol {
) -> Update<Self> {
var model = state
model.dom = dom
return Update(state: model)
return update(
state: model,
action: .fetchTranscludePreviews,
environment: environment
)
}

static func fetchTranscludePreviews(
state: MemoViewerDetailModel,
environment: MemoViewerDetailModel.Environment
) -> Update<MemoViewerDetailModel> {

guard let owner = state.owner else {
return Update(state: state)
bfollington marked this conversation as resolved.
Show resolved Hide resolved
}

let links = state.dom.slashlinks
.map { value in value.toSlashlink() }
.compactMap { value in value }
bfollington marked this conversation as resolved.
Show resolved Hide resolved

let fx: Fx<MemoViewerDetailAction> =
environment.transclude
bfollington marked this conversation as resolved.
Show resolved Hide resolved
.fetchTranscludePreviewsPublisher(slashlinks: links, owner: owner)
.map { entries in
MemoViewerDetailAction.succeedFetchTranscludePreviews(entries)
}
.recover { error in
MemoViewerDetailAction.failFetchTranscludePreviews(error.localizedDescription)
}
.eraseToAnyPublisher()

return Update(state: state, fx: fx)
}

static func fetchOwnerProfile(
state: MemoViewerDetailModel,
environment: MemoViewerDetailModel.Environment
) -> Update<MemoViewerDetailModel> {
let fx: Fx<MemoViewerDetailAction> =
Future.detached {
guard let petname = state.address?.toPetname() else {
return try await environment
.userProfile
.requestOurProfile()
.profile
}

return try await environment
.userProfile
.requestUserProfile(petname: petname)
.profile
}
.map { profile in
.succeedFetchOwnerProfile(profile)
}
.recover { error in
.failFetchOwnerProfile(error.localizedDescription)
}
.eraseToAnyPublisher()

return Update(state: state, fx: fx)
}

static func presentMetaSheet(
Expand Down Expand Up @@ -403,13 +525,22 @@ struct MemoViewerDetailView_Previews: PreviewProvider {

Say not, "I have found the path of the soul." Say rather, "I have met the soul walking upon my path."

For the soul walks upon all paths. /infinity-paths
For the soul walks upon all paths.

/infinity-paths

The soul walks not upon a line, neither does it grow like a reed.

The soul unfolds itself, like a [[lotus]] of countless petals.
"""
),
transcludePreviews: [
Slashlink("/infinity-paths")!: EntryStub(
address: Slashlink("/infinity-paths")!,
excerpt: "Say not, \"I have discovered the soul's destination,\" but rather, \"I have glimpsed the soul's journey, ever unfolding along the way.\"",
bfollington marked this conversation as resolved.
Show resolved Hide resolved
modified: Date.now
)
],
address: Slashlink(slug: Slug("truth-the-prophet")!),
backlinks: [],
send: { action in },
Expand Down
Loading