From 0043f0059deaa0d6ca062043a44a705edeef030a Mon Sep 17 00:00:00 2001 From: William Casarin Date: Sat, 16 Mar 2024 11:59:56 +0000 Subject: [PATCH 1/8] strings: add pluralized quoted_repost_count string We will be using this for translating "Quote{,s}" pluralization. For now we add the english version. Signed-off-by: William Casarin --- damus/en-US.lproj/Localizable.stringsdict | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/damus/en-US.lproj/Localizable.stringsdict b/damus/en-US.lproj/Localizable.stringsdict index 755c5ace..bef83fac 100644 --- a/damus/en-US.lproj/Localizable.stringsdict +++ b/damus/en-US.lproj/Localizable.stringsdict @@ -226,6 +226,22 @@ Reposts + quoted_reposts_count + + NSStringLocalizedFormatKey + %#@QUOTE_REPOSTS@ + QUOTE_REPOSTS + + NSStringFormatSpecTypeKey + NSStringPluralRuleType + NSStringFormatValueTypeKey + d + one + Quote + other + Quotes + + sats NSStringLocalizedFormatKey From 6111e244de4129a16d4472f95a3888eca09a5eab Mon Sep 17 00:00:00 2001 From: William Casarin Date: Sat, 16 Mar 2024 12:03:08 +0000 Subject: [PATCH 2/8] filter: add reposts query filter helper Add a filter helper to easily query quote repost queries. Signed-off-by: William Casarin --- damus/Nostr/NostrFilter.swift | 5 ++++- damus/Notify/RepostedNotify.swift | 26 -------------------------- 2 files changed, 4 insertions(+), 27 deletions(-) delete mode 100644 damus/Notify/RepostedNotify.swift diff --git a/damus/Nostr/NostrFilter.swift b/damus/Nostr/NostrFilter.swift index cd6472bf..1310defa 100644 --- a/damus/Nostr/NostrFilter.swift +++ b/damus/Nostr/NostrFilter.swift @@ -18,6 +18,7 @@ struct NostrFilter: Codable, Equatable { var authors: [Pubkey]? var hashtag: [String]? var parameter: [String]? + var quotes: [NoteId]? private enum CodingKeys : String, CodingKey { case ids @@ -26,13 +27,14 @@ struct NostrFilter: Codable, Equatable { case pubkeys = "#p" case hashtag = "#t" case parameter = "#d" + case quotes = "#q" case since case until case authors case limit } - init(ids: [NoteId]? = nil, kinds: [NostrKind]? = nil, referenced_ids: [NoteId]? = nil, pubkeys: [Pubkey]? = nil, since: UInt32? = nil, until: UInt32? = nil, limit: UInt32? = nil, authors: [Pubkey]? = nil, hashtag: [String]? = nil) { + init(ids: [NoteId]? = nil, kinds: [NostrKind]? = nil, referenced_ids: [NoteId]? = nil, pubkeys: [Pubkey]? = nil, since: UInt32? = nil, until: UInt32? = nil, limit: UInt32? = nil, authors: [Pubkey]? = nil, hashtag: [String]? = nil, quotes: [NoteId]? = nil) { self.ids = ids self.kinds = kinds self.referenced_ids = referenced_ids @@ -42,6 +44,7 @@ struct NostrFilter: Codable, Equatable { self.limit = limit self.authors = authors self.hashtag = hashtag + self.quotes = quotes } public static func copy(from: NostrFilter) -> NostrFilter { diff --git a/damus/Notify/RepostedNotify.swift b/damus/Notify/RepostedNotify.swift deleted file mode 100644 index b9eb8ad4..00000000 --- a/damus/Notify/RepostedNotify.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// BoostedNotify.swift -// damus -// -// Created by William Casarin on 2023-07-30. -// - -import Foundation - -struct RepostedNotify: Notify { - typealias Payload = Counted - var payload: Payload -} - -extension NotifyHandler { - static var reposted: NotifyHandler { - .init() - } -} - -extension Notifications { - static func reposted(_ counts: Counted) -> Notifications { - .init(.init(payload: counts)) - } -} - From 8cdbc8409363194f197415a5ed65a6e840bcd8ba Mon Sep 17 00:00:00 2001 From: William Casarin Date: Sat, 16 Mar 2024 12:05:49 +0000 Subject: [PATCH 3/8] home: add quote repost counter and handler This adds the initial support code for counting and handling quote reposts. Eventually we are going to replace all of the event counts by stats within nostrdb, but we do this in the meantime now. Signed-off-by: William Casarin --- damus/ContentView.swift | 3 ++- damus/Models/ActionBarModel.swift | 12 +++++++++++- damus/Models/DamusState.swift | 7 +++++-- damus/Models/HomeModel.swift | 15 +++++++++++++-- damus/TestData.swift | 4 +++- damusTests/Mocking/MockDamusState.swift | 3 ++- 6 files changed, 36 insertions(+), 8 deletions(-) diff --git a/damus/ContentView.swift b/damus/ContentView.swift index 930f163b..6fe9a1ee 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -723,7 +723,8 @@ struct ContentView: View { nav: self.navigationCoordinator, music: MusicController(onChange: music_changed), video: VideoController(), - ndb: ndb + ndb: ndb, + quote_reposts: .init(our_pubkey: pubkey) ) home.damus_state = self.damus_state! diff --git a/damus/Models/ActionBarModel.swift b/damus/Models/ActionBarModel.swift index d0109757..90f2c228 100644 --- a/damus/Models/ActionBarModel.swift +++ b/damus/Models/ActionBarModel.swift @@ -16,10 +16,12 @@ enum Zapped { class ActionBarModel: ObservableObject { @Published var our_like: NostrEvent? @Published var our_boost: NostrEvent? + @Published var our_quote_repost: NostrEvent? @Published var our_reply: NostrEvent? @Published var our_zap: Zapping? @Published var likes: Int @Published var boosts: Int + @Published var quote_reposts: Int @Published private(set) var zaps: Int @Published var zap_total: Int64 @Published var replies: Int @@ -28,7 +30,7 @@ class ActionBarModel: ObservableObject { return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil) } - init(likes: Int = 0, boosts: Int = 0, zaps: Int = 0, zap_total: Int64 = 0, replies: Int = 0, our_like: NostrEvent? = nil, our_boost: NostrEvent? = nil, our_zap: Zapping? = nil, our_reply: NostrEvent? = nil) { + init(likes: Int = 0, boosts: Int = 0, zaps: Int = 0, zap_total: Int64 = 0, replies: Int = 0, our_like: NostrEvent? = nil, our_boost: NostrEvent? = nil, our_zap: Zapping? = nil, our_reply: NostrEvent? = nil, our_quote_repost: NostrEvent? = nil, quote_reposts: Int = 0) { self.likes = likes self.boosts = boosts self.zaps = zaps @@ -38,6 +40,8 @@ class ActionBarModel: ObservableObject { self.our_boost = our_boost self.our_zap = our_zap self.our_reply = our_reply + self.our_quote_repost = our_quote_repost + self.quote_reposts = quote_reposts } func update(damus: DamusState, evid: NoteId) { @@ -45,11 +49,13 @@ class ActionBarModel: ObservableObject { self.boosts = damus.boosts.counts[evid] ?? 0 self.zaps = damus.zaps.event_counts[evid] ?? 0 self.replies = damus.replies.get_replies(evid) + self.quote_reposts = damus.quote_reposts.counts[evid] ?? 0 self.zap_total = damus.zaps.event_totals[evid] ?? 0 self.our_like = damus.likes.our_events[evid] self.our_boost = damus.boosts.our_events[evid] self.our_zap = damus.zaps.our_zaps[evid]?.first self.our_reply = damus.replies.our_reply(evid) + self.our_quote_repost = damus.quote_reposts.our_events[evid] self.objectWillChange.send() } @@ -68,4 +74,8 @@ class ActionBarModel: ObservableObject { var boosted: Bool { return our_boost != nil } + + var quoted: Bool { + return our_quote_repost != nil + } } diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift index d3cafdde..97a5d843 100644 --- a/damus/Models/DamusState.swift +++ b/damus/Models/DamusState.swift @@ -13,6 +13,7 @@ class DamusState: HeadlessDamusState { let keypair: Keypair let likes: EventCounter let boosts: EventCounter + let quote_reposts: EventCounter let contacts: Contacts let mutelist_manager: MutelistManager let profiles: Profiles @@ -36,7 +37,7 @@ class DamusState: HeadlessDamusState { let ndb: Ndb var purple: DamusPurple - init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [String], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil) { + init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [String], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter) { self.pool = pool self.keypair = keypair self.likes = likes @@ -66,6 +67,7 @@ class DamusState: HeadlessDamusState { settings: settings, keypair: keypair ) + self.quote_reposts = quote_reposts } @discardableResult @@ -129,7 +131,8 @@ class DamusState: HeadlessDamusState { nav: NavigationCoordinator(), music: nil, video: VideoController(), - ndb: .empty + ndb: .empty, + quote_reposts: .init(our_pubkey: empty_pub) ) } } diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift index 4f76c899..ac6909b2 100644 --- a/damus/Models/HomeModel.swift +++ b/damus/Models/HomeModel.swift @@ -347,12 +347,19 @@ class HomeModel { case .already_counted: break case .success(let n): - let boosted = Counted(event: ev, id: e, total: n) - notify(.reposted(boosted)) notify(.update_stats(note_id: e)) } } + func handle_quote_repost_event(_ ev: NostrEvent, target: NoteId) { + switch damus_state.quote_reposts.add_event(ev, target: target) { + case .already_counted: + break + case .success(let n): + notify(.update_stats(note_id: target)) + } + } + func handle_like_event(_ ev: NostrEvent) { guard let e = ev.last_refid() else { // no id ref? invalid like event @@ -672,6 +679,10 @@ class HomeModel { damus_state.replies.count_replies(ev, keypair: self.damus_state.keypair) damus_state.events.insert(ev) + if let quoted_event = ev.referenced_quote_ids.first { + handle_quote_repost_event(ev, target: quoted_event.note_id) + } + if sub_id == home_subid { insert_home_event(ev) } else if sub_id == notifications_subid { diff --git a/damus/TestData.swift b/damus/TestData.swift index 127468a7..1de6ac02 100644 --- a/damus/TestData.swift +++ b/damus/TestData.swift @@ -92,7 +92,9 @@ var test_damus_state: DamusState = ({ nav: .init(), music: .init(onChange: {_ in }), video: .init(), - ndb: ndb) + ndb: ndb, + quote_reposts: .init(our_pubkey: our_pubkey) + ) /* let prof = Profile(name: "damus", display_name: "damus", about: "iOS app!", picture: "https://damus.io/img/logo.png", banner: "", website: "https://damus.io", lud06: nil, lud16: "jb55@sendsats.lol", nip05: "damus.io", damus_donation: nil) diff --git a/damusTests/Mocking/MockDamusState.swift b/damusTests/Mocking/MockDamusState.swift index 4c4faf53..fb02cd62 100644 --- a/damusTests/Mocking/MockDamusState.swift +++ b/damusTests/Mocking/MockDamusState.swift @@ -49,7 +49,8 @@ func generate_test_damus_state( nav: .init(), music: .init(onChange: {_ in }), video: .init(), - ndb: ndb) + ndb: ndb, + quote_reposts: .init(our_pubkey: our_pubkey) ) return damus } From 68dd47130ef865eb5365bbf1012839741c135b45 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Sat, 16 Mar 2024 12:11:32 +0000 Subject: [PATCH 4/8] eventsmodel: remove inheritence in Reactions/Reposts model Simplify with new EventsModel constructors. This is slightly less typesafe but its not a big deal, I hate inheritence more. Signed-off-by: William Casarin --- damus.xcodeproj/project.pbxproj | 12 ------------ damus/Models/EventsModel.swift | 8 ++++++++ damus/Models/ReactionsModel.swift | 16 ---------------- damus/Models/RepostsModel.swift | 15 --------------- damus/Util/Router.swift | 4 ++-- damus/Views/ActionBar/EventDetailBar.swift | 4 ++-- damus/Views/ReactionsView.swift | 6 +++--- damus/Views/RepostsView.swift | 4 ++-- 8 files changed, 17 insertions(+), 52 deletions(-) delete mode 100644 damus/Models/ReactionsModel.swift delete mode 100644 damus/Models/RepostsModel.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index de4189af..af93ba92 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -25,7 +25,6 @@ 3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; }; 3A90B1812A4EA3AF00000D94 /* UserSearchCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A90B1802A4EA3AF00000D94 /* UserSearchCache.swift */; }; 3A90B1832A4EA3C600000D94 /* UserSearchCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A90B1822A4EA3C600000D94 /* UserSearchCacheTests.swift */; }; - 3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; }; 3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; }; 3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; }; 3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA59D1C2999B0400061C48E /* DraftsModel.swift */; }; @@ -61,7 +60,6 @@ 4C1253662A76D0FF0004F4B8 /* OnlyZapsNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1253652A76D0FF0004F4B8 /* OnlyZapsNotify.swift */; }; 4C1253682A76D2470004F4B8 /* MuteNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1253672A76D2470004F4B8 /* MuteNotify.swift */; }; 4C12536A2A76D3850004F4B8 /* RelaysChangedNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1253692A76D3850004F4B8 /* RelaysChangedNotify.swift */; }; - 4C12536C2A76D4B00004F4B8 /* RepostedNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C12536B2A76D4B00004F4B8 /* RepostedNotify.swift */; }; 4C15C7152A55DE7A00D0A0DB /* ReactionsSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C15C7142A55DE7A00D0A0DB /* ReactionsSettingsView.swift */; }; 4C190F202A535FC200027FD5 /* CustomizeZapModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C190F1F2A535FC200027FD5 /* CustomizeZapModel.swift */; }; 4C190F252A547D2000027FD5 /* LoadScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C190F242A547D2000027FD5 /* LoadScript.swift */; }; @@ -294,7 +292,6 @@ 4CB8838B296F6E1E00DC99E7 /* NIP05Badge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8838A296F6E1E00DC99E7 /* NIP05Badge.swift */; }; 4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8838C296F710400DC99E7 /* Reposted.swift */; }; 4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8838E296F781C00DC99E7 /* ReactionsView.swift */; }; - 4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88392296F798300DC99E7 /* ReactionsModel.swift */; }; 4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88395296F7F8B00DC99E7 /* ReactionView.swift */; }; 4CB8839A297322D200DC99E7 /* DMTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB88399297322D200DC99E7 /* DMTests.swift */; }; 4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */; }; @@ -770,7 +767,6 @@ 3A96D41A298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = ""; }; 3A96D41B298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; 3A96D41C298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Localizable.stringsdict; sourceTree = ""; }; - 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepostsModel.swift; sourceTree = ""; }; 3AA247FE297E3D900090C62D /* RepostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostsView.swift; sourceTree = ""; }; 3AA24801297E3DC20090C62D /* RepostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostView.swift; sourceTree = ""; }; 3AA59D1C2999B0400061C48E /* DraftsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsModel.swift; sourceTree = ""; }; @@ -844,7 +840,6 @@ 4C1253652A76D0FF0004F4B8 /* OnlyZapsNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnlyZapsNotify.swift; sourceTree = ""; }; 4C1253672A76D2470004F4B8 /* MuteNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteNotify.swift; sourceTree = ""; }; 4C1253692A76D3850004F4B8 /* RelaysChangedNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaysChangedNotify.swift; sourceTree = ""; }; - 4C12536B2A76D4B00004F4B8 /* RepostedNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostedNotify.swift; sourceTree = ""; }; 4C15C7142A55DE7A00D0A0DB /* ReactionsSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionsSettingsView.swift; sourceTree = ""; }; 4C190F1F2A535FC200027FD5 /* CustomizeZapModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeZapModel.swift; sourceTree = ""; }; 4C190F242A547D2000027FD5 /* LoadScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadScript.swift; sourceTree = ""; }; @@ -1212,7 +1207,6 @@ 4CB8838A296F6E1E00DC99E7 /* NIP05Badge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05Badge.swift; sourceTree = ""; }; 4CB8838C296F710400DC99E7 /* Reposted.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reposted.swift; sourceTree = ""; }; 4CB8838E296F781C00DC99E7 /* ReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsView.swift; sourceTree = ""; }; - 4CB88392296F798300DC99E7 /* ReactionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsModel.swift; sourceTree = ""; }; 4CB88395296F7F8B00DC99E7 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = ""; }; 4CB88399297322D200DC99E7 /* DMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMTests.swift; sourceTree = ""; }; 4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrlPayRequest.swift; sourceTree = ""; }; @@ -1590,7 +1584,6 @@ BA3759882ABCCDE30018D73B /* Camera */, 4C190F1E2A535FC200027FD5 /* Zaps */, 4C54AA0829A55416003E4487 /* Notifications */, - 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */, 4C0A3F8E280F640A000448DE /* ThreadModel.swift */, 4C0A3F92280F66F5000448DE /* ReplyMap.swift */, 4C3BEFD12819DB9B00B3DE84 /* ProfileModel.swift */, @@ -1620,7 +1613,6 @@ 4C216F372871EDE300040376 /* DirectMessageModel.swift */, BA693073295D649800ADDB87 /* UserSettingsStore.swift */, 4FE60CDC295E1C5E00105A1F /* Wallet.swift */, - 4CB88392296F798300DC99E7 /* ReactionsModel.swift */, 4CF0ABD32980996B00D66079 /* Report.swift */, 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */, 3AAA95C9298DF87B00F3D526 /* TranslationService.swift */, @@ -2262,7 +2254,6 @@ 4C86F7C32A76C44C00EC0817 /* ZappingNotify.swift */, 4C1253672A76D2470004F4B8 /* MuteNotify.swift */, 4C1253692A76D3850004F4B8 /* RelaysChangedNotify.swift */, - 4C12536B2A76D4B00004F4B8 /* RepostedNotify.swift */, 4C4E137A2A76D5FB00BDD832 /* MuteThreadNotify.swift */, 4C4E137C2A76D63600BDD832 /* UnmuteThreadNotify.swift */, B57B4C612B312BD700A232C0 /* ReconnectRelaysNotify.swift */, @@ -3113,7 +3104,6 @@ 4C8D00C829DF791C0036AF10 /* CompatibleAttribute.swift in Sources */, 4C7D09742A0AEF9000943473 /* AlbyGradient.swift in Sources */, 4C687C272A6039500092C550 /* TestData.swift in Sources */, - 3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */, 50C3E08A2AA8E3F7006A4BC0 /* AVPlayer+Additions.swift in Sources */, 4C198DF229F88C6B004C165C /* BlurHashDecode.swift in Sources */, F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */, @@ -3329,7 +3319,6 @@ 4C32B95E2A9AD44700DC3548 /* FlatBufferObject.swift in Sources */, D783A63F2AD4E53D00658DDA /* SuggestedHashtagsView.swift in Sources */, 4C3EA64F28FF59F200C48A62 /* tal.c in Sources */, - 4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */, 5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */, 4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */, 50A16FFD2AA7525700DFEC1F /* DamusVideoPlayerViewModel.swift in Sources */, @@ -3421,7 +3410,6 @@ 4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */, 7527271E2A93FF0100214108 /* Block.swift in Sources */, 4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */, - 4C12536C2A76D4B00004F4B8 /* RepostedNotify.swift in Sources */, 4CE4F0F229D4FCFA005914DB /* DebouncedOnChange.swift in Sources */, 4C32B9592A9AD44700DC3548 /* Table.swift in Sources */, 4C5D5C9D2A6B2CB40024563C /* AsciiCharacter.swift in Sources */, diff --git a/damus/Models/EventsModel.swift b/damus/Models/EventsModel.swift index 7ce906db..ae51fcac 100644 --- a/damus/Models/EventsModel.swift +++ b/damus/Models/EventsModel.swift @@ -23,6 +23,14 @@ class EventsModel: ObservableObject { self.kind = kind } + public static func reposts(state: DamusState, target: NoteId) -> EventsModel { + EventsModel(state: state, target: target, kind: .boost) + } + + public static func likes(state: DamusState, target: NoteId) -> EventsModel { + EventsModel(state: state, target: target, kind: .like) + } + private func get_filter() -> NostrFilter { var filter = NostrFilter(kinds: [kind]) filter.referenced_ids = [target] diff --git a/damus/Models/ReactionsModel.swift b/damus/Models/ReactionsModel.swift deleted file mode 100644 index 3f3bf723..00000000 --- a/damus/Models/ReactionsModel.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// LikesModel.swift -// damus -// -// Created by William Casarin on 2023-01-11. -// - -import Foundation - - -final class ReactionsModel: EventsModel { - - init(state: DamusState, target: NoteId) { - super.init(state: state, target: target, kind: .like) - } -} diff --git a/damus/Models/RepostsModel.swift b/damus/Models/RepostsModel.swift deleted file mode 100644 index 2369b900..00000000 --- a/damus/Models/RepostsModel.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// RepostsModel.swift -// damus -// -// Created by Terry Yiu on 1/22/23. -// - -import Foundation - -final class RepostsModel: EventsModel { - - init(state: DamusState, target: NoteId) { - super.init(state: state, target: target, kind: .boost) - } -} diff --git a/damus/Util/Router.swift b/damus/Util/Router.swift index 90f0b622..b2899e93 100644 --- a/damus/Util/Router.swift +++ b/damus/Util/Router.swift @@ -31,8 +31,8 @@ enum Route: Hashable { case SearchSettings(settings: UserSettingsStore) case DeveloperSettings(settings: UserSettingsStore) case Thread(thread: ThreadModel) - case Reposts(reposts: RepostsModel) - case Reactions(reactions: ReactionsModel) + case Reposts(reposts: EventsModel) + case Reactions(reactions: EventsModel) case Zaps(target: ZapTarget) case Search(search: SearchModel) case EULA diff --git a/damus/Views/ActionBar/EventDetailBar.swift b/damus/Views/ActionBar/EventDetailBar.swift index aca05e65..af688c1c 100644 --- a/damus/Views/ActionBar/EventDetailBar.swift +++ b/damus/Views/ActionBar/EventDetailBar.swift @@ -25,7 +25,7 @@ struct EventDetailBar: View { var body: some View { HStack { if bar.boosts > 0 { - NavigationLink(value: Route.Reposts(reposts: RepostsModel(state: state, target: target))) { + NavigationLink(value: Route.Reposts(reposts: .reposts(state: state, target: target))) { let nounString = pluralizedString(key: "reposts_count", count: bar.boosts) let noun = Text(nounString).foregroundColor(.gray) Text("\(Text(verbatim: bar.boosts.formatted()).font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.") @@ -34,7 +34,7 @@ struct EventDetailBar: View { } if bar.likes > 0 && !state.settings.onlyzaps_mode { - NavigationLink(value: Route.Reactions(reactions: ReactionsModel(state: state, target: target))) { + NavigationLink(value: Route.Reactions(reactions: .likes(state: state, target: target))) { let nounString = pluralizedString(key: "reactions_count", count: bar.likes) let noun = Text(nounString).foregroundColor(.gray) Text("\(Text(verbatim: bar.likes.formatted()).font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.") diff --git a/damus/Views/ReactionsView.swift b/damus/Views/ReactionsView.swift index 5d1e02f4..5be84455 100644 --- a/damus/Views/ReactionsView.swift +++ b/damus/Views/ReactionsView.swift @@ -9,8 +9,8 @@ import SwiftUI struct ReactionsView: View { let damus_state: DamusState - @StateObject var model: ReactionsModel - + @StateObject var model: EventsModel + @Environment(\.dismiss) var dismiss var body: some View { @@ -38,6 +38,6 @@ struct ReactionsView: View { struct ReactionsView_Previews: PreviewProvider { static var previews: some View { let state = test_damus_state - ReactionsView(damus_state: state, model: ReactionsModel(state: state, target: test_note.id)) + ReactionsView(damus_state: state, model: .likes(state: state, target: test_note.id)) } } diff --git a/damus/Views/RepostsView.swift b/damus/Views/RepostsView.swift index 210cd703..2aadecb2 100644 --- a/damus/Views/RepostsView.swift +++ b/damus/Views/RepostsView.swift @@ -9,7 +9,7 @@ import SwiftUI struct RepostsView: View { let damus_state: DamusState - @StateObject var model: RepostsModel + @StateObject var model: EventsModel var body: some View { ScrollView { @@ -33,6 +33,6 @@ struct RepostsView: View { struct RepostsView_Previews: PreviewProvider { static var previews: some View { let state = test_damus_state - RepostsView(damus_state: state, model: RepostsModel(state: state, target: test_note.id)) + RepostsView(damus_state: state, model: .reposts(state: state, target: test_note.id)) } } From 770a845b366346ae662e2b6d268b5f0072f369e3 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Sat, 16 Mar 2024 12:14:19 +0000 Subject: [PATCH 5/8] filters: add ContentFilters helper constructor This is slightly faster for timeline code that needs default filters Signed-off-by: William Casarin --- damus/Models/ContentFilters.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/damus/Models/ContentFilters.swift b/damus/Models/ContentFilters.swift index 63a23a36..d0d13107 100644 --- a/damus/Models/ContentFilters.swift +++ b/damus/Models/ContentFilters.swift @@ -53,6 +53,10 @@ struct ContentFilters { } extension ContentFilters { + static func default_filters(damus_state: DamusState) -> ContentFilters { + return ContentFilters(filters: ContentFilters.defaults(damus_state: damus_state)) + } + static func defaults(damus_state: DamusState) -> [(NostrEvent) -> Bool] { var filters = Array<(NostrEvent) -> Bool>() if damus_state.settings.hide_nsfw_tagged_content { From 1339ec3ded7e3f85a3471737cd15879255c7bf9e Mon Sep 17 00:00:00 2001 From: William Casarin Date: Sat, 16 Mar 2024 12:15:48 +0000 Subject: [PATCH 6/8] note: add is_quote_repost helper This will be used for excluding quote repost notes from threads Signed-off-by: William Casarin --- nostrdb/NdbNote.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nostrdb/NdbNote.swift b/nostrdb/NdbNote.swift index ba543a71..4ce5e726 100644 --- a/nostrdb/NdbNote.swift +++ b/nostrdb/NdbNote.swift @@ -280,6 +280,13 @@ extension NdbNote { return kind == 1 || kind == 42 || kind == 30023 } + var is_quote_repost: NoteId? { + guard kind == 1, let quoted_note_id = referenced_quote_ids.first else { + return nil + } + return quoted_note_id.note_id + } + var known_kind: NostrKind? { return NostrKind.init(rawValue: kind) } From 3f1f257df2dbdfada879c1d6e7b4ef7f57da8ca6 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Sat, 16 Mar 2024 12:19:29 +0000 Subject: [PATCH 7/8] model: upgrade EventsModel to support quote reposts queries We also switch to using an eventholder because that is a bit nicer when it comes to rendering quotes in timelines. Signed-off-by: William Casarin --- damus/Models/EventsModel.swift | 59 ++++++++++++++++++++++++--------- damus/Views/ReactionsView.swift | 2 +- damus/Views/RepostsView.swift | 2 +- 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/damus/Models/EventsModel.swift b/damus/Models/EventsModel.swift index ae51fcac..74ee5b12 100644 --- a/damus/Models/EventsModel.swift +++ b/damus/Models/EventsModel.swift @@ -11,16 +11,39 @@ import Foundation class EventsModel: ObservableObject { let state: DamusState let target: NoteId - let kind: NostrKind + let kind: QueryKind let sub_id = UUID().uuidString let profiles_id = UUID().uuidString - - @Published var events: [NostrEvent] = [] - + var events: EventHolder + @Published var loading: Bool + + enum QueryKind { + case kind(NostrKind) + case quotes + } + init(state: DamusState, target: NoteId, kind: NostrKind) { self.state = state self.target = target - self.kind = kind + self.kind = .kind(kind) + self.loading = true + self.events = EventHolder(on_queue: { ev in + preload_events(state: state, events: [ev]) + }) + } + + init(state: DamusState, target: NoteId, query: EventsModel.QueryKind) { + self.state = state + self.target = target + self.kind = query + self.loading = true + self.events = EventHolder(on_queue: { ev in + preload_events(state: state, events: [ev]) + }) + } + + public static func quotes(state: DamusState, target: NoteId) -> EventsModel { + EventsModel(state: state, target: target, query: .quotes) } public static func reposts(state: DamusState, target: NoteId) -> EventsModel { @@ -32,8 +55,15 @@ class EventsModel: ObservableObject { } private func get_filter() -> NostrFilter { - var filter = NostrFilter(kinds: [kind]) - filter.referenced_ids = [target] + var filter: NostrFilter + switch kind { + case .kind(let k): + filter = NostrFilter(kinds: [k]) + filter.referenced_ids = [target] + case .quotes: + filter = NostrFilter(kinds: [.text]) + filter.quotes = [target] + } filter.limit = 500 return filter } @@ -49,21 +79,17 @@ class EventsModel: ObservableObject { } private func handle_event(relay_id: String, ev: NostrEvent) { - guard ev.kind == kind.rawValue, - ev.referenced_ids.last == target else { - return - } - - if insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { a, b in a.created_at < b.created_at } ) { + if events.insert(ev) { objectWillChange.send() } } func handle_nostr_event(relay_id: String, ev: NostrConnectionEvent) { - guard case .nostr_event(let nev) = ev else { + guard case .nostr_event(let nev) = ev, nev.subid == self.sub_id + else { return } - + switch nev { case .event(_, let ev): handle_event(relay_id: relay_id, ev: ev) @@ -74,10 +100,11 @@ class EventsModel: ObservableObject { case .auth: break case .eose: + self.loading = false guard let txn = NdbTxn(ndb: self.state.ndb) else { return } - load_profiles(context: "events_model", profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state, txn: txn) + load_profiles(context: "events_model", profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events.all_events), damus_state: state, txn: txn) } } } diff --git a/damus/Views/ReactionsView.swift b/damus/Views/ReactionsView.swift index 5be84455..2c6e6503 100644 --- a/damus/Views/ReactionsView.swift +++ b/damus/Views/ReactionsView.swift @@ -16,7 +16,7 @@ struct ReactionsView: View { var body: some View { ScrollView { LazyVStack { - ForEach(model.events, id: \.id) { ev in + ForEach(model.events.events, id: \.id) { ev in ReactionView(damus_state: damus_state, reaction: ev) } } diff --git a/damus/Views/RepostsView.swift b/damus/Views/RepostsView.swift index 2aadecb2..368f11ce 100644 --- a/damus/Views/RepostsView.swift +++ b/damus/Views/RepostsView.swift @@ -14,7 +14,7 @@ struct RepostsView: View { var body: some View { ScrollView { LazyVStack { - ForEach(model.events, id: \.id) { ev in + ForEach(model.events.events, id: \.id) { ev in RepostView(damus_state: damus_state, repost: ev) } } From c521998158d53855e8372ae4ae72ef950d48e2a1 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Sat, 16 Mar 2024 12:20:59 +0000 Subject: [PATCH 8/8] ui: add quoted reposts view to threads This adds quote reposts as an additional detail view on threads. It will list quoted reposts that have the `q` tag. Not all clients have updated to this yet (like primal), but hopefully they will soon. Changelog-Added: Show list of quoted reposts in threads Signed-off-by: William Casarin --- damus.xcodeproj/project.pbxproj | 20 ++++++++------ damus/Models/EventsModel.swift | 1 - damus/Models/ThreadModel.swift | 20 ++++++++++---- damus/Nostr/Id.swift | 2 +- damus/Util/Router.swift | 6 +++++ damus/Views/ActionBar/EventDetailBar.swift | 9 +++++++ damus/Views/Reposts/QuoteRepostsView.swift | 31 ++++++++++++++++++++++ 7 files changed, 74 insertions(+), 15 deletions(-) create mode 100644 damus/Views/Reposts/QuoteRepostsView.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index af93ba92..daa93927 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -253,6 +253,7 @@ 4C9146FD2A2A87C200DDEA40 /* wasm.c in Sources */ = {isa = PBXBuildFile; fileRef = 4CA9276E2A2A5D110098A105 /* wasm.c */; }; 4C9146FE2A2A87C200DDEA40 /* nostrscript.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C4F14A92A2A71AB0045A0B9 /* nostrscript.c */; }; 4C9147002A2A891E00DDEA40 /* error.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C9146FF2A2A891E00DDEA40 /* error.c */; }; + 4C94D6432BA5AEFE00C26EFF /* QuoteRepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C94D6422BA5AEFE00C26EFF /* QuoteRepostsView.swift */; }; 4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C987B56283FD07F0042CE38 /* FollowersModel.swift */; }; 4C9AA14A2A4587A6003F49FD /* NotificationStatusModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9AA1492A4587A6003F49FD /* NotificationStatusModel.swift */; }; 4C9B0DEE2A65A75F00CBDA21 /* AttrStringTestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9B0DED2A65A75F00CBDA21 /* AttrStringTestExtensions.swift */; }; @@ -1161,6 +1162,7 @@ 4C90BD19283AA67F008EE7EF /* Bech32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32.swift; sourceTree = ""; }; 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Tests.swift; sourceTree = ""; }; 4C9146FF2A2A891E00DDEA40 /* error.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = error.c; sourceTree = ""; }; + 4C94D6422BA5AEFE00C26EFF /* QuoteRepostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteRepostsView.swift; sourceTree = ""; }; 4C987B56283FD07F0042CE38 /* FollowersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersModel.swift; sourceTree = ""; }; 4C9AA1492A4587A6003F49FD /* NotificationStatusModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationStatusModel.swift; sourceTree = ""; }; 4C9B0DED2A65A75F00CBDA21 /* AttrStringTestExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttrStringTestExtensions.swift; sourceTree = ""; }; @@ -1506,6 +1508,7 @@ children = ( 3AA24801297E3DC20090C62D /* RepostView.swift */, 4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */, + 4C94D6422BA5AEFE00C26EFF /* QuoteRepostsView.swift */, ); path = Reposts; sourceTree = ""; @@ -2735,14 +2738,6 @@ path = DamusNotificationService; sourceTree = ""; }; - E06336A72B7582D600A88E6B /* Assets */ = { - isa = PBXGroup; - children = ( - E06336A82B7582E000A88E6B /* img_with_location.jpeg */, - ); - path = Assets; - sourceTree = ""; - }; D7CBD1D22B8D21C100BFD889 /* Extensions */ = { isa = PBXGroup; children = ( @@ -2751,6 +2746,14 @@ path = Extensions; sourceTree = ""; }; + E06336A72B7582D600A88E6B /* Assets */ = { + isa = PBXGroup; + children = ( + E06336A82B7582E000A88E6B /* img_with_location.jpeg */, + ); + path = Assets; + sourceTree = ""; + }; F71694E82A66221E001F4053 /* Onboarding */ = { isa = PBXGroup; children = ( @@ -3248,6 +3251,7 @@ 4C3AC7A12835A81400E1F516 /* SetupView.swift in Sources */, 4C06670128FC7C5900038D2A /* RelayView.swift in Sources */, 4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */, + 4C94D6432BA5AEFE00C26EFF /* QuoteRepostsView.swift in Sources */, D7EDED332B12ACAE0018B19C /* DamusUserDefaults.swift in Sources */, 4CA352AE2A76C1AC003BB08B /* FollowedNotify.swift in Sources */, 4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */, diff --git a/damus/Models/EventsModel.swift b/damus/Models/EventsModel.swift index 74ee5b12..5e263c3f 100644 --- a/damus/Models/EventsModel.swift +++ b/damus/Models/EventsModel.swift @@ -7,7 +7,6 @@ import Foundation - class EventsModel: ObservableObject { let state: DamusState let target: NoteId diff --git a/damus/Models/ThreadModel.swift b/damus/Models/ThreadModel.swift index dbc86929..a02a299e 100644 --- a/damus/Models/ThreadModel.swift +++ b/damus/Models/ThreadModel.swift @@ -56,6 +56,7 @@ class ThreadModel: ObservableObject { func subscribe() { var meta_events = NostrFilter() + var quote_events = NostrFilter() var event_filter = NostrFilter() var ref_events = NostrFilter() @@ -74,11 +75,14 @@ class ThreadModel: ObservableObject { kinds.append(.like) } meta_events.kinds = kinds - meta_events.limit = 1000 - + + quote_events.kinds = [.text] + quote_events.quotes = [event.id] + quote_events.limit = 1000 + let base_filters = [event_filter, ref_events] - let meta_filters = [meta_events] + let meta_filters = [meta_events, quote_events] print("subscribing to thread \(event.id) with sub_id \(base_subid)") damus_state.pool.subscribe(sub_id: base_subid, filters: base_filters, handler: handle_event) @@ -90,7 +94,7 @@ class ThreadModel: ObservableObject { return } - let the_ev = damus_state.events.upsert(ev) + damus_state.events.upsert(ev) damus_state.replies.count_replies(ev, keypair: keypair) damus_state.events.add_replies(ev: ev, keypair: keypair) @@ -111,7 +115,13 @@ class ThreadModel: ObservableObject { } } else if ev.is_textlike { - self.add_event(ev, keypair: damus_state.keypair) + // handle thread quote reposts, we just count them instead of + // adding them to the thread + if let target = ev.is_quote_repost, target == self.event.id { + //let _ = self.damus_state.quote_reposts.add_event(ev, target: target) + } else { + self.add_event(ev, keypair: damus_state.keypair) + } } } diff --git a/damus/Nostr/Id.swift b/damus/Nostr/Id.swift index 101faddd..5d8864bd 100644 --- a/damus/Nostr/Id.swift +++ b/damus/Nostr/Id.swift @@ -41,7 +41,7 @@ struct QuoteId: IdType, TagKey, TagConvertible { self.id = data } - /// Refer to this QuoteId as a NoteId + /// The note id being quoted var note_id: NoteId { NoteId(self.id) } diff --git a/damus/Util/Router.swift b/damus/Util/Router.swift index b2899e93..3bf55eff 100644 --- a/damus/Util/Router.swift +++ b/damus/Util/Router.swift @@ -32,6 +32,7 @@ enum Route: Hashable { case DeveloperSettings(settings: UserSettingsStore) case Thread(thread: ThreadModel) case Reposts(reposts: EventsModel) + case QuoteReposts(quotes: EventsModel) case Reactions(reactions: EventsModel) case Zaps(target: ZapTarget) case Search(search: SearchModel) @@ -92,6 +93,8 @@ enum Route: Hashable { ThreadView(state: damusState, thread: thread) case .Reposts(let reposts): RepostsView(damus_state: damusState, model: reposts) + case .QuoteReposts(let quote_reposts): + QuoteRepostsView(damus_state: damusState, model: quote_reposts) case .Reactions(let reactions): ReactionsView(damus_state: damusState, model: reactions) case .Zaps(let target): @@ -178,6 +181,9 @@ enum Route: Hashable { case .Reposts(let reposts): hasher.combine("reposts") hasher.combine(reposts.target) + case .QuoteReposts(let evs_model): + hasher.combine("quote_reposts") + hasher.combine(evs_model.events.events.count) case .Zaps(let target): hasher.combine("zaps") hasher.combine(target.id) diff --git a/damus/Views/ActionBar/EventDetailBar.swift b/damus/Views/ActionBar/EventDetailBar.swift index af688c1c..8035b880 100644 --- a/damus/Views/ActionBar/EventDetailBar.swift +++ b/damus/Views/ActionBar/EventDetailBar.swift @@ -33,6 +33,15 @@ struct EventDetailBar: View { .buttonStyle(PlainButtonStyle()) } + if bar.quote_reposts > 0 { + NavigationLink(value: Route.QuoteReposts(quotes: .quotes(state: state, target: target))) { + let nounString = pluralizedString(key: "quoted_reposts_count", count: bar.quote_reposts) + let noun = Text(nounString).foregroundColor(.gray) + Text("\(Text(verbatim: bar.quote_reposts.formatted()).font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many quoted reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.") + } + .buttonStyle(PlainButtonStyle()) + } + if bar.likes > 0 && !state.settings.onlyzaps_mode { NavigationLink(value: Route.Reactions(reactions: .likes(state: state, target: target))) { let nounString = pluralizedString(key: "reactions_count", count: bar.likes) diff --git a/damus/Views/Reposts/QuoteRepostsView.swift b/damus/Views/Reposts/QuoteRepostsView.swift new file mode 100644 index 00000000..d9e53173 --- /dev/null +++ b/damus/Views/Reposts/QuoteRepostsView.swift @@ -0,0 +1,31 @@ +// +// QuoteRepostsView.swift +// damus +// +// Created by William Casarin on 2024-03-16. +// + +import SwiftUI + +struct QuoteRepostsView: View { + let damus_state: DamusState + @ObservedObject var model: EventsModel + + var body: some View { + TimelineView(events: model.events, loading: $model.loading, damus: damus_state, show_friend_icon: true, filter: ContentFilters.default_filters(damus_state: damus_state).filter(ev:)) + .navigationBarTitle(NSLocalizedString("Quotes", comment: "Navigation bar title for Quote Reposts view.")) + .onAppear { + model.subscribe() + } + .onDisappear { + model.unsubscribe() + } + } +} + +struct QuoteRepostsView_Previews: PreviewProvider { + static var previews: some View { + let state = test_damus_state + QuoteRepostsView(damus_state: state, model: .reposts(state: state, target: test_note.id)) + } +}