From da71fbcb965ea05effdac56579950baf7d35c23f Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Fri, 5 May 2023 14:38:16 +1000 Subject: [PATCH 01/23] Render transclude blocks in MemoViewer --- .../Shared/Components/AppView.swift | 2 + .../Components/Common/SubtextView.swift | 22 ++++++- .../Detail/MemoViewerDetailView.swift | 66 ++++++++++++++++++- .../Shared/Services/DatabaseService.swift | 44 +++++++++++++ .../Shared/Services/TranscludeService.swift | 37 +++++++++++ .../Subconscious.xcodeproj/project.pbxproj | 6 ++ 6 files changed, 172 insertions(+), 5 deletions(-) create mode 100644 xcode/Subconscious/Shared/Services/TranscludeService.swift diff --git a/xcode/Subconscious/Shared/Components/AppView.swift b/xcode/Subconscious/Shared/Components/AppView.swift index 557cec39..50b1e1f5 100644 --- a/xcode/Subconscious/Shared/Components/AppView.swift +++ b/xcode/Subconscious/Shared/Components/AppView.swift @@ -1958,6 +1958,7 @@ struct AppEnvironment { var database: DatabaseService var data: DataService var feed: FeedService + var transclude: TranscludeService var recoveryPhrase: RecoveryPhraseEnvironment = RecoveryPhraseEnvironment() @@ -2045,6 +2046,7 @@ struct AppEnvironment { self.feed = FeedService() self.gatewayProvisioningService = GatewayProvisioningService() + self.transclude = TranscludeService(database: database) } } diff --git a/xcode/Subconscious/Shared/Components/Common/SubtextView.swift b/xcode/Subconscious/Shared/Components/Common/SubtextView.swift index 4ab1cca9..60d26d7f 100644 --- a/xcode/Subconscious/Shared/Components/Common/SubtextView.swift +++ b/xcode/Subconscious/Shared/Components/Common/SubtextView.swift @@ -10,11 +10,25 @@ import SwiftUI struct SubtextView: View { private static var renderer = SubtextAttributedStringRenderer() var subtext: Subtext + var transcludes: Dictionary + var onViewTransclude: (Slashlink) -> Void var body: some View { VStack(alignment: .leading) { ForEach(subtext.blocks, id: \.self) { block in - Text(Self.renderer.render(block.description)) + if let link = block + .slashlinks + .get(0)? + .toSlashlink(), + let entry = transcludes[link] { + Transclude2View( + address: entry.address, + excerpt: entry.excerpt, + action: { onViewTransclude(link) } + ) + } else { + Text(Self.renderer.render(block.description)) + } } } .expandAlignedLeading() @@ -51,7 +65,11 @@ struct SubtextView_Previews: PreviewProvider { Yea, I shall return with the tide, """ - ) + ), + transcludes: [ + Slashlink("/wanderer-your-footsteps-are-the-road")!: EntryStub(address: Slashlink("/wanderer-your-footsteps-are-the-road")!.toPublicMemoAddress(), excerpt: "hello world", modified: Date.now) + ], + onViewTransclude: { _ in } ) } } diff --git a/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift b/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift index 2f8a295f..c4f00950 100644 --- a/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift +++ b/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift @@ -35,6 +35,7 @@ struct MemoViewerDetailView: View { MemoViewerDetailLoadedView( title: store.state.title, dom: store.state.dom, + transcludes: store.state.transcludes, address: description.address, backlinks: store.state.backlinks, send: store.send, @@ -127,6 +128,7 @@ struct MemoViewerDetailLoadingView: View { struct MemoViewerDetailLoadedView: View { var title: String var dom: Subtext + var transcludes: [Slashlink: EntryStub] var address: Slashlink var backlinks: [EntryStub] var send: (MemoViewerDetailAction) -> Void @@ -158,6 +160,17 @@ struct MemoViewerDetailLoadedView: View { ) return .handled } + + private func onViewTransclude( + address: Slashlink + ) { + if address.isOurs { + notify(.requestDetail(.editor(MemoEditorDetailDescription(address: address)))) + } else { + notify(.requestDetail(.viewer(MemoViewerDetailDescription(address: address)))) + } + } + var body: some View { GeometryReader { geometry in @@ -165,7 +178,9 @@ struct MemoViewerDetailLoadedView: View { VStack { VStack { SubtextView( - subtext: dom + subtext: dom, + transcludes: transcludes, + onViewTransclude: self.onViewTransclude ).textSelection( .enabled ).environment(\.openURL, OpenURLAction { url in @@ -216,6 +231,10 @@ enum MemoViewerDetailAction: Hashable { case failLoadDetail(_ message: String) case presentMetaSheet(_ isPresented: Bool) + case fetchTranscludes + case succeedFetchTranscludes(Dictionary) + case failFetchTranscludes(_ error: String) + /// Synonym for `.metaSheet(.setAddress(_))` static func setMetaSheetAddress(_ address: Slashlink) -> Self { .metaSheet(.setAddress(address)) @@ -255,6 +274,9 @@ struct MemoViewerDetailModel: ModelProtocol { var isMetaSheetPresented = false var metaSheet = MemoViewerDetailMetaSheetModel() + /// Transclude block preview cache + var transcludes: Dictionary = Dictionary() + static func update( state: Self, action: Action, @@ -294,6 +316,32 @@ struct MemoViewerDetailModel: ModelProtocol { environment: environment, isPresented: isPresented ) + + case .fetchTranscludes: + let links = state.dom.slashlinks + .map { value in value.toSlashlink() } + .compactMap { value in value } + + let fx: Fx = + environment.transclude + .fetchTranscludesPublisher(slashlinks: links) + .map { entries in + MemoViewerDetailAction.succeedFetchTranscludes(entries) + } + .recover { error in + MemoViewerDetailAction.failFetchTranscludes(error.localizedDescription) + } + .eraseToAnyPublisher() + + return Update(state: state, fx: fx) + case .succeedFetchTranscludes(let transcludes): + var model = state + model.transcludes = transcludes + return Update(state: model) + + case .failFetchTranscludes(let error): + logger.error("Failed to fetch transcludes: \(error)") + return Update(state: state) } } @@ -314,7 +362,10 @@ struct MemoViewerDetailModel: ModelProtocol { return update( state: model, // Set meta sheet address as well - action: .setMetaSheetAddress(description.address), + actions: [ + .setMetaSheetAddress(description.address), + .fetchTranscludes + ], environment: environment ).mergeFx(fx) } @@ -403,13 +454,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. """ ), + transcludes: [ + Slashlink("/infinity-paths")!: EntryStub( + address: Slashlink("/infinity-paths")!.toPublicMemoAddress(), + excerpt: "Say not, \"I have discovered the soul's destination,\" but rather, \"I have glimpsed the soul's journey, ever unfolding along the way.\"", + modified: Date.now + ) + ], address: Slashlink(slug: Slug("truth-the-prophet")!), backlinks: [], send: { action in }, diff --git a/xcode/Subconscious/Shared/Services/DatabaseService.swift b/xcode/Subconscious/Shared/Services/DatabaseService.swift index 47153a6e..d657069c 100644 --- a/xcode/Subconscious/Shared/Services/DatabaseService.swift +++ b/xcode/Subconscious/Shared/Services/DatabaseService.swift @@ -434,6 +434,50 @@ final class DatabaseService { return results.col(0)?.toInt() } + func listEntriesForSlashlinks(slashlinks: [Slashlink]) throws -> [EntryStub] { + guard self.state == .ready else { + throw DatabaseServiceError.notReady + } + + let parameters = + slashlinks + .flatMap { s in + // TODO: this is almost certainly the wrong way to do this + [ + SQLite3Database.Value.text(s.toPublicMemoAddress().description), + SQLite3Database.Value.text(s.toLocalMemoAddress().description) + ] + } + + let parameterPlaceholders = (parameters.map { _ in "?" }).joined(separator: ", ") + + let results = try database.execute( + sql: """ + SELECT id, modified, excerpt + FROM memo + WHERE id IN (\(parameterPlaceholders)) + ORDER BY modified DESC + LIMIT 1000 + """, + parameters: parameters + ) + + return results.compactMap({ row in + guard + let address = row.col(0)?.toString()?.toMemoAddress(), + let modified = row.col(1)?.toDate(), + let excerpt = row.col(2)?.toString() + else { + return nil + } + return EntryStub( + address: address, + excerpt: excerpt, + modified: modified + ) + }) + } + /// List recent entries func listRecentMemos(owner: Did?) throws -> [EntryStub] { guard self.state == .ready else { diff --git a/xcode/Subconscious/Shared/Services/TranscludeService.swift b/xcode/Subconscious/Shared/Services/TranscludeService.swift new file mode 100644 index 00000000..8cc32ed0 --- /dev/null +++ b/xcode/Subconscious/Shared/Services/TranscludeService.swift @@ -0,0 +1,37 @@ +// +// TranscludeService.swift +// Subconscious +// +// Created by Ben Follington on 5/5/2023. +// + +import Foundation +import Combine + +actor TranscludeService { + private var database: DatabaseService + + init(database: DatabaseService) { + self.database = database + } + + func fetchTranscludes(slashlinks: [Slashlink]) async throws -> Dictionary { + let entries = try database.listEntriesForSlashlinks(slashlinks: slashlinks) + + var dict = Dictionary() + for entry in entries { + dict[entry.address.toSlashlink()] = entry + } + + return dict + } + + nonisolated func fetchTranscludesPublisher( + slashlinks: [Slashlink] + ) -> AnyPublisher, Error> { + Future.detached(priority: .utility) { + try await self.fetchTranscludes(slashlinks: slashlinks) + } + .eraseToAnyPublisher() + } +} diff --git a/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj b/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj index 575d1006..06bd2f69 100644 --- a/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj +++ b/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ B508956E29E7862A0048106B /* Tests_AddressBookService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B508956D29E7862A0048106B /* Tests_AddressBookService.swift */; }; B508957029E79BE70048106B /* UserProfileService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B508956F29E79BE70048106B /* UserProfileService.swift */; }; B508957129E79BE70048106B /* UserProfileService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B508956F29E79BE70048106B /* UserProfileService.swift */; }; + B50B045B2A04B61000AA584B /* TranscludeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50B045A2A04B61000AA584B /* TranscludeService.swift */; }; + B50B045C2A04B61000AA584B /* TranscludeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B50B045A2A04B61000AA584B /* TranscludeService.swift */; }; B51EEAA129F0C37B0055887B /* AppIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51EEAA029F0C37B0055887B /* AppIcon.swift */; }; B5293B892A426645001C4DA7 /* Sentry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5293B882A426645001C4DA7 /* Sentry.swift */; }; B5293B8A2A426645001C4DA7 /* Sentry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5293B882A426645001C4DA7 /* Sentry.swift */; }; @@ -451,6 +453,7 @@ /* Begin PBXFileReference section */ B508956D29E7862A0048106B /* Tests_AddressBookService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_AddressBookService.swift; sourceTree = ""; }; B508956F29E79BE70048106B /* UserProfileService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileService.swift; sourceTree = ""; }; + B50B045A2A04B61000AA584B /* TranscludeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscludeService.swift; sourceTree = ""; }; B51EEAA029F0C37B0055887B /* AppIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIcon.swift; sourceTree = ""; }; B5293B882A426645001C4DA7 /* Sentry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sentry.swift; sourceTree = ""; }; B532F8C229B1752E00CE9256 /* TranscludeBlockLayoutFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscludeBlockLayoutFragment.swift; sourceTree = ""; }; @@ -1009,6 +1012,7 @@ B8CBAFA8299580E50079107E /* StoreProtocol.swift */, B508956F29E79BE70048106B /* UserProfileService.swift */, B5CA129F29FF732A00860E9E /* GatewayProvisioningService.swift */, + B50B045A2A04B61000AA584B /* TranscludeService.swift */, ); path = Services; sourceTree = ""; @@ -1758,6 +1762,7 @@ B8CBAFA42994491B0079107E /* MenuButtonView.swift in Sources */, B57C0AF729D29A8F00D352E3 /* TabbedThreeColumnView.swift in Sources */, B57C0AEE29D280BB00D352E3 /* ThreeColumnView.swift in Sources */, + B50B045B2A04B61000AA584B /* TranscludeService.swift in Sources */, B8133AEB29B8FD1300B38760 /* SubtextAttributedStringRenderer.swift in Sources */, B85EC460296F099700558761 /* ProfilePic.swift in Sources */, B57C0AF529D2865600D352E3 /* UserProfileDetailView.swift in Sources */, @@ -1987,6 +1992,7 @@ B8EC568A26F4204F00AC64E5 /* SQLite3Database.swift in Sources */, B8B4251928FDE8AA0081B8D5 /* MemoData.swift in Sources */, B8A59D6A28B692900010DB2F /* StoryPrompt.swift in Sources */, + B50B045C2A04B61000AA584B /* TranscludeService.swift in Sources */, B5CFC7FF29E5403900178631 /* FollowUserSheet.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; From 2d32cda4697cb04bc1923e9039ca2fb8ae7beffb Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Fri, 5 May 2023 14:51:25 +1000 Subject: [PATCH 02/23] Formatting --- .../Detail/MemoViewerDetailView.swift | 4 ++-- .../Shared/Services/TranscludeService.swift | 20 +++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift b/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift index c4f00950..cb1768b6 100644 --- a/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift +++ b/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift @@ -232,7 +232,7 @@ enum MemoViewerDetailAction: Hashable { case presentMetaSheet(_ isPresented: Bool) case fetchTranscludes - case succeedFetchTranscludes(Dictionary) + case succeedFetchTranscludes([Slashlink: EntryStub]) case failFetchTranscludes(_ error: String) /// Synonym for `.metaSheet(.setAddress(_))` @@ -275,7 +275,7 @@ struct MemoViewerDetailModel: ModelProtocol { var metaSheet = MemoViewerDetailMetaSheetModel() /// Transclude block preview cache - var transcludes: Dictionary = Dictionary() + var transcludes: [Slashlink: EntryStub] = [:] static func update( state: Self, diff --git a/xcode/Subconscious/Shared/Services/TranscludeService.swift b/xcode/Subconscious/Shared/Services/TranscludeService.swift index 8cc32ed0..f289fca2 100644 --- a/xcode/Subconscious/Shared/Services/TranscludeService.swift +++ b/xcode/Subconscious/Shared/Services/TranscludeService.swift @@ -15,20 +15,24 @@ actor TranscludeService { self.database = database } - func fetchTranscludes(slashlinks: [Slashlink]) async throws -> Dictionary { + func fetchTranscludes( + slashlinks: [Slashlink] + ) async throws -> [Slashlink: EntryStub] { let entries = try database.listEntriesForSlashlinks(slashlinks: slashlinks) - var dict = Dictionary() - for entry in entries { - dict[entry.address.toSlashlink()] = entry - } - - return dict + return + Dictionary( + entries.map { entry in + (entry.address.toSlashlink(), entry) + }, + uniquingKeysWith: { a, b in a} + ) } + nonisolated func fetchTranscludesPublisher( slashlinks: [Slashlink] - ) -> AnyPublisher, Error> { + ) -> AnyPublisher<[Slashlink: EntryStub], Error> { Future.detached(priority: .utility) { try await self.fetchTranscludes(slashlinks: slashlinks) } From c1b2930c53a60f6ae1ef3cec3a4debf8d032a919 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Mon, 8 May 2023 15:29:37 +1000 Subject: [PATCH 03/23] Render all slashlinks as transcludes --- .../Components/Common/SubtextView.swift | 40 ++++++++++++------- .../Detail/MemoViewerDetailView.swift | 2 +- .../Shared/Services/DatabaseService.swift | 6 +-- .../Shared/Services/TranscludeService.swift | 2 +- 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Common/SubtextView.swift b/xcode/Subconscious/Shared/Components/Common/SubtextView.swift index 60d26d7f..5ad32228 100644 --- a/xcode/Subconscious/Shared/Components/Common/SubtextView.swift +++ b/xcode/Subconscious/Shared/Components/Common/SubtextView.swift @@ -16,18 +16,28 @@ struct SubtextView: View { var body: some View { VStack(alignment: .leading) { ForEach(subtext.blocks, id: \.self) { block in - if let link = block - .slashlinks - .get(0)? - .toSlashlink(), - let entry = transcludes[link] { - Transclude2View( - address: entry.address, - excerpt: entry.excerpt, - action: { onViewTransclude(link) } - ) - } else { - Text(Self.renderer.render(block.description)) + Text(Self.renderer.render(block.description)) + + let entries: [EntryStub] = + block.slashlinks + .compactMap { link in + guard let slashlink = link.toSlashlink() else { + return nil + } + + return transcludes[slashlink] + } + + if entries.count > 0 { + ForEach(entries, id: \.self) { entry in + Transclude2View( + address: entry.address, + excerpt: entry.excerpt, + action: { + onViewTransclude(entry.address) + } + ) + } } } } @@ -59,7 +69,7 @@ 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. @@ -67,7 +77,9 @@ struct SubtextView_Previews: PreviewProvider { """ ), transcludes: [ - Slashlink("/wanderer-your-footsteps-are-the-road")!: EntryStub(address: Slashlink("/wanderer-your-footsteps-are-the-road")!.toPublicMemoAddress(), excerpt: "hello world", modified: Date.now) + Slashlink("/wanderer-your-footsteps-are-the-road")!: EntryStub(address: Slashlink("/wanderer-your-footsteps-are-the-road")!, excerpt: "hello world", modified: Date.now), + Slashlink("/voice")!: EntryStub(address: Slashlink("/voice")!, excerpt: "hello world", modified: Date.now), + Slashlink("/memory")!: EntryStub(address: Slashlink("/memory")!, excerpt: "hello world", modified: Date.now) ], onViewTransclude: { _ in } ) diff --git a/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift b/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift index cb1768b6..59c4abbe 100644 --- a/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift +++ b/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift @@ -465,7 +465,7 @@ struct MemoViewerDetailView_Previews: PreviewProvider { ), transcludes: [ Slashlink("/infinity-paths")!: EntryStub( - address: Slashlink("/infinity-paths")!.toPublicMemoAddress(), + 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.\"", modified: Date.now ) diff --git a/xcode/Subconscious/Shared/Services/DatabaseService.swift b/xcode/Subconscious/Shared/Services/DatabaseService.swift index d657069c..162ec7a9 100644 --- a/xcode/Subconscious/Shared/Services/DatabaseService.swift +++ b/xcode/Subconscious/Shared/Services/DatabaseService.swift @@ -444,8 +444,8 @@ final class DatabaseService { .flatMap { s in // TODO: this is almost certainly the wrong way to do this [ - SQLite3Database.Value.text(s.toPublicMemoAddress().description), - SQLite3Database.Value.text(s.toLocalMemoAddress().description) + SQLite3Database.Value.text(s.description), + SQLite3Database.Value.text(s.description) ] } @@ -464,7 +464,7 @@ final class DatabaseService { return results.compactMap({ row in guard - let address = row.col(0)?.toString()?.toMemoAddress(), + let address = row.col(0)?.toString()?.toSlashlink(), let modified = row.col(1)?.toDate(), let excerpt = row.col(2)?.toString() else { diff --git a/xcode/Subconscious/Shared/Services/TranscludeService.swift b/xcode/Subconscious/Shared/Services/TranscludeService.swift index f289fca2..27aa5ffe 100644 --- a/xcode/Subconscious/Shared/Services/TranscludeService.swift +++ b/xcode/Subconscious/Shared/Services/TranscludeService.swift @@ -23,7 +23,7 @@ actor TranscludeService { return Dictionary( entries.map { entry in - (entry.address.toSlashlink(), entry) + (entry.address, entry) }, uniquingKeysWith: { a, b in a} ) From 506140ed341d880e55b1c5e0c849c758a66a27c2 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Mon, 8 May 2023 15:31:55 +1000 Subject: [PATCH 04/23] Filter out empty transclude blocks --- .../Shared/Components/Common/SubtextView.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Common/SubtextView.swift b/xcode/Subconscious/Shared/Components/Common/SubtextView.swift index 5ad32228..739fd95d 100644 --- a/xcode/Subconscious/Shared/Components/Common/SubtextView.swift +++ b/xcode/Subconscious/Shared/Components/Common/SubtextView.swift @@ -27,6 +27,10 @@ struct SubtextView: View { return transcludes[slashlink] } + .filter { entry in + // Avoid empty transclude blocks + entry.excerpt.count > 0 + } if entries.count > 0 { ForEach(entries, id: \.self) { entry in @@ -77,8 +81,8 @@ struct SubtextView_Previews: PreviewProvider { """ ), transcludes: [ - Slashlink("/wanderer-your-footsteps-are-the-road")!: EntryStub(address: Slashlink("/wanderer-your-footsteps-are-the-road")!, excerpt: "hello world", modified: Date.now), - Slashlink("/voice")!: EntryStub(address: Slashlink("/voice")!, excerpt: "hello world", modified: Date.now), + 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 } From f30388cd610ee578edc83bac296c7a325e06e282 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Mon, 8 May 2023 17:09:51 +1000 Subject: [PATCH 05/23] Rework DB method for fetching transcludes --- .../Shared/Services/DatabaseService.swift | 29 +++++++++---------- .../Shared/Services/TranscludeService.swift | 2 +- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/xcode/Subconscious/Shared/Services/DatabaseService.swift b/xcode/Subconscious/Shared/Services/DatabaseService.swift index 162ec7a9..0005f760 100644 --- a/xcode/Subconscious/Shared/Services/DatabaseService.swift +++ b/xcode/Subconscious/Shared/Services/DatabaseService.swift @@ -434,32 +434,30 @@ final class DatabaseService { return results.col(0)?.toInt() } - func listEntriesForSlashlinks(slashlinks: [Slashlink]) throws -> [EntryStub] { + func listEntries(for slashlinks: [Slashlink]) throws -> [EntryStub] { + return try slashlinks.compactMap({ slashlink in + return try readEntry(for: slashlink) + }) + } + + func readEntry(for slashlink: Slashlink) throws -> EntryStub? { guard self.state == .ready else { throw DatabaseServiceError.notReady } - let parameters = - slashlinks - .flatMap { s in - // TODO: this is almost certainly the wrong way to do this - [ - SQLite3Database.Value.text(s.description), - SQLite3Database.Value.text(s.description) - ] - } - - let parameterPlaceholders = (parameters.map { _ in "?" }).joined(separator: ", ") - let results = try database.execute( sql: """ SELECT id, modified, excerpt FROM memo - WHERE id IN (\(parameterPlaceholders)) + WHERE id IN (?, ?) ORDER BY modified DESC LIMIT 1000 """, - parameters: parameters + parameters: [ + .text( + slashlink.description // TODO: this might be wrong + ), + ] ) return results.compactMap({ row in @@ -476,6 +474,7 @@ final class DatabaseService { modified: modified ) }) + .first } /// List recent entries diff --git a/xcode/Subconscious/Shared/Services/TranscludeService.swift b/xcode/Subconscious/Shared/Services/TranscludeService.swift index 27aa5ffe..2409bb37 100644 --- a/xcode/Subconscious/Shared/Services/TranscludeService.swift +++ b/xcode/Subconscious/Shared/Services/TranscludeService.swift @@ -18,7 +18,7 @@ actor TranscludeService { func fetchTranscludes( slashlinks: [Slashlink] ) async throws -> [Slashlink: EntryStub] { - let entries = try database.listEntriesForSlashlinks(slashlinks: slashlinks) + let entries = try database.listEntries(for: slashlinks) return Dictionary( From e473e2a51190580091eac8d0beda3e484704f979 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Mon, 8 May 2023 17:11:21 +1000 Subject: [PATCH 06/23] Prefer short dictionary syntax --- xcode/Subconscious/Shared/Components/Common/SubtextView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xcode/Subconscious/Shared/Components/Common/SubtextView.swift b/xcode/Subconscious/Shared/Components/Common/SubtextView.swift index 739fd95d..8ae08d6a 100644 --- a/xcode/Subconscious/Shared/Components/Common/SubtextView.swift +++ b/xcode/Subconscious/Shared/Components/Common/SubtextView.swift @@ -10,7 +10,7 @@ import SwiftUI struct SubtextView: View { private static var renderer = SubtextAttributedStringRenderer() var subtext: Subtext - var transcludes: Dictionary + var transcludes: [Slashlink: EntryStub] var onViewTransclude: (Slashlink) -> Void var body: some View { From 6637e1e19c76de52034f316b646b88202e5ce6fe Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Fri, 23 Jun 2023 14:02:18 +1000 Subject: [PATCH 07/23] Fetch transcludes locally Unsure if this works for 3P, gateways aren't working --- .../Shared/Components/AppView.swift | 2 +- .../Detail/MemoViewerDetailView.swift | 9 ++++++--- .../Notebook/NotebookNavigationView.swift | 5 ++--- .../Shared/Services/DatabaseService.swift | 18 ++++++++++-------- .../Shared/Services/TranscludeService.swift | 13 ++++++++++--- 5 files changed, 29 insertions(+), 18 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/AppView.swift b/xcode/Subconscious/Shared/Components/AppView.swift index 50b1e1f5..07b14145 100644 --- a/xcode/Subconscious/Shared/Components/AppView.swift +++ b/xcode/Subconscious/Shared/Components/AppView.swift @@ -2046,7 +2046,7 @@ struct AppEnvironment { self.feed = FeedService() self.gatewayProvisioningService = GatewayProvisioningService() - self.transclude = TranscludeService(database: database) + self.transclude = TranscludeService(database: database, noosphere: noosphere) } } diff --git a/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift b/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift index 59c4abbe..702131c5 100644 --- a/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift +++ b/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift @@ -363,8 +363,7 @@ struct MemoViewerDetailModel: ModelProtocol { state: model, // Set meta sheet address as well actions: [ - .setMetaSheetAddress(description.address), - .fetchTranscludes + .setMetaSheetAddress(description.address) ], environment: environment ).mergeFx(fx) @@ -405,7 +404,11 @@ struct MemoViewerDetailModel: ModelProtocol { ) -> Update { var model = state model.dom = dom - return Update(state: model) + return update( + state: model, + action: .fetchTranscludes, + environment: environment + ) } static func presentMetaSheet( diff --git a/xcode/Subconscious/Shared/Components/Notebook/NotebookNavigationView.swift b/xcode/Subconscious/Shared/Components/Notebook/NotebookNavigationView.swift index 19c9c037..5ddb385f 100644 --- a/xcode/Subconscious/Shared/Components/Notebook/NotebookNavigationView.swift +++ b/xcode/Subconscious/Shared/Components/Notebook/NotebookNavigationView.swift @@ -25,9 +25,8 @@ struct NotebookNavigationView: View { onEntryPress: { entry in store.send( .pushDetail( - MemoEditorDetailDescription( - address: entry.address, - fallback: entry.excerpt + MemoViewerDetailDescription( + address: entry.address ) ) ) diff --git a/xcode/Subconscious/Shared/Services/DatabaseService.swift b/xcode/Subconscious/Shared/Services/DatabaseService.swift index 0005f760..7f3a4d2c 100644 --- a/xcode/Subconscious/Shared/Services/DatabaseService.swift +++ b/xcode/Subconscious/Shared/Services/DatabaseService.swift @@ -434,13 +434,13 @@ final class DatabaseService { return results.col(0)?.toInt() } - func listEntries(for slashlinks: [Slashlink]) throws -> [EntryStub] { + func listEntries(for slashlinks: [Slashlink], owner: Did?) throws -> [EntryStub] { return try slashlinks.compactMap({ slashlink in - return try readEntry(for: slashlink) + return try readEntry(for: slashlink, owner: owner) }) } - func readEntry(for slashlink: Slashlink) throws -> EntryStub? { + func readEntry(for slashlink: Slashlink, owner: Did?) throws -> EntryStub? { guard self.state == .ready else { throw DatabaseServiceError.notReady } @@ -449,20 +449,22 @@ final class DatabaseService { sql: """ SELECT id, modified, excerpt FROM memo - WHERE id IN (?, ?) + WHERE slashlink = ? ORDER BY modified DESC LIMIT 1000 """, parameters: [ - .text( - slashlink.description // TODO: this might be wrong - ), + .text(slashlink.markup), ] ) return results.compactMap({ row in guard - let address = row.col(0)?.toString()?.toSlashlink(), + let address = row.col(0)? + .toString()? + .toLink()? + .toSlashlink()? + .relativizeIfNeeded(did: owner), let modified = row.col(1)?.toDate(), let excerpt = row.col(2)?.toString() else { diff --git a/xcode/Subconscious/Shared/Services/TranscludeService.swift b/xcode/Subconscious/Shared/Services/TranscludeService.swift index 2409bb37..01611204 100644 --- a/xcode/Subconscious/Shared/Services/TranscludeService.swift +++ b/xcode/Subconscious/Shared/Services/TranscludeService.swift @@ -10,20 +10,27 @@ import Combine actor TranscludeService { private var database: DatabaseService + private var noosphere: NoosphereService - init(database: DatabaseService) { + init(database: DatabaseService, noosphere: NoosphereService) { self.database = database + self.noosphere = noosphere } func fetchTranscludes( slashlinks: [Slashlink] ) async throws -> [Slashlink: EntryStub] { - let entries = try database.listEntries(for: slashlinks) + let identity = try await noosphere.identity() + let entries = try database.listEntries(for: slashlinks, owner: identity) return Dictionary( entries.map { entry in - (entry.address, entry) + if entry.address.isLocal { + return (Slashlink(slug: entry.address.slug), entry) + } + + return (entry.address, entry) }, uniquingKeysWith: { a, b in a} ) From a13a74e22a85f3eb1b3b90d7a32806392b7b2f92 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Fri, 23 Jun 2023 14:02:27 +1000 Subject: [PATCH 08/23] Pin exact version of CodeScanner --- xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj b/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj index 06bd2f69..48dc5f00 100644 --- a/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj +++ b/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj @@ -2522,8 +2522,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/twostraws/CodeScanner"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 2.0.0; + kind = exactVersion; + version = 2.3.2; }; }; B822F18927C9615600943C6B /* XCRemoteSwiftPackageReference "ObservableStore" */ = { From de4ea4cfc034fe8b610da2456ff21cd67cddfb7d Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Fri, 23 Jun 2023 14:10:23 +1000 Subject: [PATCH 09/23] Extract private function --- .../Components/Common/SubtextView.swift | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Common/SubtextView.swift b/xcode/Subconscious/Shared/Components/Common/SubtextView.swift index 8ae08d6a..fca9a68b 100644 --- a/xcode/Subconscious/Shared/Components/Common/SubtextView.swift +++ b/xcode/Subconscious/Shared/Components/Common/SubtextView.swift @@ -12,36 +12,34 @@ struct SubtextView: View { var subtext: Subtext var transcludes: [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 transcludes[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)) - - let entries: [EntryStub] = - block.slashlinks - .compactMap { link in - guard let slashlink = link.toSlashlink() else { - return nil + ForEach(self.entries(for: block), id: \.self) { entry in + Transclude2View( + address: entry.address, + excerpt: entry.excerpt, + action: { + onViewTransclude(entry.address) } - - return transcludes[slashlink] - } - .filter { entry in - // Avoid empty transclude blocks - entry.excerpt.count > 0 - } - - if entries.count > 0 { - ForEach(entries, id: \.self) { entry in - Transclude2View( - address: entry.address, - excerpt: entry.excerpt, - action: { - onViewTransclude(entry.address) - } - ) - } + ) } } } From 98d56974dc8b41bd75d025953bd01edb6464f18c Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Fri, 23 Jun 2023 14:13:17 +1000 Subject: [PATCH 10/23] Extract update method --- .../Detail/MemoViewerDetailView.swift | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift b/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift index 702131c5..cbb6b0df 100644 --- a/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift +++ b/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift @@ -316,24 +316,11 @@ struct MemoViewerDetailModel: ModelProtocol { environment: environment, isPresented: isPresented ) - case .fetchTranscludes: - let links = state.dom.slashlinks - .map { value in value.toSlashlink() } - .compactMap { value in value } - - let fx: Fx = - environment.transclude - .fetchTranscludesPublisher(slashlinks: links) - .map { entries in - MemoViewerDetailAction.succeedFetchTranscludes(entries) - } - .recover { error in - MemoViewerDetailAction.failFetchTranscludes(error.localizedDescription) - } - .eraseToAnyPublisher() - - return Update(state: state, fx: fx) + return fetchTranscludes( + state: state, + environment: environment + ) case .succeedFetchTranscludes(let transcludes): var model = state model.transcludes = transcludes @@ -411,6 +398,28 @@ struct MemoViewerDetailModel: ModelProtocol { ) } + static func fetchTranscludes( + state: MemoViewerDetailModel, + environment: MemoViewerDetailModel.Environment + ) -> Update { + let links = state.dom.slashlinks + .map { value in value.toSlashlink() } + .compactMap { value in value } + + let fx: Fx = + environment.transclude + .fetchTranscludesPublisher(slashlinks: links) + .map { entries in + MemoViewerDetailAction.succeedFetchTranscludes(entries) + } + .recover { error in + MemoViewerDetailAction.failFetchTranscludes(error.localizedDescription) + } + .eraseToAnyPublisher() + + return Update(state: state, fx: fx) + } + static func presentMetaSheet( state: Self, environment: Environment, From d3895da28aa239e16807e001da7f2fba206b395d Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Fri, 23 Jun 2023 14:14:31 +1000 Subject: [PATCH 11/23] Restore normal notebook action --- .../Shared/Components/Notebook/NotebookNavigationView.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Notebook/NotebookNavigationView.swift b/xcode/Subconscious/Shared/Components/Notebook/NotebookNavigationView.swift index 5ddb385f..19c9c037 100644 --- a/xcode/Subconscious/Shared/Components/Notebook/NotebookNavigationView.swift +++ b/xcode/Subconscious/Shared/Components/Notebook/NotebookNavigationView.swift @@ -25,8 +25,9 @@ struct NotebookNavigationView: View { onEntryPress: { entry in store.send( .pushDetail( - MemoViewerDetailDescription( - address: entry.address + MemoEditorDetailDescription( + address: entry.address, + fallback: entry.excerpt ) ) ) From 94c15a9906c41b94bfe1eac1fc16b3ce66c797eb Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Sat, 24 Jun 2023 16:20:43 +1000 Subject: [PATCH 12/23] Hacky but seemingly functional transcludes! --- .../Detail/MemoViewerDetailView.swift | 41 ++++++++++++++++++- .../Shared/Models/Slashlink.swift | 9 ++++ .../Shared/Services/DatabaseService.swift | 13 +++--- .../Shared/Services/TranscludeService.swift | 23 ++++++++--- 4 files changed, 70 insertions(+), 16 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift b/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift index cbb6b0df..4f34d9a9 100644 --- a/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift +++ b/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift @@ -235,6 +235,10 @@ enum MemoViewerDetailAction: Hashable { case succeedFetchTranscludes([Slashlink: EntryStub]) case failFetchTranscludes(_ error: String) + case fetchOwnerProfile + case succeedFetchOwnerProfile(UserProfile) + case failFetchOwnerProfile(_ error: String) + /// Synonym for `.metaSheet(.setAddress(_))` static func setMetaSheetAddress(_ address: Slashlink) -> Self { .metaSheet(.setAddress(address)) @@ -264,6 +268,7 @@ struct MemoViewerDetailModel: ModelProtocol { var loadingState = LoadingState.loading + var owner: UserProfile? var address: Slashlink? var defaultAudience = Audience.local var title = "" @@ -329,6 +334,32 @@ struct MemoViewerDetailModel: ModelProtocol { case .failFetchTranscludes(let error): logger.error("Failed to fetch transcludes: \(error)") return Update(state: state) + + case .fetchOwnerProfile: + let fx: Fx = Future.detached { + if let petname = state.address?.toPetname() { + return try await environment.userProfile.requestUserProfile(petname: petname) + .profile + } else { + return try await environment.userProfile.requestOurProfile().profile + } + } + .map { profile in + .succeedFetchOwnerProfile(profile) + } + .recover { error in + .failFetchOwnerProfile(error.localizedDescription) + } + .eraseToAnyPublisher() + + return Update(state: state, fx: fx) + case .succeedFetchOwnerProfile(let profile): + var model = state + model.owner = profile + return update(state: model, action: .fetchTranscludes, environment: environment) + case .failFetchOwnerProfile(let error): + logger.error("Failed to fetch owner: \(error)") + return Update(state: state) } } @@ -350,7 +381,8 @@ struct MemoViewerDetailModel: ModelProtocol { state: model, // Set meta sheet address as well actions: [ - .setMetaSheetAddress(description.address) + .setMetaSheetAddress(description.address), + .fetchOwnerProfile ], environment: environment ).mergeFx(fx) @@ -402,13 +434,18 @@ struct MemoViewerDetailModel: ModelProtocol { state: MemoViewerDetailModel, environment: MemoViewerDetailModel.Environment ) -> Update { + + guard let owner = state.owner else { + return Update(state: state) + } + let links = state.dom.slashlinks .map { value in value.toSlashlink() } .compactMap { value in value } let fx: Fx = environment.transclude - .fetchTranscludesPublisher(slashlinks: links) + .fetchTranscludesPublisher(slashlinks: links, owner: owner) .map { entries in MemoViewerDetailAction.succeedFetchTranscludes(entries) } diff --git a/xcode/Subconscious/Shared/Models/Slashlink.swift b/xcode/Subconscious/Shared/Models/Slashlink.swift index 5490695f..fa99092c 100644 --- a/xcode/Subconscious/Shared/Models/Slashlink.swift +++ b/xcode/Subconscious/Shared/Models/Slashlink.swift @@ -224,6 +224,15 @@ extension Slashlink { return self } } + + func relativizeIfNeeded(petname base: Petname?) -> Slashlink { + switch self.peer { + case .petname(let name) where name == base: + return Slashlink(slug: self.slug) + default: + return self + } + } /// Get petname from slashlink (if any) func toPetname() -> Petname? { diff --git a/xcode/Subconscious/Shared/Services/DatabaseService.swift b/xcode/Subconscious/Shared/Services/DatabaseService.swift index 7f3a4d2c..b342e2a8 100644 --- a/xcode/Subconscious/Shared/Services/DatabaseService.swift +++ b/xcode/Subconscious/Shared/Services/DatabaseService.swift @@ -434,20 +434,20 @@ final class DatabaseService { return results.col(0)?.toInt() } - func listEntries(for slashlinks: [Slashlink], owner: Did?) throws -> [EntryStub] { + func listEntries(for slashlinks: [Slashlink], owner: Petname?) throws -> [EntryStub] { return try slashlinks.compactMap({ slashlink in return try readEntry(for: slashlink, owner: owner) }) } - func readEntry(for slashlink: Slashlink, owner: Did?) throws -> EntryStub? { + func readEntry(for slashlink: Slashlink, owner: Petname?) throws -> EntryStub? { guard self.state == .ready else { throw DatabaseServiceError.notReady } let results = try database.execute( sql: """ - SELECT id, modified, excerpt + SELECT slashlink, modified, excerpt FROM memo WHERE slashlink = ? ORDER BY modified DESC @@ -462,9 +462,8 @@ final class DatabaseService { guard let address = row.col(0)? .toString()? - .toLink()? .toSlashlink()? - .relativizeIfNeeded(did: owner), + .relativizeIfNeeded(petname: owner), let modified = row.col(1)?.toDate(), let excerpt = row.col(2)?.toString() else { @@ -507,9 +506,7 @@ final class DatabaseService { guard let address = row.col(0)? .toString()? - .toLink()? - .toSlashlink()? - .relativizeIfNeeded(did: owner), + .toSlashlink(), let modified = row.col(1)?.toDate(), let excerpt = row.col(2)?.toString() else { diff --git a/xcode/Subconscious/Shared/Services/TranscludeService.swift b/xcode/Subconscious/Shared/Services/TranscludeService.swift index 01611204..56a58fc7 100644 --- a/xcode/Subconscious/Shared/Services/TranscludeService.swift +++ b/xcode/Subconscious/Shared/Services/TranscludeService.swift @@ -18,10 +18,20 @@ actor TranscludeService { } func fetchTranscludes( - slashlinks: [Slashlink] + slashlinks: [Slashlink], + owner: UserProfile ) async throws -> [Slashlink: EntryStub] { - let identity = try await noosphere.identity() - let entries = try database.listEntries(for: slashlinks, owner: identity) + let petname = owner.address.petname + + let slashlinks = slashlinks.map { s in + if case let .petname(basePetname) = s.peer { + return s.rebaseIfNeeded(petname: basePetname) + } else { + return Slashlink(peer: owner.address.peer, slug: s.slug) + } + } + + let entries = try database.listEntries(for: slashlinks, owner: petname) return Dictionary( @@ -30,7 +40,7 @@ actor TranscludeService { return (Slashlink(slug: entry.address.slug), entry) } - return (entry.address, entry) + return (entry.address.relativizeIfNeeded(petname: petname), entry) }, uniquingKeysWith: { a, b in a} ) @@ -38,10 +48,11 @@ actor TranscludeService { nonisolated func fetchTranscludesPublisher( - slashlinks: [Slashlink] + slashlinks: [Slashlink], + owner: UserProfile ) -> AnyPublisher<[Slashlink: EntryStub], Error> { Future.detached(priority: .utility) { - try await self.fetchTranscludes(slashlinks: slashlinks) + try await self.fetchTranscludes(slashlinks: slashlinks, owner: owner) } .eraseToAnyPublisher() } From 5c629b25fbb0f9b68d5a561a3e7a991d02a20221 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Mon, 26 Jun 2023 13:54:23 +1000 Subject: [PATCH 13/23] Fix thread blocking sleep --- xcode/Subconscious/Shared/Library/Func.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xcode/Subconscious/Shared/Library/Func.swift b/xcode/Subconscious/Shared/Library/Func.swift index 6bc1e214..2ab84767 100644 --- a/xcode/Subconscious/Shared/Library/Func.swift +++ b/xcode/Subconscious/Shared/Library/Func.swift @@ -77,8 +77,8 @@ struct Func { powf(2.0, Float(attempts)) ) ) - sleep(seconds) - + try await Task.sleep(for: .seconds(seconds)) + return try await self.retryWithBackoff( maxAttempts: maxAttempts, maxWaitSeconds: maxWaitSeconds, From ecbe9114f24b07ead3827a7c57c2d36470706e2e Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Mon, 26 Jun 2023 13:54:35 +1000 Subject: [PATCH 14/23] Hacky re-writing of addresses --- .../Subconscious/Shared/Services/TranscludeService.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/xcode/Subconscious/Shared/Services/TranscludeService.swift b/xcode/Subconscious/Shared/Services/TranscludeService.swift index 56a58fc7..d7a52630 100644 --- a/xcode/Subconscious/Shared/Services/TranscludeService.swift +++ b/xcode/Subconscious/Shared/Services/TranscludeService.swift @@ -40,7 +40,13 @@ actor TranscludeService { return (Slashlink(slug: entry.address.slug), entry) } - return (entry.address.relativizeIfNeeded(petname: petname), entry) + guard let petname = petname else { + return (entry.address.relativizeIfNeeded(petname: petname), entry) + } + + let x = EntryStub(address: entry.address.rebaseIfNeeded(petname: petname), excerpt: entry.excerpt, modified: entry.modified) + + return (entry.address.relativizeIfNeeded(petname: petname), x) }, uniquingKeysWith: { a, b in a} ) From 864936dfa526aa215355017b0c8cf7fb7d16b57d Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Mon, 26 Jun 2023 14:30:30 +1000 Subject: [PATCH 15/23] Cleanup, rename, simplify --- .../Components/Common/SubtextView.swift | 6 +-- .../Detail/MemoViewerDetailView.swift | 38 +++++++++---------- .../Shared/Services/DatabaseService.swift | 3 +- .../Shared/Services/TranscludeService.swift | 31 ++++++++------- 4 files changed, 40 insertions(+), 38 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Common/SubtextView.swift b/xcode/Subconscious/Shared/Components/Common/SubtextView.swift index fca9a68b..72f0c73c 100644 --- a/xcode/Subconscious/Shared/Components/Common/SubtextView.swift +++ b/xcode/Subconscious/Shared/Components/Common/SubtextView.swift @@ -10,7 +10,7 @@ import SwiftUI struct SubtextView: View { private static var renderer = SubtextAttributedStringRenderer() var subtext: Subtext - var transcludes: [Slashlink: EntryStub] + var transcludePreviews: [Slashlink: EntryStub] var onViewTransclude: (Slashlink) -> Void private func entries(for block: Subtext.Block) -> [EntryStub] { @@ -20,7 +20,7 @@ struct SubtextView: View { return nil } - return transcludes[slashlink] + return transcludePreviews[slashlink] } .filter { entry in // Avoid empty transclude blocks @@ -78,7 +78,7 @@ struct SubtextView_Previews: PreviewProvider { Yea, I shall return with the tide, """ ), - transcludes: [ + 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) diff --git a/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift b/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift index 4f34d9a9..d41bb7ee 100644 --- a/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift +++ b/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift @@ -35,7 +35,7 @@ struct MemoViewerDetailView: View { MemoViewerDetailLoadedView( title: store.state.title, dom: store.state.dom, - transcludes: store.state.transcludes, + transcludePreviews: store.state.transcludePreviews, address: description.address, backlinks: store.state.backlinks, send: store.send, @@ -128,7 +128,7 @@ struct MemoViewerDetailLoadingView: View { struct MemoViewerDetailLoadedView: View { var title: String var dom: Subtext - var transcludes: [Slashlink: EntryStub] + var transcludePreviews: [Slashlink: EntryStub] var address: Slashlink var backlinks: [EntryStub] var send: (MemoViewerDetailAction) -> Void @@ -179,7 +179,7 @@ struct MemoViewerDetailLoadedView: View { VStack { SubtextView( subtext: dom, - transcludes: transcludes, + transcludePreviews: transcludePreviews, onViewTransclude: self.onViewTransclude ).textSelection( .enabled @@ -231,9 +231,9 @@ enum MemoViewerDetailAction: Hashable { case failLoadDetail(_ message: String) case presentMetaSheet(_ isPresented: Bool) - case fetchTranscludes - case succeedFetchTranscludes([Slashlink: EntryStub]) - case failFetchTranscludes(_ error: String) + case fetchTranscludePreviews + case succeedFetchTranscludePreviews([Slashlink: EntryStub]) + case failFetchTranscludePreviews(_ error: String) case fetchOwnerProfile case succeedFetchOwnerProfile(UserProfile) @@ -280,7 +280,7 @@ struct MemoViewerDetailModel: ModelProtocol { var metaSheet = MemoViewerDetailMetaSheetModel() /// Transclude block preview cache - var transcludes: [Slashlink: EntryStub] = [:] + var transcludePreviews: [Slashlink: EntryStub] = [:] static func update( state: Self, @@ -321,17 +321,17 @@ struct MemoViewerDetailModel: ModelProtocol { environment: environment, isPresented: isPresented ) - case .fetchTranscludes: - return fetchTranscludes( + case .fetchTranscludePreviews: + return fetchTranscludePreviews( state: state, environment: environment ) - case .succeedFetchTranscludes(let transcludes): + case .succeedFetchTranscludePreviews(let transcludes): var model = state - model.transcludes = transcludes + model.transcludePreviews = transcludes return Update(state: model) - case .failFetchTranscludes(let error): + case .failFetchTranscludePreviews(let error): logger.error("Failed to fetch transcludes: \(error)") return Update(state: state) @@ -356,7 +356,7 @@ struct MemoViewerDetailModel: ModelProtocol { case .succeedFetchOwnerProfile(let profile): var model = state model.owner = profile - return update(state: model, action: .fetchTranscludes, environment: environment) + return update(state: model, action: .fetchTranscludePreviews, environment: environment) case .failFetchOwnerProfile(let error): logger.error("Failed to fetch owner: \(error)") return Update(state: state) @@ -425,12 +425,12 @@ struct MemoViewerDetailModel: ModelProtocol { model.dom = dom return update( state: model, - action: .fetchTranscludes, + action: .fetchTranscludePreviews, environment: environment ) } - static func fetchTranscludes( + static func fetchTranscludePreviews( state: MemoViewerDetailModel, environment: MemoViewerDetailModel.Environment ) -> Update { @@ -445,12 +445,12 @@ struct MemoViewerDetailModel: ModelProtocol { let fx: Fx = environment.transclude - .fetchTranscludesPublisher(slashlinks: links, owner: owner) + .fetchTranscludePreviewsPublisher(slashlinks: links, owner: owner) .map { entries in - MemoViewerDetailAction.succeedFetchTranscludes(entries) + MemoViewerDetailAction.succeedFetchTranscludePreviews(entries) } .recover { error in - MemoViewerDetailAction.failFetchTranscludes(error.localizedDescription) + MemoViewerDetailAction.failFetchTranscludePreviews(error.localizedDescription) } .eraseToAnyPublisher() @@ -512,7 +512,7 @@ struct MemoViewerDetailView_Previews: PreviewProvider { The soul unfolds itself, like a [[lotus]] of countless petals. """ ), - transcludes: [ + 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.\"", diff --git a/xcode/Subconscious/Shared/Services/DatabaseService.swift b/xcode/Subconscious/Shared/Services/DatabaseService.swift index b342e2a8..8ca40a0b 100644 --- a/xcode/Subconscious/Shared/Services/DatabaseService.swift +++ b/xcode/Subconscious/Shared/Services/DatabaseService.swift @@ -462,8 +462,7 @@ final class DatabaseService { guard let address = row.col(0)? .toString()? - .toSlashlink()? - .relativizeIfNeeded(petname: owner), + .toSlashlink(), let modified = row.col(1)?.toDate(), let excerpt = row.col(2)?.toString() else { diff --git a/xcode/Subconscious/Shared/Services/TranscludeService.swift b/xcode/Subconscious/Shared/Services/TranscludeService.swift index d7a52630..8bb0bfa3 100644 --- a/xcode/Subconscious/Shared/Services/TranscludeService.swift +++ b/xcode/Subconscious/Shared/Services/TranscludeService.swift @@ -17,18 +17,19 @@ actor TranscludeService { self.noosphere = noosphere } - func fetchTranscludes( + func fetchTranscludePreviews( slashlinks: [Slashlink], owner: UserProfile ) async throws -> [Slashlink: EntryStub] { let petname = owner.address.petname - let slashlinks = slashlinks.map { s in - if case let .petname(basePetname) = s.peer { - return s.rebaseIfNeeded(petname: basePetname) - } else { - return Slashlink(peer: owner.address.peer, slug: s.slug) + let slashlinks = slashlinks.map { address in + guard case .petname(_) = address.peer else { + // Rebase relative slashlinks to the owner's handle + return Slashlink(peer: owner.address.peer, slug: address.slug) } + + return address } let entries = try database.listEntries(for: slashlinks, owner: petname) @@ -36,29 +37,31 @@ actor TranscludeService { return Dictionary( entries.map { entry in + // If it's did:local we know where to look if entry.address.isLocal { return (Slashlink(slug: entry.address.slug), entry) } - guard let petname = petname else { - return (entry.address.relativizeIfNeeded(petname: petname), entry) - } - - let x = EntryStub(address: entry.address.rebaseIfNeeded(petname: petname), excerpt: entry.excerpt, modified: entry.modified) + // Ensure links to the owner's content are relativized to match + // the in-text representation + let displayAddress = entry.address.relativizeIfNeeded(petname: petname) - return (entry.address.relativizeIfNeeded(petname: petname), x) + return (displayAddress, entry) }, uniquingKeysWith: { a, b in a} ) } - nonisolated func fetchTranscludesPublisher( + nonisolated func fetchTranscludePreviewsPublisher( slashlinks: [Slashlink], owner: UserProfile ) -> AnyPublisher<[Slashlink: EntryStub], Error> { Future.detached(priority: .utility) { - try await self.fetchTranscludes(slashlinks: slashlinks, owner: owner) + try await self.fetchTranscludePreviews( + slashlinks: slashlinks, + owner: owner + ) } .eraseToAnyPublisher() } From d819d9595c25df84f076c3433fe1093906ae9714 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Mon, 26 Jun 2023 14:32:04 +1000 Subject: [PATCH 16/23] Remove useless comment --- .../Shared/Components/Detail/MemoViewerDetailView.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift b/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift index d41bb7ee..2977b62f 100644 --- a/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift +++ b/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift @@ -279,7 +279,6 @@ struct MemoViewerDetailModel: ModelProtocol { var isMetaSheetPresented = false var metaSheet = MemoViewerDetailMetaSheetModel() - /// Transclude block preview cache var transcludePreviews: [Slashlink: EntryStub] = [:] static func update( From 7d202ec7ebbd535b0c919b987abaef7d14f2ef8f Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Mon, 26 Jun 2023 14:35:01 +1000 Subject: [PATCH 17/23] Extract functions from viewer update --- .../Detail/MemoViewerDetailView.swift | 59 +++++++++++++------ 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift b/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift index 2977b62f..2b58db6c 100644 --- a/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift +++ b/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift @@ -281,6 +281,8 @@ struct MemoViewerDetailModel: ModelProtocol { var transcludePreviews: [Slashlink: EntryStub] = [:] + + static func update( state: Self, action: Action, @@ -335,27 +337,18 @@ struct MemoViewerDetailModel: ModelProtocol { return Update(state: state) case .fetchOwnerProfile: - let fx: Fx = Future.detached { - if let petname = state.address?.toPetname() { - return try await environment.userProfile.requestUserProfile(petname: petname) - .profile - } else { - return try await environment.userProfile.requestOurProfile().profile - } - } - .map { profile in - .succeedFetchOwnerProfile(profile) - } - .recover { error in - .failFetchOwnerProfile(error.localizedDescription) - } - .eraseToAnyPublisher() - - return Update(state: state, fx: fx) + return fetchOwnerProfile( + state: state, + environment: environment + ) case .succeedFetchOwnerProfile(let profile): var model = state model.owner = profile - return update(state: model, action: .fetchTranscludePreviews, environment: environment) + return update( + state: model, + action: .fetchTranscludePreviews, + environment: environment + ) case .failFetchOwnerProfile(let error): logger.error("Failed to fetch owner: \(error)") return Update(state: state) @@ -456,6 +449,36 @@ struct MemoViewerDetailModel: ModelProtocol { return Update(state: state, fx: fx) } + static func fetchOwnerProfile( + state: MemoViewerDetailModel, + environment: MemoViewerDetailModel.Environment + ) -> Update { + let fx: Fx = + 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( state: Self, environment: Environment, From bc2c72ac1988c0b0603bd5c88002ff78a590293d Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Mon, 26 Jun 2023 14:38:27 +1000 Subject: [PATCH 18/23] Restore listRecent --- xcode/Subconscious/Shared/Services/DatabaseService.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/xcode/Subconscious/Shared/Services/DatabaseService.swift b/xcode/Subconscious/Shared/Services/DatabaseService.swift index 8ca40a0b..21c91589 100644 --- a/xcode/Subconscious/Shared/Services/DatabaseService.swift +++ b/xcode/Subconscious/Shared/Services/DatabaseService.swift @@ -505,7 +505,9 @@ final class DatabaseService { guard let address = row.col(0)? .toString()? - .toSlashlink(), + .toLink()? + .toSlashlink()? + .relativizeIfNeeded(did: owner), let modified = row.col(1)?.toDate(), let excerpt = row.col(2)?.toString() else { From dc931a79f0d2475c1e79f377aaff14f87801c764 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Mon, 26 Jun 2023 14:45:26 +1000 Subject: [PATCH 19/23] Use shortcut syntax --- .../Components/Detail/MemoViewerDetailView.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift b/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift index 2b58db6c..85422369 100644 --- a/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift +++ b/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift @@ -164,11 +164,14 @@ struct MemoViewerDetailLoadedView: View { private func onViewTransclude( address: Slashlink ) { - if address.isOurs { - notify(.requestDetail(.editor(MemoEditorDetailDescription(address: address)))) - } else { - notify(.requestDetail(.viewer(MemoViewerDetailDescription(address: address)))) - } + notify( + .requestDetail( + MemoDetailDescription.from( + address: address, + fallback: address.description + ) + ) + ) } From 4801f27c1e840525c162bd3357a9ac082817ad76 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Mon, 26 Jun 2023 16:21:50 +1000 Subject: [PATCH 20/23] Add tests for transclude service --- .../SubconsciousTests/Tests_TranscludeService.swift | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 xcode/Subconscious/SubconsciousTests/Tests_TranscludeService.swift diff --git a/xcode/Subconscious/SubconsciousTests/Tests_TranscludeService.swift b/xcode/Subconscious/SubconsciousTests/Tests_TranscludeService.swift new file mode 100644 index 00000000..602f5938 --- /dev/null +++ b/xcode/Subconscious/SubconsciousTests/Tests_TranscludeService.swift @@ -0,0 +1,8 @@ +// +// Tests_TranscludeSerivce.swift +// SubconsciousTests +// +// Created by Ben Follington on 26/6/2023. +// + +import Foundation From a9ce26b6480cbc16dd1607237bb8437a6446f126 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Mon, 26 Jun 2023 16:22:00 +1000 Subject: [PATCH 21/23] Rename UserCategory.you -> .ourself --- .../Common/Byline/PetnameView.swift | 2 +- .../Common/Profile/EditProfileSheet.swift | 2 +- .../Profile/UserProfileHeaderView.swift | 8 +-- .../Common/Profile/UserProfileView.swift | 2 +- .../Common/Story/StoryUserView.swift | 8 +-- .../Detail/UserProfileDetailView.swift | 4 +- .../Shared/Components/Notebook/Notebook.swift | 2 +- .../Notebook/NotebookNavigationView.swift | 5 +- .../Shared/Library/DummyDataUtilities.swift | 16 +++++ .../Shared/Services/TranscludeService.swift | 2 +- .../Shared/Services/UserProfileService.swift | 2 +- .../Subconscious.xcodeproj/project.pbxproj | 4 ++ .../SubconsciousTests/TestUtilities.swift | 9 ++- .../Tests_TranscludeService.swift | 68 ++++++++++++++++++- 14 files changed, 112 insertions(+), 22 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Common/Byline/PetnameView.swift b/xcode/Subconscious/Shared/Components/Common/Byline/PetnameView.swift index b6326ad5..9ca42d0a 100644 --- a/xcode/Subconscious/Shared/Components/Common/Byline/PetnameView.swift +++ b/xcode/Subconscious/Shared/Components/Common/Byline/PetnameView.swift @@ -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) diff --git a/xcode/Subconscious/Shared/Components/Common/Profile/EditProfileSheet.swift b/xcode/Subconscious/Shared/Components/Common/Profile/EditProfileSheet.swift index cd4bd19f..903f2143 100644 --- a/xcode/Subconscious/Shared/Components/Common/Profile/EditProfileSheet.swift +++ b/xcode/Subconscious/Shared/Components/Common/Profile/EditProfileSheet.swift @@ -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 ) diff --git a/xcode/Subconscious/Shared/Components/Common/Profile/UserProfileHeaderView.swift b/xcode/Subconscious/Shared/Components/Common/Profile/UserProfileHeaderView.swift index a42bb4dc..ddadc8a7 100644 --- a/xcode/Subconscious/Shared/Components/Common/Profile/UserProfileHeaderView.swift +++ b/xcode/Subconscious/Shared/Components/Common/Profile/UserProfileHeaderView.swift @@ -40,7 +40,7 @@ struct UserProfileHeaderView: View { Button( action: { switch (user.category, user.ourFollowStatus) { - case (.you, _): + case (.ourself, _): action(.editOwnProfile) case (_, .following(_)): action(.requestUnfollow) @@ -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) @@ -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) } } @@ -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 ) diff --git a/xcode/Subconscious/Shared/Components/Common/Profile/UserProfileView.swift b/xcode/Subconscious/Shared/Components/Common/Profile/UserProfileView.swift index 4311cbde..a083932f 100644 --- a/xcode/Subconscious/Shared/Components/Common/Profile/UserProfileView.swift +++ b/xcode/Subconscious/Shared/Components/Common/Profile/UserProfileView.swift @@ -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: { diff --git a/xcode/Subconscious/Shared/Components/Common/Story/StoryUserView.swift b/xcode/Subconscious/Shared/Components/Common/Story/StoryUserView.swift index 430fe5a8..e9c48ede 100644 --- a/xcode/Subconscious/Shared/Components/Common/Story/StoryUserView.swift +++ b/xcode/Subconscious/Shared/Components/Common/Story/StoryUserView.swift @@ -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 (_, _): @@ -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) @@ -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 ) @@ -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 ) diff --git a/xcode/Subconscious/Shared/Components/Detail/UserProfileDetailView.swift b/xcode/Subconscious/Shared/Components/Detail/UserProfileDetailView.swift index 15b548a3..a22bf398 100644 --- a/xcode/Subconscious/Shared/Components/Detail/UserProfileDetailView.swift +++ b/xcode/Subconscious/Shared/Components/Detail/UserProfileDetailView.swift @@ -168,7 +168,7 @@ struct UserProfileStatistics: Equatable, Codable, Hashable { enum UserCategory: Equatable, Codable, Hashable, CaseIterable { case human case geist - case you + case ourself } struct UserProfile: Equatable, Codable, Hashable { @@ -698,7 +698,7 @@ struct UserProfileDetailModel: ModelProtocol { // Refresh our profile & show the following list if we followed someone new // This matters if we used the manual "Follow User" form if let user = state.user { - if user.category == .you { + if user.category == .ourself { actions.append(.tabIndexSelected(Self.followingTabIndex)) } } diff --git a/xcode/Subconscious/Shared/Components/Notebook/Notebook.swift b/xcode/Subconscious/Shared/Components/Notebook/Notebook.swift index 5d1733e5..720c2e02 100644 --- a/xcode/Subconscious/Shared/Components/Notebook/Notebook.swift +++ b/xcode/Subconscious/Shared/Components/Notebook/Notebook.swift @@ -284,7 +284,7 @@ extension NotebookAction { case let .requestNavigateToProfile(user): let user = Func.run { switch (user.category, user.ourFollowStatus) { - case (.you, _): + case (.ourself, _): // Loop back to our profile return user.overrideAddress(Slashlink.ourProfile) case (_, .following(let name)): diff --git a/xcode/Subconscious/Shared/Components/Notebook/NotebookNavigationView.swift b/xcode/Subconscious/Shared/Components/Notebook/NotebookNavigationView.swift index 19c9c037..5ddb385f 100644 --- a/xcode/Subconscious/Shared/Components/Notebook/NotebookNavigationView.swift +++ b/xcode/Subconscious/Shared/Components/Notebook/NotebookNavigationView.swift @@ -25,9 +25,8 @@ struct NotebookNavigationView: View { onEntryPress: { entry in store.send( .pushDetail( - MemoEditorDetailDescription( - address: entry.address, - fallback: entry.excerpt + MemoViewerDetailDescription( + address: entry.address ) ) ) diff --git a/xcode/Subconscious/Shared/Library/DummyDataUtilities.swift b/xcode/Subconscious/Shared/Library/DummyDataUtilities.swift index cbbd89d1..eec2c1a4 100644 --- a/xcode/Subconscious/Shared/Library/DummyDataUtilities.swift +++ b/xcode/Subconscious/Shared/Library/DummyDataUtilities.swift @@ -180,6 +180,22 @@ extension UserProfile: DummyData { ourFollowStatus: .notFollowing ) } + + static func dummyData(category: UserCategory) -> UserProfile { + let nickname = Petname.Name.dummyData() + return UserProfile( + did: Did.dummyData(), + nickname: nickname, + address: category == .ourself + ? Slashlink.ourProfile + : Slashlink(petname: nickname.toPetname()), + pfp: .image(String.dummyProfilePicture()), + bio: UserProfileBio.dummyData(), + category: category, + resolutionStatus: .unresolved, + ourFollowStatus: .notFollowing + ) + } } extension UserProfileStatistics: DummyData { diff --git a/xcode/Subconscious/Shared/Services/TranscludeService.swift b/xcode/Subconscious/Shared/Services/TranscludeService.swift index 8bb0bfa3..fe9d9c7e 100644 --- a/xcode/Subconscious/Shared/Services/TranscludeService.swift +++ b/xcode/Subconscious/Shared/Services/TranscludeService.swift @@ -25,7 +25,7 @@ actor TranscludeService { let slashlinks = slashlinks.map { address in guard case .petname(_) = address.peer else { - // Rebase relative slashlinks to the owner's handle + // Rebase relative slashlinks to the owner's handle (if they have one) return Slashlink(peer: owner.address.peer, slug: address.slug) } diff --git a/xcode/Subconscious/Shared/Services/UserProfileService.swift b/xcode/Subconscious/Shared/Services/UserProfileService.swift index b15cf3c1..3bb84571 100644 --- a/xcode/Subconscious/Shared/Services/UserProfileService.swift +++ b/xcode/Subconscious/Shared/Services/UserProfileService.swift @@ -188,7 +188,7 @@ actor UserProfileService { address: address, pfp: .none(did), bio: UserProfileBio(userProfileData?.bio ?? ""), - category: isOurs ? UserCategory.you : UserCategory.human, + category: isOurs ? UserCategory.ourself : UserCategory.human, resolutionStatus: resolutionStatus, ourFollowStatus: followingStatus ) diff --git a/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj b/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj index 48dc5f00..5d574b5e 100644 --- a/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj +++ b/xcode/Subconscious/Subconscious.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ B5690C3D29FB4DEF00067580 /* DidQrCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B569971529B6A0DF003204FC /* DidQrCodeView.swift */; }; B569971629B6A0DF003204FC /* FollowUserViaQRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B569971429B6A0DF003204FC /* FollowUserViaQRCodeView.swift */; }; B569971729B6A0DF003204FC /* DidQrCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B569971529B6A0DF003204FC /* DidQrCodeView.swift */; }; + B56C2D4E2A4962D00062DAC0 /* Tests_TranscludeService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56C2D4D2A4962D00062DAC0 /* Tests_TranscludeService.swift */; }; B56C3D3E2A01E5020071EF70 /* InviteCode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56C3D3D2A01E5020071EF70 /* InviteCode.swift */; }; B575834528ED8D9100F6EE88 /* combo.json in Resources */ = {isa = PBXBuildFile; fileRef = B575834428ED8D9100F6EE88 /* combo.json */; }; B579FA922A1AE4D1008A4D2F /* ResolutionStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = B579FA912A1AE4D1008A4D2F /* ResolutionStatus.swift */; }; @@ -465,6 +466,7 @@ B54B922728E669D6003ACA1F /* MementoGeist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MementoGeist.swift; sourceTree = ""; }; B569971429B6A0DF003204FC /* FollowUserViaQRCodeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FollowUserViaQRCodeView.swift; sourceTree = ""; }; B569971529B6A0DF003204FC /* DidQrCodeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DidQrCodeView.swift; sourceTree = ""; }; + B56C2D4D2A4962D00062DAC0 /* Tests_TranscludeService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_TranscludeService.swift; sourceTree = ""; }; B56C3D3D2A01E5020071EF70 /* InviteCode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InviteCode.swift; sourceTree = ""; }; B575834428ED8D9100F6EE88 /* combo.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = combo.json; sourceTree = ""; }; B579FA912A1AE4D1008A4D2F /* ResolutionStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResolutionStatus.swift; sourceTree = ""; }; @@ -902,6 +904,7 @@ B80890A92A0693C40087E091 /* Tests_HeaderSubtext.swift */, B840CCC62A0C1F840000C025 /* Tests_Audience.swift */, B8099F012A3B6FA50014FC2E /* Tests_MemoRecord.swift */, + B56C2D4D2A4962D00062DAC0 /* Tests_TranscludeService.swift */, ); path = SubconsciousTests; sourceTree = ""; @@ -1618,6 +1621,7 @@ B80C9E432A2A7CE400E152FB /* Tests_HeaderSubtext.swift in Sources */, B8F27EE42970CD8F00A33E78 /* Tests_Sphere.swift in Sources */, B80C9E452A2A7CF100E152FB /* Tests_Audience.swift in Sources */, + B56C2D4E2A4962D00062DAC0 /* Tests_TranscludeService.swift in Sources */, B82BB7FE28243F32000C9FCC /* Tests_Parser.swift in Sources */, B8099F022A3B6FA50014FC2E /* Tests_MemoRecord.swift in Sources */, B84AD8E82811C827006B3153 /* Tests_URLComponentsUtilities.swift in Sources */, diff --git a/xcode/Subconscious/SubconsciousTests/TestUtilities.swift b/xcode/Subconscious/SubconsciousTests/TestUtilities.swift index 3d70bf2f..d65dce17 100644 --- a/xcode/Subconscious/SubconsciousTests/TestUtilities.swift +++ b/xcode/Subconscious/SubconsciousTests/TestUtilities.swift @@ -31,6 +31,7 @@ struct TestUtilities { var local: HeaderSubtextMemoStore var addressBook: AddressBookService var userProfile: UserProfileService + var transclude: TranscludeService } /// Set up and return a data service instance @@ -86,13 +87,19 @@ struct TestUtilities { database: database, addressBook: addressBook ) + + let transclude = TranscludeService( + database: database, + noosphere: noosphere + ) return DataServiceEnvironment( data: data, noosphere: noosphere, local: local, addressBook: addressBook, - userProfile: userProfile + userProfile: userProfile, + transclude: transclude ) } } diff --git a/xcode/Subconscious/SubconsciousTests/Tests_TranscludeService.swift b/xcode/Subconscious/SubconsciousTests/Tests_TranscludeService.swift index 602f5938..d932df03 100644 --- a/xcode/Subconscious/SubconsciousTests/Tests_TranscludeService.swift +++ b/xcode/Subconscious/SubconsciousTests/Tests_TranscludeService.swift @@ -1,8 +1,72 @@ // -// Tests_TranscludeSerivce.swift +// Tests_TranscludeService.swift // SubconsciousTests // // Created by Ben Follington on 26/6/2023. // -import Foundation +import XCTest +import Combine +import ObservableStore +@testable import Subconscious + +final class Tests_TranscludeService: XCTestCase { + /// A place to put cancellables from publishers + var cancellables: Set = Set() + + var data: DataService? + + func testFetchLocalTranscludes() async throws { + let tmp = try TestUtilities.createTmpDir() + let environment = try await TestUtilities.createDataServiceEnvironment( + tmp: tmp + ) + + let address = Slashlink("/test")! + let memo = Memo( + contentType: ContentType.subtext.rawValue, + created: Date.now, + modified: Date.now, + fileExtension: ContentType.subtext.fileExtension, + additionalHeaders: [], + body: "Test content" + ) + + try await environment.data.writeMemo( + address: address, + memo: memo + ) + + let address2 = Slashlink("/test-again")! + let memo2 = Memo( + contentType: ContentType.subtext.rawValue, + created: Date.now, + modified: Date.now, + fileExtension: ContentType.subtext.fileExtension, + additionalHeaders: [], + body: "With different content" + ) + + try await environment.data.writeMemo( + address: address2, + memo: memo2 + ) + + _ = try await environment.data.indexOurSphere() + + let profile = UserProfile.dummyData(category: .ourself) + + let transcludes = try await environment + .transclude + .fetchTranscludePreviews( + slashlinks: [address, address2], + owner: profile + ) + + XCTAssertEqual(transcludes.count, 2) + XCTAssertTrue(transcludes[address] != nil) + XCTAssertEqual(transcludes[address]!.excerpt, "Test content") + XCTAssertTrue(transcludes[address2] != nil) + XCTAssertEqual(transcludes[address2]!.excerpt, "With different content") + } +} From 158118406b3264a6e25aa070831e0f17a2a41ff7 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Mon, 26 Jun 2023 18:18:40 +1000 Subject: [PATCH 22/23] Revert accidental change --- .../Shared/Components/Notebook/NotebookNavigationView.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Notebook/NotebookNavigationView.swift b/xcode/Subconscious/Shared/Components/Notebook/NotebookNavigationView.swift index 5ddb385f..19c9c037 100644 --- a/xcode/Subconscious/Shared/Components/Notebook/NotebookNavigationView.swift +++ b/xcode/Subconscious/Shared/Components/Notebook/NotebookNavigationView.swift @@ -25,8 +25,9 @@ struct NotebookNavigationView: View { onEntryPress: { entry in store.send( .pushDetail( - MemoViewerDetailDescription( - address: entry.address + MemoEditorDetailDescription( + address: entry.address, + fallback: entry.excerpt ) ) ) From 8b8fd256a40e510fdea4834055fd153346a7ef4e Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Mon, 26 Jun 2023 18:18:48 +1000 Subject: [PATCH 23/23] Remove empty lines --- .../Shared/Components/Detail/MemoViewerDetailView.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift b/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift index 85422369..f0c5243e 100644 --- a/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift +++ b/xcode/Subconscious/Shared/Components/Detail/MemoViewerDetailView.swift @@ -284,8 +284,6 @@ struct MemoViewerDetailModel: ModelProtocol { var transcludePreviews: [Slashlink: EntryStub] = [:] - - static func update( state: Self, action: Action, @@ -463,7 +461,6 @@ struct MemoViewerDetailModel: ModelProtocol { .userProfile .requestOurProfile() .profile - } return try await environment