damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit b31b917b70dfd045996f5b6b30ae0b38d49db981
parent 4413ec0ec5e02d6bc8e939905d8678b880850c76
Author: William Casarin <jb55@jb55.com>
Date:   Sun, 17 Mar 2024 09:33:29 +0000

Merge remote-tracking branch 'github/quote-reposts'

This adds quote repost listing support to Damus!

Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 28++++++++++------------------
Mdamus/ContentView.swift | 3++-
Mdamus/Models/ActionBarModel.swift | 12+++++++++++-
Mdamus/Models/ContentFilters.swift | 4++++
Mdamus/Models/DamusState.swift | 7+++++--
Mdamus/Models/EventsModel.swift | 68+++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mdamus/Models/HomeModel.swift | 15+++++++++++++--
Ddamus/Models/ReactionsModel.swift | 16----------------
Ddamus/Models/RepostsModel.swift | 15---------------
Mdamus/Models/ThreadModel.swift | 20+++++++++++++++-----
Mdamus/Nostr/Id.swift | 2+-
Mdamus/Nostr/NostrFilter.swift | 5++++-
Ddamus/Notify/RepostedNotify.swift | 26--------------------------
Mdamus/TestData.swift | 4+++-
Mdamus/Util/Router.swift | 10++++++++--
Mdamus/Views/ActionBar/EventDetailBar.swift | 13+++++++++++--
Mdamus/Views/ReactionsView.swift | 8++++----
Adamus/Views/Reposts/QuoteRepostsView.swift | 31+++++++++++++++++++++++++++++++
Mdamus/Views/RepostsView.swift | 6+++---
Mdamus/en-US.lproj/Localizable.stringsdict | 16++++++++++++++++
MdamusTests/Mocking/MockDamusState.swift | 3++-
Mnostrdb/NdbNote.swift | 7+++++++
22 files changed, 201 insertions(+), 118 deletions(-)

