damus

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

commit 64b1a57918d0aeed621185103c69414827d3f637
parent e4dd585754fa2d64bcde83c014c1468dfb05e361
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 21 Feb 2023 12:27:03 -0800

Notifications

Changelog-Added: Add new Notifications View

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 52++++++++++++++++++++++++++++++++++++++++++++++++++--
Mdamus/Components/UserView.swift | 6+-----
Mdamus/ContentView.swift | 5+++--
Mdamus/Models/DamusState.swift | 4++--
Mdamus/Models/EventsModel.swift | 2+-
Mdamus/Models/HomeModel.swift | 30+++++++++++++++---------------
Adamus/Models/Notifications/EventGroup.swift | 32++++++++++++++++++++++++++++++++
Adamus/Models/Notifications/ZapGroup.swift | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus/Models/NotificationsModel.swift | 294+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Models/SearchHomeModel.swift | 36++++++++++++++++++++++++++++++++----
Mdamus/Models/ThreadModel.swift | 2+-
Mdamus/Models/ZapsModel.swift | 6+++---
Mdamus/Nostr/NostrEvent.swift | 4++++
Adamus/Util/EventCache.swift | 27+++++++++++++++++++++++++++
Mdamus/Util/EventHolder.swift | 2+-
Mdamus/Util/InsertSort.swift | 18+++++++++++++++---
Mdamus/Util/Zaps.swift | 2+-
Mdamus/Views/DMChatView.swift | 4+---
Mdamus/Views/EventView.swift | 4+---
Mdamus/Views/Events/EventBody.swift | 10++++++++--
Mdamus/Views/Events/EventProfile.swift | 5+----
Mdamus/Views/Events/TextEvent.swift | 5+----
Adamus/Views/Notifications/EventGroupView.swift | 189+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus/Views/Notifications/NotificationItemView.swift | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus/Views/Notifications/NotificationsView.swift | 43+++++++++++++++++++++++++++++++++++++++++++
Adamus/Views/Notifications/ProfilePicturesView.swift | 37+++++++++++++++++++++++++++++++++++++
Mdamus/Views/ProfileView.swift | 21++++++++++++++++-----
27 files changed, 918 insertions(+), 61 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -47,6 +47,11 @@ 4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8B28398BC6008A31F1 /* Keys.swift */; }; 4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */; }; 4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */; }; + 4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */; }; + 4C30AC7429A5680900E2BD5A /* EventGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7329A5680900E2BD5A /* EventGroupView.swift */; }; + 4C30AC7629A5770900E2BD5A /* NotificationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */; }; + 4C30AC7829A577AB00E2BD5A /* EventCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7729A577AB00E2BD5A /* EventCache.swift */; }; + 4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7F29A6A53F00E2BD5A /* ProfilePicturesView.swift */; }; 4C363A8428233689006E126D /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8328233689006E126D /* Parser.swift */; }; 4C363A8828236948006E126D /* BlocksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8728236948006E126D /* BlocksView.swift */; }; 4C363A8A28236B57006E126D /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8928236B57006E126D /* MentionView.swift */; }; @@ -97,6 +102,9 @@ 4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */; }; 4C42812C298C848200DBF26F /* TranslateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C42812B298C848200DBF26F /* TranslateView.swift */; }; 4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C477C9D282C3A4800033AA3 /* TipCounter.swift */; }; + 4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0629A540BA003E4487 /* NotificationsModel.swift */; }; + 4C54AA0A29A55429003E4487 /* EventGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0929A55429003E4487 /* EventGroup.swift */; }; + 4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0B29A5543C003E4487 /* ZapGroup.swift */; }; 4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */; }; 4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */; }; 4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5F9113283D694D0052CD1C /* FollowTarget.swift */; }; @@ -345,6 +353,11 @@ 4C285C8B28398BC6008A31F1 /* Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keys.swift; sourceTree = "<group>"; }; 4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveKeysView.swift; sourceTree = "<group>"; }; 4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; }; + 4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; }; + 4C30AC7329A5680900E2BD5A /* EventGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGroupView.swift; sourceTree = "<group>"; }; + 4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemView.swift; sourceTree = "<group>"; }; + 4C30AC7729A577AB00E2BD5A /* EventCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventCache.swift; sourceTree = "<group>"; }; + 4C30AC7F29A6A53F00E2BD5A /* ProfilePicturesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePicturesView.swift; sourceTree = "<group>"; }; 4C363A8328233689006E126D /* Parser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = "<group>"; }; 4C363A8728236948006E126D /* BlocksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlocksView.swift; sourceTree = "<group>"; }; 4C363A8928236B57006E126D /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = "<group>"; }; @@ -425,6 +438,9 @@ 4C42812B298C848200DBF26F /* TranslateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslateView.swift; sourceTree = "<group>"; }; 4C477C9D282C3A4800033AA3 /* TipCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipCounter.swift; sourceTree = "<group>"; }; 4C4A3A5A288A1B2200453788 /* damus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = damus.entitlements; sourceTree = "<group>"; }; + 4C54AA0629A540BA003E4487 /* NotificationsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsModel.swift; sourceTree = "<group>"; }; + 4C54AA0929A55429003E4487 /* EventGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGroup.swift; sourceTree = "<group>"; }; + 4C54AA0B29A5543C003E4487 /* ZapGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapGroup.swift; sourceTree = "<group>"; }; 4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeModel.swift; sourceTree = "<group>"; }; 4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsView.swift; sourceTree = "<group>"; }; 4C5F9113283D694D0052CD1C /* FollowTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowTarget.swift; sourceTree = "<group>"; }; @@ -665,6 +681,7 @@ 4C0A3F8D280F63FF000448DE /* Models */ = { isa = PBXGroup; children = ( + 4C54AA0829A55416003E4487 /* Notifications */, 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */, 4C0A3F8E280F640A000448DE /* ThreadModel.swift */, 4C0A3F92280F66F5000448DE /* ReplyMap.swift */, @@ -704,13 +721,35 @@ 3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */, 4CE8795A2996C47A00F758CC /* ZapsModel.swift */, 3AA59D1C2999B0400061C48E /* DraftsModel.swift */, + 4C54AA0629A540BA003E4487 /* NotificationsModel.swift */, ); path = Models; sourceTree = "<group>"; }; + 4C30AC7029A5676F00E2BD5A /* Notifications */ = { + isa = PBXGroup; + children = ( + 4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */, + 4C30AC7329A5680900E2BD5A /* EventGroupView.swift */, + 4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */, + 4C30AC7F29A6A53F00E2BD5A /* ProfilePicturesView.swift */, + ); + path = Notifications; + sourceTree = "<group>"; + }; + 4C54AA0829A55416003E4487 /* Notifications */ = { + isa = PBXGroup; + children = ( + 4C54AA0929A55429003E4487 /* EventGroup.swift */, + 4C54AA0B29A5543C003E4487 /* ZapGroup.swift */, + ); + path = Notifications; + sourceTree = "<group>"; + }; 4C75EFA227FA576C0006080F /* Views */ = { isa = PBXGroup; children = ( + 4C30AC7029A5676F00E2BD5A /* Notifications */, 4CE0E2B029A3DF4700DB4CA2 /* Timeline */, 4CE879562996C44A00F758CC /* Zaps */, 4CB9D4A52992D01900A9A7E4 /* Profile */, @@ -828,6 +867,7 @@ 7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */, 4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */, 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */, + 4C30AC7729A577AB00E2BD5A /* EventCache.swift */, ); path = Util; sourceTree = "<group>"; @@ -1239,6 +1279,7 @@ 4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */, 4C363A8A28236B57006E126D /* MentionView.swift in Sources */, 4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */, + 4C30AC7829A577AB00E2BD5A /* EventCache.swift in Sources */, 4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */, 4C216F34286F5ACD00040376 /* DMView.swift in Sources */, 4C3EA64428FF558100C48A62 /* sha256.c in Sources */, @@ -1251,6 +1292,7 @@ 4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */, 4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */, 4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */, + 4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */, 4C75EFB728049D990006080F /* RelayPool.swift in Sources */, 4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */, 4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */, @@ -1293,6 +1335,7 @@ 4C649844285A952100EAE2B3 /* LocalUserConfig.swift in Sources */, 4C75EFB328049D640006080F /* NostrEvent.swift in Sources */, 4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */, + 4C30AC7629A5770900E2BD5A /* NotificationItemView.swift in Sources */, 4C363A8428233689006E126D /* Parser.swift in Sources */, 3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */, 4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */, @@ -1343,11 +1386,13 @@ 4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */, 4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */, 4CE879582996C45300F758CC /* ZapsView.swift in Sources */, + 4C30AC7429A5680900E2BD5A /* EventGroupView.swift in Sources */, 4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */, 9609F058296E220800069BF3 /* BannerImageView.swift in Sources */, 4C363A94282704FA006E126D /* Post.swift in Sources */, 4C216F32286E388800040376 /* DMChatView.swift in Sources */, 4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */, + 4C54AA0A29A55429003E4487 /* EventGroup.swift in Sources */, 4C3EA67928FF7ABF00C48A62 /* list.c in Sources */, 4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */, 4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */, @@ -1373,6 +1418,7 @@ 4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */, 4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */, 4CF0ABD82981980C00D66079 /* Lists.swift in Sources */, + 4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */, 4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */, 7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */, 6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */, @@ -1402,6 +1448,7 @@ 4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */, 4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */, 7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */, + 4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */, 4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */, 4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */, 4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */, @@ -1426,6 +1473,7 @@ 4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */, 4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */, 4C75EFA427FA577B0006080F /* PostView.swift in Sources */, + 4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */, 4C75EFB528049D790006080F /* Relay.swift in Sources */, 4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */, 4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */, @@ -1691,7 +1739,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_TEAM = XK7H4JAB3D; ENABLE_PREVIEWS = YES; @@ -1733,7 +1781,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_TEAM = XK7H4JAB3D; ENABLE_PREVIEWS = YES; diff --git a/damus/Components/UserView.swift b/damus/Components/UserView.swift @@ -12,11 +12,7 @@ struct UserView: View { let pubkey: String var body: some View { - let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state) - let followers = FollowersModel(damus_state: damus_state, target: pubkey) - let pv = ProfileView(damus_state: damus_state, profile: pmodel, followers: followers) - - NavigationLink(destination: pv) { + NavigationLink(destination: ProfileView(damus_state: damus_state, pubkey: pubkey)) { ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles) VStack(alignment: .leading) { diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -192,7 +192,7 @@ struct ContentView: View { case .notifications: VStack(spacing: 0) { Divider() - TimelineView(events: home.notifications, loading: $home.loading, damus: damus, show_friend_icon: true, filter: { _ in true }) + NotificationsView(state: damus, notifications: home.notifications) } case .dms: DirectMessagesView(damus_state: damus_state!) @@ -615,7 +615,8 @@ struct ContentView: View { settings: UserSettingsStore(), relay_filters: relay_filters, relay_metadata: metadatas, - drafts: Drafts() + drafts: Drafts(), + events: EventCache() ) home.damus_state = self.damus_state! diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift @@ -24,6 +24,7 @@ struct DamusState { let relay_filters: RelayFilters let relay_metadata: RelayMetadatas let drafts: Drafts + let events: EventCache var pubkey: String { return keypair.pubkey @@ -32,9 +33,8 @@ struct DamusState { var is_privkey_user: Bool { keypair.privkey != nil } - static var empty: DamusState { - return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts()) + return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache()) } } diff --git a/damus/Models/EventsModel.swift b/damus/Models/EventsModel.swift @@ -65,7 +65,7 @@ class EventsModel: ObservableObject { case .notice(_): break case .eose(_): - load_profiles(profiles_subid: profiles_id, relay_id: relay_id, events: events, damus_state: state) + load_profiles(profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state) } } } diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -50,22 +50,18 @@ class HomeModel: ObservableObject { let profiles_subid = UUID().description @Published var new_events: NewEventsBits = NewEventsBits() - @Published var notifications: EventHolder + @Published var notifications = NotificationsModel() @Published var dms: DirectMessagesModel - @Published var events: EventHolder + @Published var events = EventHolder() @Published var loading: Bool = false @Published var signal: SignalModel = SignalModel() init() { - self.events = EventHolder() - self.notifications = EventHolder() self.damus_state = DamusState.empty self.dms = DirectMessagesModel(our_pubkey: "") } init(damus_state: DamusState) { - self.events = EventHolder() - self.notifications = EventHolder() self.damus_state = damus_state self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey) self.setup_debouncer() @@ -143,7 +139,7 @@ class HomeModel: ObservableObject { return } - if !notifications.insert(ev) { + if !notifications.insert_zap(zap) { return } @@ -229,7 +225,7 @@ class HomeModel: ObservableObject { guard inner_ev.is_valid else { return } - + if inner_ev.is_textlike { handle_text_event(sub_id: sub_id, ev) } @@ -255,12 +251,11 @@ class HomeModel: ObservableObject { return } - // CHECK SIGS ON THESE - switch damus_state.likes.add_event(ev, target: e.ref_id) { case .already_counted: break case .success(let n): + handle_notification(ev: ev) let liked = Counted(event: ev, id: e.ref_id, total: n) notify(.liked, liked) notify(.update_stats, e.ref_id) @@ -320,9 +315,9 @@ class HomeModel: ObservableObject { if sub_id == dms_subid { var dms = dms.dms.flatMap { $0.1.events } dms.append(contentsOf: incoming_dms) - load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: dms, damus_state: damus_state) + load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(dms), damus_state: damus_state) } else if sub_id == notifications_subid { - load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: notifications.all_events, damus_state: damus_state) + load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_keys(notifications.uniq_pubkeys()), damus_state: damus_state) } self.loading = false @@ -375,7 +370,6 @@ class HomeModel: ObservableObject { // TODO: separate likes? var home_filter = NostrFilter.filter_kinds([ NostrKind.text.rawValue, - NostrKind.chat.rawValue, NostrKind.like.rawValue, NostrKind.boost.rawValue, ]) @@ -385,7 +379,6 @@ class HomeModel: ObservableObject { var notifications_filter = NostrFilter.filter_kinds([ NostrKind.text.rawValue, - NostrKind.chat.rawValue, NostrKind.like.rawValue, NostrKind.boost.rawValue, NostrKind.zap.rawValue, @@ -461,7 +454,12 @@ class HomeModel: ObservableObject { return } - if !notifications.insert(ev) { + damus_state.events.insert(ev) + if let inner_ev = ev.inner_event { + damus_state.events.insert(inner_ev) + } + + if !notifications.insert_event(ev) { return } @@ -484,6 +482,8 @@ class HomeModel: ObservableObject { guard should_show_event(contacts: damus_state.contacts, ev: ev) else { return } + + damus_state.events.insert(ev) if sub_id == home_subid { insert_home_event(ev) diff --git a/damus/Models/Notifications/EventGroup.swift b/damus/Models/Notifications/EventGroup.swift @@ -0,0 +1,32 @@ +// +// ReactionGroup.swift +// damus +// +// Created by William Casarin on 2023-02-21. +// + +import Foundation + +class EventGroup { + var events: [NostrEvent] + + var last_event_at: Int64 { + guard let first = self.events.first else { + return 0 + } + + return first.created_at + } + + init() { + self.events = [] + } + + init(events: [NostrEvent]) { + self.events = events + } + + func insert(_ ev: NostrEvent) -> Bool { + return insert_uniq_sorted_event_created(events: &events, new_ev: ev) + } +} diff --git a/damus/Models/Notifications/ZapGroup.swift b/damus/Models/Notifications/ZapGroup.swift @@ -0,0 +1,53 @@ +// +// ZapGroup.swift +// damus +// +// Created by William Casarin on 2023-02-21. +// + +import Foundation + +class ZapGroup { + var zaps: [Zap] + var msat_total: Int64 + var zappers: Set<String> + + var last_event_at: Int64 { + guard let first = zaps.first else { + return 0 + } + + return first.event.created_at + } + + func zap_requests() -> [NostrEvent] { + zaps.map { z in z.request.ev } + } + + init(zaps: [Zap]) { + self.zaps = zaps + self.msat_total = 0 + self.zappers = Set() + } + + init() { + self.zaps = [] + self.msat_total = 0 + self.zappers = Set() + } + + func insert(_ zap: Zap) -> Bool { + if !insert_uniq_sorted_zap_by_created(zaps: &zaps, new_zap: zap) { + return false + } + + msat_total += zap.invoice.amount + + if !zappers.contains(zap.request.ev.pubkey) { + zappers.insert(zap.request.ev.pubkey) + } + + return true + } +} + diff --git a/damus/Models/NotificationsModel.swift b/damus/Models/NotificationsModel.swift @@ -0,0 +1,294 @@ +// +// NotificationsModel.swift +// damus +// +// Created by William Casarin on 2023-02-21. +// + +import Foundation + +enum NotificationItem { + case repost(String, EventGroup) + case reaction(String, EventGroup) + case profile_zap(ZapGroup) + case event_zap(String, ZapGroup) + case reply(NostrEvent) + + var id: String { + switch self { + case .repost(let evid, _): + return "repost_" + evid + case .reaction(let evid, _): + return "reaction_" + evid + case .profile_zap: + return "profile_zap" + case .event_zap(let evid, _): + return "event_zap_" + evid + case .reply(let ev): + return "reply_" + ev.id + } + } + + var last_event_at: Int64 { + switch self { + case .reaction(_, let evgrp): + return evgrp.last_event_at + case .repost(_, let evgrp): + return evgrp.last_event_at + case .profile_zap(let zapgrp): + return zapgrp.last_event_at + case .event_zap(_, let zapgrp): + return zapgrp.last_event_at + case .reply(let reply): + return reply.created_at + } + } +} + +class NotificationsModel: ObservableObject { + var incoming_zaps: [Zap] + var incoming_events: [NostrEvent] + var should_queue: Bool + + // mappings from events to + var zaps: [String: ZapGroup] + var profile_zaps: ZapGroup + var reactions: [String: EventGroup] + var reposts: [String: EventGroup] + var replies: [NostrEvent] + var has_reply: Set<String> + + @Published var notifications: [NotificationItem] + + init() { + self.zaps = [:] + self.reactions = [:] + self.reposts = [:] + self.replies = [] + self.has_reply = Set() + self.should_queue = true + self.incoming_zaps = [] + self.incoming_events = [] + self.profile_zaps = ZapGroup() + self.notifications = [] + } + + func uniq_pubkeys() -> [String] { + var pks = Set<String>() + + for ev in incoming_events { + pks.insert(ev.pubkey) + } + + for grp in reposts { + for ev in grp.value.events { + pks.insert(ev.pubkey) + } + } + + for ev in replies { + pks.insert(ev.pubkey) + } + + for zap in incoming_zaps { + pks.insert(zap.request.ev.pubkey) + } + + return Array(pks) + } + + func build_notifications() -> [NotificationItem] { + var notifs: [NotificationItem] = [] + + for el in zaps { + let evid = el.key + let zapgrp = el.value + + let notif: NotificationItem = .event_zap(evid, zapgrp) + notifs.append(notif) + } + + if !profile_zaps.zaps.isEmpty { + notifs.append(.profile_zap(profile_zaps)) + } + + for el in reposts { + let evid = el.key + let evgrp = el.value + + notifs.append(.repost(evid, evgrp)) + } + + for el in reactions { + let evid = el.key + let evgrp = el.value + + notifs.append(.reaction(evid, evgrp)) + } + + for reply in replies { + notifs.append(.reply(reply)) + } + + notifs.sort { $0.last_event_at > $1.last_event_at } + return notifs + } + + + private func insert_repost(_ ev: NostrEvent) -> Bool { + guard let reposted_ev = ev.inner_event else { + return false + } + + let id = reposted_ev.id + + if let evgrp = self.reposts[id] { + return evgrp.insert(ev) + } else { + let evgrp = EventGroup() + self.reposts[id] = evgrp + return evgrp.insert(ev) + } + } + + private func insert_text(_ ev: NostrEvent) -> Bool { + guard !has_reply.contains(ev.id) else { + return false + } + + has_reply.insert(ev.id) + replies.append(ev) + + return true + } + + private func insert_reaction(_ ev: NostrEvent) -> Bool { + guard let ref_id = ev.referenced_ids.last else { + return false + } + + let id = ref_id.id + + if let evgrp = self.reactions[id] { + return evgrp.insert(ev) + } else { + let evgrp = EventGroup() + self.reactions[id] = evgrp + return evgrp.insert(ev) + } + } + + private func insert_event_immediate(_ ev: NostrEvent) -> Bool { + if ev.known_kind == .boost { + return insert_repost(ev) + } else if ev.known_kind == .like { + return insert_reaction(ev) + } else if ev.known_kind == .text { + return insert_text(ev) + } + + return false + } + + private func insert_zap_immediate(_ zap: Zap) -> Bool { + switch zap.target { + case .note(let notezt): + let id = notezt.note_id + if let zapgrp = self.zaps[notezt.note_id] { + return zapgrp.insert(zap) + } else { + let zapgrp = ZapGroup() + self.zaps[id] = zapgrp + return zapgrp.insert(zap) + } + + case .profile: + return profile_zaps.insert(zap) + } + } + + func insert_event(_ ev: NostrEvent) -> Bool { + if should_queue { + return insert_uniq_sorted_event_created(events: &incoming_events, new_ev: ev) + } + + if insert_event_immediate(ev) { + self.notifications = build_notifications() + return true + } + + return false + } + + func insert_zap(_ zap: Zap) -> Bool { + if should_queue { + return insert_uniq_sorted_zap_by_created(zaps: &incoming_zaps, new_zap: zap) + } + + if insert_zap_immediate(zap) { + self.notifications = build_notifications() + return true + } + + return false + } + + func filter(_ isIncluded: (NostrEvent) -> Bool) { + var changed = false + var count = 0 + + count = incoming_events.count + incoming_events = incoming_events.filter(isIncluded) + changed = changed || incoming_events.count != count + + count = profile_zaps.zaps.count + profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request.ev) } + changed = changed || profile_zaps.zaps.count != count + + for el in reactions { + count = el.value.events.count + el.value.events = el.value.events.filter(isIncluded) + changed = changed || el.value.events.count != count + } + + for el in reposts { + count = el.value.events.count + el.value.events = el.value.events.filter(isIncluded) + changed = changed || el.value.events.count != count + } + + for el in zaps { + count = el.value.zaps.count + el.value.zaps = el.value.zaps.filter { + isIncluded($0.request.ev) + } + changed = changed || el.value.zaps.count != count + } + + count = replies.count + replies = replies.filter(isIncluded) + changed = changed || replies.count != count + + if changed { + self.notifications = build_notifications() + } + } + + func flush() -> Bool { + var inserted = false + + for zap in incoming_zaps { + inserted = insert_zap_immediate(zap) || inserted + } + + for event in incoming_events { + inserted = insert_event_immediate(event) || inserted + } + + if inserted { + self.notifications = build_notifications() + } + + return inserted + } +} diff --git a/damus/Models/SearchHomeModel.swift b/damus/Models/SearchHomeModel.swift @@ -76,7 +76,7 @@ class SearchHomeModel: ObservableObject { // global events are not realtime unsubscribe(to: relay_id) - load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: events.all_events, damus_state: damus_state) + load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.all_events), damus_state: damus_state) } @@ -98,8 +98,31 @@ func find_profiles_to_fetch_pk(profiles: Profiles, event_pubkeys: [String]) -> [ return Array(pubkeys) } + +func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad) -> [String] { + switch load { + case .from_events(let events): + return find_profiles_to_fetch_from_events(profiles: profiles, events: events) + case .from_keys(let pks): + return find_profiles_to_fetch_from_keys(profiles: profiles, pks: pks) + } +} + +func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [String]) -> [String] { + var pubkeys = Set<String>() -func find_profiles_to_fetch(profiles: Profiles, events: [NostrEvent]) -> [String] { + for pk in pks { + if profiles.lookup(id: pk) != nil { + continue + } + + pubkeys.insert(pk) + } + + return Array(pubkeys) +} + +func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent]) -> [String] { var pubkeys = Set<String>() for ev in events { @@ -113,9 +136,14 @@ func find_profiles_to_fetch(profiles: Profiles, events: [NostrEvent]) -> [String return Array(pubkeys) } -func load_profiles(profiles_subid: String, relay_id: String, events: [NostrEvent], damus_state: DamusState) { +enum PubkeysToLoad { + case from_events([NostrEvent]) + case from_keys([String]) +} + +func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad, damus_state: DamusState) { var filter = NostrFilter.filter_profiles - let authors = find_profiles_to_fetch(profiles: damus_state.profiles, events: events) + let authors = find_profiles_to_fetch(profiles: damus_state.profiles, load: load) filter.authors = authors guard !authors.isEmpty else { diff --git a/damus/Models/ThreadModel.swift b/damus/Models/ThreadModel.swift @@ -207,7 +207,7 @@ class ThreadModel: ObservableObject { } if sub_id == self.base_subid { - load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, events: events, damus_state: damus_state) + load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: damus_state) } } diff --git a/damus/Models/ZapsModel.swift b/damus/Models/ZapsModel.swift @@ -50,14 +50,14 @@ class ZapsModel: ObservableObject { break case .eose: let events = self.zaps.map { $0.request.ev } - load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: events, damus_state: state) + load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state) case .event(_, let ev): guard ev.kind == 9735 else { return } if let zap = state.zaps.zaps[ev.id] { - if insert_uniq_sorted_zap(zaps: &zaps, new_zap: zap) { + if insert_uniq_sorted_zap_by_amount(zaps: &zaps, new_zap: zap) { objectWillChange.send() } } else { @@ -71,7 +71,7 @@ class ZapsModel: ObservableObject { state.zaps.add_zap(zap: zap) - if insert_uniq_sorted_zap(zaps: &zaps, new_zap: zap) { + if insert_uniq_sorted_zap_by_amount(zaps: &zaps, new_zap: zap) { objectWillChange.send() } } diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift @@ -168,6 +168,9 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has return decrypted(privkey: privkey) ?? "*failed to decrypt content*" } + return content + + /* switch validity { case .ok: return content @@ -176,6 +179,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has case .bad_sig: return content + "\n\n*WARNING: invalid signature, could be forged!*" } + */ } var description: String { diff --git a/damus/Util/EventCache.swift b/damus/Util/EventCache.swift @@ -0,0 +1,27 @@ +// +// EventCache.swift +// damus +// +// Created by William Casarin on 2023-02-21. +// + +import Foundation + +class EventCache { + private var events: [String: NostrEvent] + + func lookup(_ evid: String) -> NostrEvent? { + return events[evid] + } + + func insert(_ ev: NostrEvent) { + guard events[ev.id] == nil else { + return + } + events[ev.id] = ev + } + + init() { + self.events = [:] + } +} diff --git a/damus/Util/EventHolder.swift b/damus/Util/EventHolder.swift @@ -12,7 +12,7 @@ class EventHolder: ObservableObject { private var has_event: Set<String> @Published var events: [NostrEvent] @Published var incoming: [NostrEvent] - @Published var should_queue: Bool + var should_queue: Bool var queued: Int { return incoming.count diff --git a/damus/Util/InsertSort.swift b/damus/Util/InsertSort.swift @@ -38,8 +38,7 @@ func insert_uniq_by_pubkey(events: inout [NostrEvent], new_ev: NostrEvent, cmp: return true } -@discardableResult -func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool { +func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap, cmp: (Zap, Zap) -> Bool) -> Bool { var i: Int = 0 for zap in zaps { @@ -48,7 +47,7 @@ func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool { return false } - if new_zap.invoice.amount > zap.invoice.amount { + if cmp(new_zap, zap) { zaps.insert(new_zap, at: i) return true } @@ -59,6 +58,19 @@ func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool { return true } +@discardableResult +func insert_uniq_sorted_zap_by_created(zaps: inout [Zap], new_zap: Zap) -> Bool { + return insert_uniq_sorted_zap(zaps: &zaps, new_zap: new_zap) { (a, b) in + a.event.created_at > b.event.created_at + } +} + +@discardableResult +func insert_uniq_sorted_zap_by_amount(zaps: inout [Zap], new_zap: Zap) -> Bool { + return insert_uniq_sorted_zap(zaps: &zaps, new_zap: new_zap) { (a, b) in + a.invoice.amount > b.invoice.amount + } +} func insert_uniq_sorted_event_created(events: inout [NostrEvent], new_ev: NostrEvent) -> Bool { return insert_uniq_sorted_event(events: &events, new_ev: new_ev) { diff --git a/damus/Util/Zaps.swift b/damus/Util/Zaps.swift @@ -36,7 +36,7 @@ class Zaps { if our_zaps[note_target.note_id] == nil { our_zaps[note_target.note_id] = [zap] } else { - insert_uniq_sorted_zap(zaps: &(our_zaps[note_target.note_id]!), new_zap: zap) + insert_uniq_sorted_zap_by_amount(zaps: &(our_zaps[note_target.note_id]!), new_zap: zap) } case .profile(_): break diff --git a/damus/Views/DMChatView.swift b/damus/Views/DMChatView.swift @@ -37,9 +37,7 @@ struct DMChatView: View { var Header: some View { let profile = damus_state.profiles.lookup(id: pubkey) - let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state) - let fmodel = FollowersModel(damus_state: damus_state, target: pubkey) - let profile_page = ProfileView(damus_state: damus_state, profile: pmodel, followers: fmodel) + let profile_page = ProfileView(damus_state: damus_state, pubkey: pubkey) return NavigationLink(destination: profile_page) { HStack { ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles) diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift @@ -61,10 +61,8 @@ struct EventView: View { if event.known_kind == .boost { if let inner_ev = event.inner_event { VStack(alignment: .leading) { - let prof_model = ProfileModel(pubkey: event.pubkey, damus: damus) - let follow_model = FollowersModel(damus_state: damus, target: event.pubkey) let prof = damus.profiles.lookup(id: event.pubkey) - let booster_profile = ProfileView(damus_state: damus, profile: prof_model, followers: follow_model) + let booster_profile = ProfileView(damus_state: damus, pubkey: event.pubkey) NavigationLink(destination: booster_profile) { Reposted(damus: damus, pubkey: event.pubkey, profile: prof) diff --git a/damus/Views/Events/EventBody.swift b/damus/Views/Events/EventBody.swift @@ -11,6 +11,14 @@ struct EventBody: View { let damus_state: DamusState let event: NostrEvent let size: EventViewKind + let should_show_img: Bool + + init(damus_state: DamusState, event: NostrEvent, size: EventViewKind, should_show_img: Bool? = nil) { + self.damus_state = damus_state + self.event = event + self.size = size + self.should_show_img = should_show_img ?? should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey) + } var content: String { event.get_content(damus_state.keypair.privkey) @@ -21,8 +29,6 @@ struct EventBody: View { ReplyDescription(event: event, profiles: damus_state.profiles) } - let should_show_img = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey, booster_pubkey: nil) - NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: size, artifacts: .just_content(content)) .frame(maxWidth: .infinity, alignment: .leading) } diff --git a/damus/Views/Events/EventProfile.swift b/damus/Views/Events/EventProfile.swift @@ -31,10 +31,7 @@ struct EventProfile: View { var body: some View { HStack(alignment: .center) { VStack { - let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state) - let pv = ProfileView(damus_state: damus_state, profile: pmodel, followers: FollowersModel(damus_state: damus_state, target: pubkey)) - - NavigationLink(destination: pv) { + NavigationLink(destination: ProfileView(damus_state: damus_state, pubkey: pubkey)) { ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles) } } diff --git a/damus/Views/Events/TextEvent.swift b/damus/Views/Events/TextEvent.swift @@ -19,10 +19,7 @@ struct TextEvent: View { let profile = damus.profiles.lookup(id: pubkey) VStack { - let pmodel = ProfileModel(pubkey: pubkey, damus: damus) - let pv = ProfileView(damus_state: damus, profile: pmodel, followers: FollowersModel(damus_state: damus, target: pubkey)) - - NavigationLink(destination: pv) { + NavigationLink(destination: ProfileView(damus_state: damus, pubkey: pubkey)) { ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus.profiles) } diff --git a/damus/Views/Notifications/EventGroupView.swift b/damus/Views/Notifications/EventGroupView.swift @@ -0,0 +1,189 @@ +// +// RepostGroupView.swift +// damus +// +// Created by William Casarin on 2023-02-21. +// + +import SwiftUI + + +enum EventGroupType { + case repost(EventGroup) + case reaction(EventGroup) + case zap(ZapGroup) + case profile_zap(ZapGroup) + + var events: [NostrEvent] { + switch self { + case .repost(let grp): + return grp.events + case .reaction(let grp): + return grp.events + case .zap(let zapgrp): + return zapgrp.zap_requests() + case .profile_zap(let zapgrp): + return zapgrp.zap_requests() + } + } +} + +enum ReactingTo { + case your_post + case tagged_in + case your_profile +} + +func determine_reacting_to(our_pubkey: String, ev: NostrEvent?) -> ReactingTo { + guard let ev else { + return .your_profile + } + + if ev.pubkey == our_pubkey { + return .your_post + } + + return .tagged_in +} + +func determine_reacting_to_text(_ r: ReactingTo) -> String { + switch r { + case .tagged_in: + return "a post you were tagged in" + case .your_post: + return "your post" + case .your_profile: + return "your profile" + } +} + +func event_author_name(profiles: Profiles, _ ev: NostrEvent) -> String { + let alice_pk = ev.pubkey + let alice_prof = profiles.lookup(id: alice_pk) + return Profile.displayName(profile: alice_prof, pubkey: alice_pk) +} + +func reacting_to_text(profiles: Profiles, our_pubkey: String, group: EventGroupType, ev: NostrEvent?) -> String { + let verb = reacting_to_verb(group: group) + + let reacting_to = determine_reacting_to(our_pubkey: our_pubkey, ev: ev) + let target = determine_reacting_to_text(reacting_to) + + if group.events.count == 1 { + let ev = group.events.first! + let profile = profiles.lookup(id: ev.pubkey) + let display_name = Profile.displayName(profile: profile, pubkey: ev.pubkey) + return String(format: "%@ is %@ %@", display_name, verb, target) + } + + if group.events.count == 2 { + let alice_name = event_author_name(profiles: profiles, group.events[0]) + let bob_name = event_author_name(profiles: profiles, group.events[1]) + + return String(format: "%@ and %@ are %@ %@", alice_name, bob_name, verb, target) + } + + if group.events.count > 2 { + let alice_name = event_author_name(profiles: profiles, group.events.first!) + let count = group.events.count - 1 + + return String(format: "%@ and %d other people are %@ %@", alice_name, count, verb, target) + } + + return "??" +} + +func reacting_to_verb(group: EventGroupType) -> String { + switch group { + case .reaction: + return "reacting" + case .repost: + return "reposting" + case .zap: fallthrough + case .profile_zap: + return "zapping" + } +} + +struct EventGroupView: View { + let state: DamusState + let event: NostrEvent? + let group: EventGroupType + + var GroupDescription: some View { + Text(reacting_to_text(profiles: state.profiles, our_pubkey: state.pubkey, group: group, ev: event)) + } + + func ZapIcon(_ zapgrp: ZapGroup) -> some View { + let fmt = format_msats_abbrev(zapgrp.msat_total) + return VStack(alignment: .center) { + Image(systemName: "bolt.fill") + .foregroundColor(.orange) + Text("\(fmt)") + .foregroundColor(Color.orange) + } + } + + var GroupIcon: some View { + Group { + switch group { + case .repost: + Image(systemName: "arrow.2.squarepath") + .foregroundColor(Color("DamusGreen")) + case .reaction: + Image("shaka-full") + .resizable() + .frame(width: 24, height: 24) + .foregroundColor(.accentColor) + case .profile_zap(let zapgrp): + ZapIcon(zapgrp) + case .zap(let zapgrp): + ZapIcon(zapgrp) + } + } + } + + var body: some View { + HStack(alignment: .top) { + GroupIcon + .frame(width: PFP_SIZE + 10) + + VStack(alignment: .leading) { + ProfilePicturesView(state: state, events: group.events) + + GroupDescription + + if let event { + NavigationLink(destination: BuildThreadV2View(damus: state, event_id: event.id)) { + Text(event.content) + .padding([.top], 1) + .foregroundColor(.gray) + } + .buttonStyle(.plain) + } + } + } + .padding([.top], 6) + } +} + +let test_encoded_post = "{\"id\": \"8ba545ab96959fe0ce7db31bc10f3ac3aa5353bc4428dbf1e56a7be7062516db\",\"pubkey\": \"7e27509ccf1e297e1df164912a43406218f8bd80129424c3ef798ca3ef5c8444\",\"created_at\": 1677013417,\"kind\": 1,\"tags\": [],\"content\": \"hello\",\"sig\": \"93684f15eddf11f42afbdd81828ee9fc35350344d8650c78909099d776e9ad8d959cd5c4bff7045be3b0b255144add43d0feef97940794a1bc9c309791bebe4a\"}" +let test_repost = NostrEvent(id: "", content: test_encoded_post, pubkey: "", kind: 6, tags: [], createdAt: 1) +let test_reposts = [test_repost, test_repost] +let test_event_group = EventGroup(events: test_reposts) + +struct EventGroupView_Previews: PreviewProvider { + static var previews: some View { + VStack { + EventGroupView(state: test_damus_state(), event: test_event, group: .repost(test_event_group)) + .frame(height: 200) + .padding() + + EventGroupView(state: test_damus_state(), event: test_event, group: .reaction(test_event_group)) + .frame(height: 200) + .padding() + } + } + +} + diff --git a/damus/Views/Notifications/NotificationItemView.swift b/damus/Views/Notifications/NotificationItemView.swift @@ -0,0 +1,86 @@ +// +// NotificationItemView.swift +// damus +// +// Created by William Casarin on 2023-02-21. +// + +import SwiftUI + +enum ShowItem { + case show(NostrEvent?) + case dontshow(NostrEvent?) +} + +func notification_item_event(events: EventCache, notif: NotificationItem) -> ShowItem { + switch notif { + case .repost(let evid, _): + return .dontshow(events.lookup(evid)) + case .reply(let ev): + return .show(ev) + case .reaction(let evid, _): + return .dontshow(events.lookup(evid)) + case .event_zap(let evid, _): + return .dontshow(events.lookup(evid)) + case .profile_zap: + return .show(nil) + } +} + +struct NotificationItemView: View { + let state: DamusState + let item: NotificationItem + + var show_item: ShowItem { + notification_item_event(events: state.events, notif: item) + } + + func Item(_ ev: NostrEvent?) -> some View { + Group { + switch item { + case .repost(_, let evgrp): + EventGroupView(state: state, event: ev, group: .repost(evgrp)) + + case .event_zap(_, let zapgrp): + EventGroupView(state: state, event: ev, group: .zap(zapgrp)) + + case .profile_zap(let grp): + EventGroupView(state: state, event: nil, group: .profile_zap(grp)) + + case .reaction(_, let evgrp): + EventGroupView(state: state, event: ev, group: .reaction(evgrp)) + + case .reply(let ev): + NavigationLink(destination: BuildThreadV2View(damus: state, event_id: ev.id)) { + EventView(damus: state, event: ev, has_action_bar: true) + } + .buttonStyle(.plain) + } + + Divider() + .padding([.top,.bottom], 5) + } + } + + var body: some View { + Group { + switch show_item { + case .show(let ev): + Item(ev) + + case .dontshow(let ev): + if let ev { + Item(ev) + } + } + } + } +} + +let test_notification_item: NotificationItem = .repost("evid", test_event_group) + +struct NotificationItemView_Previews: PreviewProvider { + static var previews: some View { + NotificationItemView(state: test_damus_state(), item: test_notification_item) + } +} diff --git a/damus/Views/Notifications/NotificationsView.swift b/damus/Views/Notifications/NotificationsView.swift @@ -0,0 +1,43 @@ +// +// NotificationsView.swift +// damus +// +// Created by William Casarin on 2023-02-21. +// + +import SwiftUI + +struct NotificationsView: View { + let state: DamusState + @ObservedObject var notifications: NotificationsModel + + var body: some View { + ScrollViewReader { scroller in + ScrollView { + LazyVStack(alignment: .leading) { + Color.white.opacity(0) + .id("startblock") + .frame(height: 5) + ForEach(notifications.notifications, id: \.id) { item in + NotificationItemView(state: state, item: item) + } + } + .padding(.horizontal) + } + .onReceive(handle_notify(.scroll_to_top)) { notif in + let _ = notifications.flush() + self.notifications.should_queue = false + scroll_to_event(scroller: scroller, id: "startblock", delay: 0.0, animate: true, anchor: .top) + } + } + .onAppear { + let _ = notifications.flush() + } + } +} + +struct NotificationsView_Previews: PreviewProvider { + static var previews: some View { + NotificationsView(state: test_damus_state(), notifications: NotificationsModel()) + } +} diff --git a/damus/Views/Notifications/ProfilePicturesView.swift b/damus/Views/Notifications/ProfilePicturesView.swift @@ -0,0 +1,37 @@ +// +// ProfilePicturesView.swift +// damus +// +// Created by William Casarin on 2023-02-22. +// + +import SwiftUI + +struct ProfilePicturesView: View { + let state: DamusState + let events: [NostrEvent] + + @State var nav_target: String? = nil + @State var navigating: Bool = false + + var body: some View { + NavigationLink(destination: ProfileView(damus_state: state, pubkey: nav_target ?? ""), isActive: $navigating) { + EmptyView() + } + HStack { + ForEach(events.prefix(8)) { ev in + ProfilePicView(pubkey: ev.pubkey, size: 32.0, highlight: .none, profiles: state.profiles) + .onTapGesture { + nav_target = ev.pubkey + navigating = true + } + } + } + } +} + +struct ProfilePicturesView_Previews: PreviewProvider { + static var previews: some View { + ProfilePicturesView(state: test_damus_state(), events: [test_event, test_event]) + } +} diff --git a/damus/Views/ProfileView.swift b/damus/Views/ProfileView.swift @@ -110,8 +110,6 @@ struct ProfileView: View { static let markdown = Markdown() @State private var selected_tab: ProfileTab = .posts - @StateObject var profile: ProfileModel - @StateObject var followers: FollowersModel @State private var showingEditProfile = false @State var showing_select_wallet: Bool = false @State var is_zoomed: Bool = false @@ -120,6 +118,21 @@ struct ProfileView: View { @State var filter_state : FilterState = .posts @State var yOffset: CGFloat = 0 + @StateObject var profile: ProfileModel + @StateObject var followers: FollowersModel + + init(damus_state: DamusState, profile: ProfileModel, followers: FollowersModel) { + self.damus_state = damus_state + self._profile = StateObject(wrappedValue: profile) + self._followers = StateObject(wrappedValue: followers) + } + + init(damus_state: DamusState, pubkey: String) { + self.damus_state = damus_state + self._profile = StateObject(wrappedValue: ProfileModel(pubkey: pubkey, damus: damus_state)) + self._followers = StateObject(wrappedValue: FollowersModel(damus_state: damus_state, target: pubkey)) + } + @Environment(\.dismiss) var dismiss @Environment(\.colorScheme) var colorScheme @Environment(\.openURL) var openURL @@ -459,9 +472,7 @@ struct ProfileView: View { struct ProfileView_Previews: PreviewProvider { static var previews: some View { let ds = test_damus_state() - let followers = FollowersModel(damus_state: ds, target: ds.pubkey) - let profile_model = ProfileModel(pubkey: ds.pubkey, damus: ds) - ProfileView(damus_state: ds, profile: profile_model, followers: followers) + ProfileView(damus_state: ds, pubkey: ds.pubkey) } }