diff --git 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 */; }; @@ -255,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 */; }; @@ -294,7 +293,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 +768,6 @@ 3A96D41A298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 3A96D41B298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; }; 3A96D41C298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; - 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepostsModel.swift; sourceTree = "<group>"; }; 3AA247FE297E3D900090C62D /* RepostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostsView.swift; sourceTree = "<group>"; }; 3AA24801297E3DC20090C62D /* RepostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostView.swift; sourceTree = "<group>"; }; 3AA59D1C2999B0400061C48E /* DraftsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsModel.swift; sourceTree = "<group>"; }; @@ -844,7 +841,6 @@ 4C1253652A76D0FF0004F4B8 /* OnlyZapsNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnlyZapsNotify.swift; sourceTree = "<group>"; }; 4C1253672A76D2470004F4B8 /* MuteNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteNotify.swift; sourceTree = "<group>"; }; 4C1253692A76D3850004F4B8 /* RelaysChangedNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaysChangedNotify.swift; sourceTree = "<group>"; }; - 4C12536B2A76D4B00004F4B8 /* RepostedNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostedNotify.swift; sourceTree = "<group>"; }; 4C15C7142A55DE7A00D0A0DB /* ReactionsSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReactionsSettingsView.swift; sourceTree = "<group>"; }; 4C190F1F2A535FC200027FD5 /* CustomizeZapModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeZapModel.swift; sourceTree = "<group>"; }; 4C190F242A547D2000027FD5 /* LoadScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadScript.swift; sourceTree = "<group>"; }; @@ -1166,6 +1162,7 @@ 4C90BD19283AA67F008EE7EF /* Bech32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32.swift; sourceTree = "<group>"; }; 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Tests.swift; sourceTree = "<group>"; }; 4C9146FF2A2A891E00DDEA40 /* error.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = error.c; sourceTree = "<group>"; }; + 4C94D6422BA5AEFE00C26EFF /* QuoteRepostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuoteRepostsView.swift; sourceTree = "<group>"; }; 4C987B56283FD07F0042CE38 /* FollowersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersModel.swift; sourceTree = "<group>"; }; 4C9AA1492A4587A6003F49FD /* NotificationStatusModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationStatusModel.swift; sourceTree = "<group>"; }; 4C9B0DED2A65A75F00CBDA21 /* AttrStringTestExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttrStringTestExtensions.swift; sourceTree = "<group>"; }; @@ -1212,7 +1209,6 @@ 4CB8838A296F6E1E00DC99E7 /* NIP05Badge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05Badge.swift; sourceTree = "<group>"; }; 4CB8838C296F710400DC99E7 /* Reposted.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reposted.swift; sourceTree = "<group>"; }; 4CB8838E296F781C00DC99E7 /* ReactionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsView.swift; sourceTree = "<group>"; }; - 4CB88392296F798300DC99E7 /* ReactionsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionsModel.swift; sourceTree = "<group>"; }; 4CB88395296F7F8B00DC99E7 /* ReactionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReactionView.swift; sourceTree = "<group>"; }; 4CB88399297322D200DC99E7 /* DMTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMTests.swift; sourceTree = "<group>"; }; 4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrlPayRequest.swift; sourceTree = "<group>"; }; @@ -1512,6 +1508,7 @@ children = ( 3AA24801297E3DC20090C62D /* RepostView.swift */, 4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */, + 4C94D6422BA5AEFE00C26EFF /* QuoteRepostsView.swift */, ); path = Reposts; sourceTree = "<group>"; @@ -1590,7 +1587,6 @@ BA3759882ABCCDE30018D73B /* Camera */, 4C190F1E2A535FC200027FD5 /* Zaps */, 4C54AA0829A55416003E4487 /* Notifications */, - 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */, 4C0A3F8E280F640A000448DE /* ThreadModel.swift */, 4C0A3F92280F66F5000448DE /* ReplyMap.swift */, 4C3BEFD12819DB9B00B3DE84 /* ProfileModel.swift */, @@ -1620,7 +1616,6 @@ 4C216F372871EDE300040376 /* DirectMessageModel.swift */, BA693073295D649800ADDB87 /* UserSettingsStore.swift */, 4FE60CDC295E1C5E00105A1F /* Wallet.swift */, - 4CB88392296F798300DC99E7 /* ReactionsModel.swift */, 4CF0ABD32980996B00D66079 /* Report.swift */, 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */, 3AAA95C9298DF87B00F3D526 /* TranslationService.swift */, @@ -2262,7 +2257,6 @@ 4C86F7C32A76C44C00EC0817 /* ZappingNotify.swift */, 4C1253672A76D2470004F4B8 /* MuteNotify.swift */, 4C1253692A76D3850004F4B8 /* RelaysChangedNotify.swift */, - 4C12536B2A76D4B00004F4B8 /* RepostedNotify.swift */, 4C4E137A2A76D5FB00BDD832 /* MuteThreadNotify.swift */, 4C4E137C2A76D63600BDD832 /* UnmuteThreadNotify.swift */, B57B4C612B312BD700A232C0 /* ReconnectRelaysNotify.swift */, @@ -2744,20 +2738,20 @@ path = DamusNotificationService; sourceTree = "<group>"; }; - E06336A72B7582D600A88E6B /* Assets */ = { + D7CBD1D22B8D21C100BFD889 /* Extensions */ = { isa = PBXGroup; children = ( - E06336A82B7582E000A88E6B /* img_with_location.jpeg */, + D7CBD1D32B8D21DC00BFD889 /* DamusPurpleNotificationManagement.swift */, ); - path = Assets; + path = Extensions; sourceTree = "<group>"; }; - D7CBD1D22B8D21C100BFD889 /* Extensions */ = { + E06336A72B7582D600A88E6B /* Assets */ = { isa = PBXGroup; children = ( - D7CBD1D32B8D21DC00BFD889 /* DamusPurpleNotificationManagement.swift */, + E06336A82B7582E000A88E6B /* img_with_location.jpeg */, ); - path = Extensions; + path = Assets; sourceTree = "<group>"; }; F71694E82A66221E001F4053 /* Onboarding */ = { @@ -3113,7 +3107,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 */, @@ -3258,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 */, @@ -3329,7 +3323,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 +3414,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/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 @@ -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/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 { diff --git 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/EventsModel.swift b/damus/Models/EventsModel.swift @@ -7,25 +7,62 @@ 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 { + 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] + 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 } @@ -41,21 +78,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) @@ -66,10 +99,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/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/Models/ReactionsModel.swift b/damus/Models/ReactionsModel.swift @@ -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 @@ -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/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 @@ -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/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 @@ -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<RepostedNotify> { - .init() - } -} - -extension Notifications { - static func reposted(_ counts: Counted) -> Notifications<RepostedNotify> { - .init(.init(payload: counts)) - } -} - diff --git 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/damus/Util/Router.swift b/damus/Util/Router.swift @@ -31,8 +31,9 @@ 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 QuoteReposts(quotes: EventsModel) + case Reactions(reactions: EventsModel) case Zaps(target: ZapTarget) case Search(search: SearchModel) case EULA @@ -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 @@ -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'.") @@ -33,8 +33,17 @@ 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: 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 @@ -9,14 +9,14 @@ 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 { ScrollView { LazyVStack { - ForEach(model.events, id: \.id) { ev in + ForEach(model.events.events, id: \.id) { ev in ReactionView(damus_state: damus_state, reaction: ev) } } @@ -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/Reposts/QuoteRepostsView.swift 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<AnyView>(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)) + } +} diff --git a/damus/Views/RepostsView.swift b/damus/Views/RepostsView.swift @@ -9,12 +9,12 @@ import SwiftUI struct RepostsView: View { let damus_state: DamusState - @StateObject var model: RepostsModel + @StateObject var model: EventsModel 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) } } @@ -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)) } } diff --git a/damus/en-US.lproj/Localizable.stringsdict b/damus/en-US.lproj/Localizable.stringsdict @@ -226,6 +226,22 @@ <string>Reposts</string> </dict> </dict> + <key>quoted_reposts_count</key> + <dict> + <key>NSStringLocalizedFormatKey</key> + <string>%#@QUOTE_REPOSTS@</string> + <key>QUOTE_REPOSTS</key> + <dict> + <key>NSStringFormatSpecTypeKey</key> + <string>NSStringPluralRuleType</string> + <key>NSStringFormatValueTypeKey</key> + <string>d</string> + <key>one</key> + <string>Quote</string> + <key>other</key> + <string>Quotes</string> + </dict> + </dict> <key>sats</key> <dict> <key>NSStringLocalizedFormatKey</key> diff --git 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 } diff --git 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) }