Merge pull request #3204 from damus-io/local-relay-model
This integrates all the local relay model work done in PR #3204. Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
This commit is contained in:
@@ -67,40 +67,40 @@ class NotificationService: UNNotificationServiceExtension {
|
|||||||
nip05: profile?.nip05)
|
nip05: profile?.nip05)
|
||||||
}()
|
}()
|
||||||
let sender_pubkey = nostr_event.pubkey
|
let sender_pubkey = nostr_event.pubkey
|
||||||
|
|
||||||
// Don't show notification details that match mute list.
|
|
||||||
// TODO: Remove this code block once we get notification suppression entitlement from Apple. It will be covered by the `guard should_display_notification` block
|
|
||||||
if state.mutelist_manager.is_event_muted(nostr_event) {
|
|
||||||
// We cannot really suppress muted notifications until we have the notification supression entitlement.
|
|
||||||
// The best we can do if we ever get those muted notifications (which we generally won't due to server-side processing) is to obscure the details
|
|
||||||
let content = UNMutableNotificationContent()
|
|
||||||
content.title = NSLocalizedString("Muted event", comment: "Title for a push notification which has been muted")
|
|
||||||
content.body = NSLocalizedString("This is an event that has been muted according to your mute list rules. We cannot suppress this notification, but we obscured the details to respect your preferences", comment: "Description for a push notification which has been muted, and explanation that we cannot suppress it")
|
|
||||||
content.sound = UNNotificationSound.default
|
|
||||||
contentHandler(content)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard should_display_notification(state: state, event: nostr_event, mode: .push) else {
|
|
||||||
Log.debug("should_display_notification failed", for: .push_notifications)
|
|
||||||
// We should not display notification for this event. Suppress notification.
|
|
||||||
// contentHandler(UNNotificationContent())
|
|
||||||
// TODO: We cannot really suppress until we have the notification supression entitlement. Show the raw notification
|
|
||||||
contentHandler(request.content)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let notification_object = generate_local_notification_object(ndb: state.ndb, from: nostr_event, state: state) else {
|
|
||||||
Log.debug("generate_local_notification_object failed", for: .push_notifications)
|
|
||||||
// We could not process this notification. Probably an unsupported nostr event kind. Suppress.
|
|
||||||
// contentHandler(UNNotificationContent())
|
|
||||||
// TODO: We cannot really suppress until we have the notification supression entitlement. Show the raw notification
|
|
||||||
contentHandler(request.content)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
|
|
||||||
|
// Don't show notification details that match mute list.
|
||||||
|
// TODO: Remove this code block once we get notification suppression entitlement from Apple. It will be covered by the `guard should_display_notification` block
|
||||||
|
if await state.mutelist_manager.is_event_muted(nostr_event) {
|
||||||
|
// We cannot really suppress muted notifications until we have the notification supression entitlement.
|
||||||
|
// The best we can do if we ever get those muted notifications (which we generally won't due to server-side processing) is to obscure the details
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = NSLocalizedString("Muted event", comment: "Title for a push notification which has been muted")
|
||||||
|
content.body = NSLocalizedString("This is an event that has been muted according to your mute list rules. We cannot suppress this notification, but we obscured the details to respect your preferences", comment: "Description for a push notification which has been muted, and explanation that we cannot suppress it")
|
||||||
|
content.sound = UNNotificationSound.default
|
||||||
|
contentHandler(content)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard await should_display_notification(state: state, event: nostr_event, mode: .push) else {
|
||||||
|
Log.debug("should_display_notification failed", for: .push_notifications)
|
||||||
|
// We should not display notification for this event. Suppress notification.
|
||||||
|
// contentHandler(UNNotificationContent())
|
||||||
|
// TODO: We cannot really suppress until we have the notification supression entitlement. Show the raw notification
|
||||||
|
contentHandler(request.content)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let notification_object = generate_local_notification_object(ndb: state.ndb, from: nostr_event, state: state) else {
|
||||||
|
Log.debug("generate_local_notification_object failed", for: .push_notifications)
|
||||||
|
// We could not process this notification. Probably an unsupported nostr event kind. Suppress.
|
||||||
|
// contentHandler(UNNotificationContent())
|
||||||
|
// TODO: We cannot really suppress until we have the notification supression entitlement. Show the raw notification
|
||||||
|
contentHandler(request.content)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let sender_dn = DisplayName(name: sender_profile.name, display_name: sender_profile.display_name, pubkey: sender_pubkey)
|
let sender_dn = DisplayName(name: sender_profile.name, display_name: sender_profile.display_name, pubkey: sender_pubkey)
|
||||||
guard let (improvedContent, _) = await NotificationFormatter.shared.format_message(displayName: sender_dn.displayName, notify: notification_object, state: state) else {
|
guard let (improvedContent, _) = await NotificationFormatter.shared.format_message(displayName: sender_dn.displayName, notify: notification_object, state: state) else {
|
||||||
|
|
||||||
|
|||||||
@@ -1189,6 +1189,15 @@
|
|||||||
D72A2D022AD9C136002AFF62 /* EventViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */; };
|
D72A2D022AD9C136002AFF62 /* EventViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */; };
|
||||||
D72A2D052AD9C1B5002AFF62 /* MockDamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */; };
|
D72A2D052AD9C1B5002AFF62 /* MockDamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */; };
|
||||||
D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */; };
|
D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */; };
|
||||||
|
D72B6FA22E7DFB450050CD1D /* ProfilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72B6FA12E7DFB3F0050CD1D /* ProfilesManager.swift */; };
|
||||||
|
D72B6FA32E7DFB450050CD1D /* ProfilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72B6FA12E7DFB3F0050CD1D /* ProfilesManager.swift */; };
|
||||||
|
D72B6FA42E7DFB450050CD1D /* ProfilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72B6FA12E7DFB3F0050CD1D /* ProfilesManager.swift */; };
|
||||||
|
D72B6FA62E7E06AD0050CD1D /* ProfileObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72B6FA52E7E06A40050CD1D /* ProfileObserver.swift */; };
|
||||||
|
D72B6FA72E7E06AD0050CD1D /* ProfileObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72B6FA52E7E06A40050CD1D /* ProfileObserver.swift */; };
|
||||||
|
D72B6FA92E7E06AD0050CD1D /* ProfileObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72B6FA52E7E06A40050CD1D /* ProfileObserver.swift */; };
|
||||||
|
D72C01312E78C10500AACB67 /* CondensedProfilePicturesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72C01302E78C0FB00AACB67 /* CondensedProfilePicturesViewModel.swift */; };
|
||||||
|
D72C01322E78C10500AACB67 /* CondensedProfilePicturesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72C01302E78C0FB00AACB67 /* CondensedProfilePicturesViewModel.swift */; };
|
||||||
|
D72C01332E78C10500AACB67 /* CondensedProfilePicturesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72C01302E78C0FB00AACB67 /* CondensedProfilePicturesViewModel.swift */; };
|
||||||
D72E12782BEED22500F4F781 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72E12772BEED22400F4F781 /* Array.swift */; };
|
D72E12782BEED22500F4F781 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72E12772BEED22400F4F781 /* Array.swift */; };
|
||||||
D72E127A2BEEEED000F4F781 /* NostrFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */; };
|
D72E127A2BEEEED000F4F781 /* NostrFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */; };
|
||||||
D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; };
|
D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; };
|
||||||
@@ -1784,8 +1793,14 @@
|
|||||||
D7DF58322DFCF18D00E9AD28 /* SendPaymentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DF58312DFCF18800E9AD28 /* SendPaymentView.swift */; };
|
D7DF58322DFCF18D00E9AD28 /* SendPaymentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DF58312DFCF18800E9AD28 /* SendPaymentView.swift */; };
|
||||||
D7DF58332DFCF18D00E9AD28 /* SendPaymentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DF58312DFCF18800E9AD28 /* SendPaymentView.swift */; };
|
D7DF58332DFCF18D00E9AD28 /* SendPaymentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DF58312DFCF18800E9AD28 /* SendPaymentView.swift */; };
|
||||||
D7DF58342DFCF18D00E9AD28 /* SendPaymentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DF58312DFCF18800E9AD28 /* SendPaymentView.swift */; };
|
D7DF58342DFCF18D00E9AD28 /* SendPaymentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DF58312DFCF18800E9AD28 /* SendPaymentView.swift */; };
|
||||||
|
D7E5B2D32EA0188200CF47AC /* StreamPipelineDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E5B2D22EA0187B00CF47AC /* StreamPipelineDiagnostics.swift */; };
|
||||||
|
D7E5B2D42EA0188200CF47AC /* StreamPipelineDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E5B2D22EA0187B00CF47AC /* StreamPipelineDiagnostics.swift */; };
|
||||||
|
D7E5B2D52EA0188200CF47AC /* StreamPipelineDiagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7E5B2D22EA0187B00CF47AC /* StreamPipelineDiagnostics.swift */; };
|
||||||
D7EB00B02CD59C8D00660C07 /* PresentFullScreenItemNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */; };
|
D7EB00B02CD59C8D00660C07 /* PresentFullScreenItemNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */; };
|
||||||
D7EB00B12CD59C8D00660C07 /* PresentFullScreenItemNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */; };
|
D7EB00B12CD59C8D00660C07 /* PresentFullScreenItemNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */; };
|
||||||
|
D7EBF8BB2E59022A004EAE29 /* NostrNetworkManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EBF8BA2E5901F7004EAE29 /* NostrNetworkManagerTests.swift */; };
|
||||||
|
D7EBF8BE2E59470D004EAE29 /* test_notes.jsonl in Resources */ = {isa = PBXBuildFile; fileRef = D7EBF8BD2E594708004EAE29 /* test_notes.jsonl */; };
|
||||||
|
D7EBF8C02E5D39DC004EAE29 /* ThreadModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EBF8BF2E5D39D1004EAE29 /* ThreadModelTests.swift */; };
|
||||||
D7EDED152B11776B0018B19C /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; };
|
D7EDED152B11776B0018B19C /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; };
|
||||||
D7EDED162B1177840018B19C /* LNUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883B5297730E400DC99E7 /* LNUrls.swift */; };
|
D7EDED162B1177840018B19C /* LNUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883B5297730E400DC99E7 /* LNUrls.swift */; };
|
||||||
D7EDED172B1177960018B19C /* TranslationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95C9298DF87B00F3D526 /* TranslationService.swift */; };
|
D7EDED172B1177960018B19C /* TranslationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95C9298DF87B00F3D526 /* TranslationService.swift */; };
|
||||||
@@ -2646,6 +2661,9 @@
|
|||||||
D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventViewTests.swift; sourceTree = "<group>"; };
|
D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventViewTests.swift; sourceTree = "<group>"; };
|
||||||
D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDamusState.swift; sourceTree = "<group>"; };
|
D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDamusState.swift; sourceTree = "<group>"; };
|
||||||
D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProfiles.swift; sourceTree = "<group>"; };
|
D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProfiles.swift; sourceTree = "<group>"; };
|
||||||
|
D72B6FA12E7DFB3F0050CD1D /* ProfilesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilesManager.swift; sourceTree = "<group>"; };
|
||||||
|
D72B6FA52E7E06A40050CD1D /* ProfileObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileObserver.swift; sourceTree = "<group>"; };
|
||||||
|
D72C01302E78C0FB00AACB67 /* CondensedProfilePicturesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CondensedProfilePicturesViewModel.swift; sourceTree = "<group>"; };
|
||||||
D72E12772BEED22400F4F781 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; };
|
D72E12772BEED22400F4F781 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; };
|
||||||
D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrFilterTests.swift; sourceTree = "<group>"; };
|
D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrFilterTests.swift; sourceTree = "<group>"; };
|
||||||
D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = "<group>"; };
|
D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = "<group>"; };
|
||||||
@@ -2730,7 +2748,11 @@
|
|||||||
D7DB93092D69485A00DA1EE5 /* NIP65.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP65.swift; sourceTree = "<group>"; };
|
D7DB93092D69485A00DA1EE5 /* NIP65.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP65.swift; sourceTree = "<group>"; };
|
||||||
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = "<group>"; };
|
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = "<group>"; };
|
||||||
D7DF58312DFCF18800E9AD28 /* SendPaymentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendPaymentView.swift; sourceTree = "<group>"; };
|
D7DF58312DFCF18800E9AD28 /* SendPaymentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendPaymentView.swift; sourceTree = "<group>"; };
|
||||||
|
D7E5B2D22EA0187B00CF47AC /* StreamPipelineDiagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamPipelineDiagnostics.swift; sourceTree = "<group>"; };
|
||||||
D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentFullScreenItemNotify.swift; sourceTree = "<group>"; };
|
D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentFullScreenItemNotify.swift; sourceTree = "<group>"; };
|
||||||
|
D7EBF8BA2E5901F7004EAE29 /* NostrNetworkManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrNetworkManagerTests.swift; sourceTree = "<group>"; };
|
||||||
|
D7EBF8BD2E594708004EAE29 /* test_notes.jsonl */ = {isa = PBXFileReference; lastKnownFileType = text; path = test_notes.jsonl; sourceTree = "<group>"; };
|
||||||
|
D7EBF8BF2E5D39D1004EAE29 /* ThreadModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadModelTests.swift; sourceTree = "<group>"; };
|
||||||
D7EDED1B2B1178FE0018B19C /* NoteContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContent.swift; sourceTree = "<group>"; };
|
D7EDED1B2B1178FE0018B19C /* NoteContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContent.swift; sourceTree = "<group>"; };
|
||||||
D7EDED1D2B11797D0018B19C /* LongformEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongformEvent.swift; sourceTree = "<group>"; };
|
D7EDED1D2B11797D0018B19C /* LongformEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongformEvent.swift; sourceTree = "<group>"; };
|
||||||
D7EDED202B117DCA0018B19C /* SequenceUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SequenceUtils.swift; sourceTree = "<group>"; };
|
D7EDED202B117DCA0018B19C /* SequenceUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SequenceUtils.swift; sourceTree = "<group>"; };
|
||||||
@@ -3133,6 +3155,7 @@
|
|||||||
4C75EFAB28049CC80006080F /* Nostr */ = {
|
4C75EFAB28049CC80006080F /* Nostr */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D72B6FA52E7E06A40050CD1D /* ProfileObserver.swift */,
|
||||||
4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */,
|
4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */,
|
||||||
50A60D132A28BEEE00186190 /* RelayLog.swift */,
|
50A60D132A28BEEE00186190 /* RelayLog.swift */,
|
||||||
4C75EFA527FF87A20006080F /* Nostr.swift */,
|
4C75EFA527FF87A20006080F /* Nostr.swift */,
|
||||||
@@ -3714,6 +3737,7 @@
|
|||||||
4CE6DEF627F7A08200C66700 /* damusTests */ = {
|
4CE6DEF627F7A08200C66700 /* damusTests */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D7EBF8BC2E5946F9004EAE29 /* NostrNetworkManagerTests */,
|
||||||
D7DB1FED2D5AC50F00CF06DA /* NIP44v2EncryptionTests.swift */,
|
D7DB1FED2D5AC50F00CF06DA /* NIP44v2EncryptionTests.swift */,
|
||||||
D7A0D8742D1FE66A00DCBE59 /* EditPictureControlTests.swift */,
|
D7A0D8742D1FE66A00DCBE59 /* EditPictureControlTests.swift */,
|
||||||
E06336A72B7582D600A88E6B /* Assets */,
|
E06336A72B7582D600A88E6B /* Assets */,
|
||||||
@@ -4340,6 +4364,7 @@
|
|||||||
5C78A7922E3036F800CF177D /* Models */ = {
|
5C78A7922E3036F800CF177D /* Models */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D72C01302E78C0FB00AACB67 /* CondensedProfilePicturesViewModel.swift */,
|
||||||
4C3BEFD12819DB9B00B3DE84 /* ProfileModel.swift */,
|
4C3BEFD12819DB9B00B3DE84 /* ProfileModel.swift */,
|
||||||
4C363A912825FCF2006E126D /* ProfileUpdate.swift */,
|
4C363A912825FCF2006E126D /* ProfileUpdate.swift */,
|
||||||
);
|
);
|
||||||
@@ -4708,6 +4733,7 @@
|
|||||||
5C78A7B82E3047DE00CF177D /* Utilities */ = {
|
5C78A7B82E3047DE00CF177D /* Utilities */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D7E5B2D22EA0187B00CF47AC /* StreamPipelineDiagnostics.swift */,
|
||||||
D77135D22E7B766300E7639F /* DataExtensions.swift */,
|
D77135D22E7B766300E7639F /* DataExtensions.swift */,
|
||||||
4CF0ABEA29844B2F00D66079 /* AnyCodable */,
|
4CF0ABEA29844B2F00D66079 /* AnyCodable */,
|
||||||
D73B74E02D8365B40067BDBC /* ExtraFonts.swift */,
|
D73B74E02D8365B40067BDBC /* ExtraFonts.swift */,
|
||||||
@@ -4982,6 +5008,7 @@
|
|||||||
D73BDB122D71212600D69970 /* NostrNetworkManager */ = {
|
D73BDB122D71212600D69970 /* NostrNetworkManager */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
D72B6FA12E7DFB3F0050CD1D /* ProfilesManager.swift */,
|
||||||
D733F9E02D92C1AA00317B11 /* SubscriptionManager.swift */,
|
D733F9E02D92C1AA00317B11 /* SubscriptionManager.swift */,
|
||||||
D73BDB172D71310C00D69970 /* UserRelayListErrors.swift */,
|
D73BDB172D71310C00D69970 /* UserRelayListErrors.swift */,
|
||||||
D73BDB132D71215F00D69970 /* UserRelayListManager.swift */,
|
D73BDB132D71215F00D69970 /* UserRelayListManager.swift */,
|
||||||
@@ -5084,6 +5111,16 @@
|
|||||||
path = NIP65;
|
path = NIP65;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D7EBF8BC2E5946F9004EAE29 /* NostrNetworkManagerTests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D7EBF8BD2E594708004EAE29 /* test_notes.jsonl */,
|
||||||
|
D7EBF8BA2E5901F7004EAE29 /* NostrNetworkManagerTests.swift */,
|
||||||
|
D7EBF8BF2E5D39D1004EAE29 /* ThreadModelTests.swift */,
|
||||||
|
);
|
||||||
|
path = NostrNetworkManagerTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
E06336A72B7582D600A88E6B /* Assets */ = {
|
E06336A72B7582D600A88E6B /* Assets */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -5407,6 +5444,7 @@
|
|||||||
files = (
|
files = (
|
||||||
E06336AB2B75850100A88E6B /* img_with_location.jpeg in Resources */,
|
E06336AB2B75850100A88E6B /* img_with_location.jpeg in Resources */,
|
||||||
D7DB1FF12D5AC5D700CF06DA /* nip44.vectors.json in Resources */,
|
D7DB1FF12D5AC5D700CF06DA /* nip44.vectors.json in Resources */,
|
||||||
|
D7EBF8BE2E59470D004EAE29 /* test_notes.jsonl in Resources */,
|
||||||
4C0C039A2A61E27B0098B3B8 /* bool_setting.wasm in Resources */,
|
4C0C039A2A61E27B0098B3B8 /* bool_setting.wasm in Resources */,
|
||||||
D7DB1FF32D5AC5EA00CF06DA /* LICENSES in Resources */,
|
D7DB1FF32D5AC5EA00CF06DA /* LICENSES in Resources */,
|
||||||
4C0C03992A61E27B0098B3B8 /* primal.wasm in Resources */,
|
4C0C03992A61E27B0098B3B8 /* primal.wasm in Resources */,
|
||||||
@@ -5781,6 +5819,7 @@
|
|||||||
4C64305C2A945AFF00B0C0E9 /* MusicController.swift in Sources */,
|
4C64305C2A945AFF00B0C0E9 /* MusicController.swift in Sources */,
|
||||||
5053ACA72A56DF3B00851AE3 /* DeveloperSettingsView.swift in Sources */,
|
5053ACA72A56DF3B00851AE3 /* DeveloperSettingsView.swift in Sources */,
|
||||||
F79C7FAD29D5E9620000F946 /* EditPictureControl.swift in Sources */,
|
F79C7FAD29D5E9620000F946 /* EditPictureControl.swift in Sources */,
|
||||||
|
D72B6FA62E7E06AD0050CD1D /* ProfileObserver.swift in Sources */,
|
||||||
4C011B5F2BD0A56A002F2F9B /* ChatroomThreadView.swift in Sources */,
|
4C011B5F2BD0A56A002F2F9B /* ChatroomThreadView.swift in Sources */,
|
||||||
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
|
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
|
||||||
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */,
|
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */,
|
||||||
@@ -5833,6 +5872,7 @@
|
|||||||
4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */,
|
4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */,
|
||||||
5C0567582C8FBC560073F23A /* NDBSearchView.swift in Sources */,
|
5C0567582C8FBC560073F23A /* NDBSearchView.swift in Sources */,
|
||||||
D72341192B6864F200E1E135 /* DamusPurpleEnvironment.swift in Sources */,
|
D72341192B6864F200E1E135 /* DamusPurpleEnvironment.swift in Sources */,
|
||||||
|
D72B6FA32E7DFB450050CD1D /* ProfilesManager.swift in Sources */,
|
||||||
4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */,
|
4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */,
|
||||||
D71528002E0A3D6900C893D6 /* InterestList.swift in Sources */,
|
D71528002E0A3D6900C893D6 /* InterestList.swift in Sources */,
|
||||||
4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */,
|
4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */,
|
||||||
@@ -5863,10 +5903,12 @@
|
|||||||
D706C5AF2D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */,
|
D706C5AF2D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */,
|
||||||
6439E014296790CF0020672B /* ProfilePicImageView.swift in Sources */,
|
6439E014296790CF0020672B /* ProfilePicImageView.swift in Sources */,
|
||||||
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */,
|
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */,
|
||||||
|
D72C01312E78C10500AACB67 /* CondensedProfilePicturesViewModel.swift in Sources */,
|
||||||
4C1253682A76D2470004F4B8 /* MuteNotify.swift in Sources */,
|
4C1253682A76D2470004F4B8 /* MuteNotify.swift in Sources */,
|
||||||
4CDA128C29EB19C40006FA5A /* LocalNotification.swift in Sources */,
|
4CDA128C29EB19C40006FA5A /* LocalNotification.swift in Sources */,
|
||||||
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */,
|
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */,
|
||||||
4C7D09762A0AF19E00943473 /* FillAndStroke.swift in Sources */,
|
4C7D09762A0AF19E00943473 /* FillAndStroke.swift in Sources */,
|
||||||
|
D7E5B2D42EA0188200CF47AC /* StreamPipelineDiagnostics.swift in Sources */,
|
||||||
4CA927612A290E340098A105 /* EventShell.swift in Sources */,
|
4CA927612A290E340098A105 /* EventShell.swift in Sources */,
|
||||||
D74EC8502E1856B70091DC51 /* NonCopyableLinkedList.swift in Sources */,
|
D74EC8502E1856B70091DC51 /* NonCopyableLinkedList.swift in Sources */,
|
||||||
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */,
|
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */,
|
||||||
@@ -6023,11 +6065,13 @@
|
|||||||
4C9B0DEE2A65A75F00CBDA21 /* AttrStringTestExtensions.swift in Sources */,
|
4C9B0DEE2A65A75F00CBDA21 /* AttrStringTestExtensions.swift in Sources */,
|
||||||
4C19AE552A5D977400C90DB7 /* HashtagTests.swift in Sources */,
|
4C19AE552A5D977400C90DB7 /* HashtagTests.swift in Sources */,
|
||||||
D72927AD2BAB515C00F93E90 /* RelayURLTests.swift in Sources */,
|
D72927AD2BAB515C00F93E90 /* RelayURLTests.swift in Sources */,
|
||||||
|
D7EBF8C02E5D39DC004EAE29 /* ThreadModelTests.swift in Sources */,
|
||||||
4C0ED07F2D7A1E260020D8A2 /* Benchmarking.swift in Sources */,
|
4C0ED07F2D7A1E260020D8A2 /* Benchmarking.swift in Sources */,
|
||||||
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
|
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
|
||||||
D71DC1EC2A9129C3006E207C /* PostViewTests.swift in Sources */,
|
D71DC1EC2A9129C3006E207C /* PostViewTests.swift in Sources */,
|
||||||
3AAC7A022A60FE72002B50DF /* LocalizationUtilTests.swift in Sources */,
|
3AAC7A022A60FE72002B50DF /* LocalizationUtilTests.swift in Sources */,
|
||||||
D7CBD1D62B8D509800BFD889 /* DamusPurpleImpendingExpirationTests.swift in Sources */,
|
D7CBD1D62B8D509800BFD889 /* DamusPurpleImpendingExpirationTests.swift in Sources */,
|
||||||
|
D7EBF8BB2E59022A004EAE29 /* NostrNetworkManagerTests.swift in Sources */,
|
||||||
D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */,
|
D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */,
|
||||||
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */,
|
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */,
|
||||||
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */,
|
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */,
|
||||||
@@ -6137,6 +6181,7 @@
|
|||||||
82D6FAC12CD99F7900C925F4 /* AsciiCharacter.swift in Sources */,
|
82D6FAC12CD99F7900C925F4 /* AsciiCharacter.swift in Sources */,
|
||||||
82D6FAC22CD99F7900C925F4 /* NdbTagElem.swift in Sources */,
|
82D6FAC22CD99F7900C925F4 /* NdbTagElem.swift in Sources */,
|
||||||
82D6FAC32CD99F7900C925F4 /* Ndb.swift in Sources */,
|
82D6FAC32CD99F7900C925F4 /* Ndb.swift in Sources */,
|
||||||
|
D72B6FA92E7E06AD0050CD1D /* ProfileObserver.swift in Sources */,
|
||||||
82D6FAC42CD99F7900C925F4 /* NdbTagsIterator.swift in Sources */,
|
82D6FAC42CD99F7900C925F4 /* NdbTagsIterator.swift in Sources */,
|
||||||
82D6FAC52CD99F7900C925F4 /* NdbTxn.swift in Sources */,
|
82D6FAC52CD99F7900C925F4 /* NdbTxn.swift in Sources */,
|
||||||
82D6FAC72CD99F7900C925F4 /* midl.c in Sources */,
|
82D6FAC72CD99F7900C925F4 /* midl.c in Sources */,
|
||||||
@@ -6198,6 +6243,7 @@
|
|||||||
82D6FB052CD99F7900C925F4 /* MusicController.swift in Sources */,
|
82D6FB052CD99F7900C925F4 /* MusicController.swift in Sources */,
|
||||||
82D6FB062CD99F7900C925F4 /* UserStatusView.swift in Sources */,
|
82D6FB062CD99F7900C925F4 /* UserStatusView.swift in Sources */,
|
||||||
82D6FB072CD99F7900C925F4 /* UserStatus.swift in Sources */,
|
82D6FB072CD99F7900C925F4 /* UserStatus.swift in Sources */,
|
||||||
|
D72B6FA22E7DFB450050CD1D /* ProfilesManager.swift in Sources */,
|
||||||
5CB017262D42C5C400A9ED05 /* TransactionsView.swift in Sources */,
|
5CB017262D42C5C400A9ED05 /* TransactionsView.swift in Sources */,
|
||||||
82D6FB082CD99F7900C925F4 /* UserStatusSheet.swift in Sources */,
|
82D6FB082CD99F7900C925F4 /* UserStatusSheet.swift in Sources */,
|
||||||
82D6FB092CD99F7900C925F4 /* SearchHeaderView.swift in Sources */,
|
82D6FB092CD99F7900C925F4 /* SearchHeaderView.swift in Sources */,
|
||||||
@@ -6476,6 +6522,7 @@
|
|||||||
82D6FC082CD99F7900C925F4 /* ProfileZapLinkView.swift in Sources */,
|
82D6FC082CD99F7900C925F4 /* ProfileZapLinkView.swift in Sources */,
|
||||||
D71AD8FE2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */,
|
D71AD8FE2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */,
|
||||||
82D6FC092CD99F7900C925F4 /* AboutView.swift in Sources */,
|
82D6FC092CD99F7900C925F4 /* AboutView.swift in Sources */,
|
||||||
|
D72C01332E78C10500AACB67 /* CondensedProfilePicturesViewModel.swift in Sources */,
|
||||||
82D6FC0A2CD99F7900C925F4 /* ProfileName.swift in Sources */,
|
82D6FC0A2CD99F7900C925F4 /* ProfileName.swift in Sources */,
|
||||||
5CB645982EA317D20018BD91 /* DamusLabs.swift in Sources */,
|
5CB645982EA317D20018BD91 /* DamusLabs.swift in Sources */,
|
||||||
82D6FC0B2CD99F7900C925F4 /* ProfilePictureSelector.swift in Sources */,
|
82D6FC0B2CD99F7900C925F4 /* ProfilePictureSelector.swift in Sources */,
|
||||||
@@ -6484,6 +6531,7 @@
|
|||||||
82D6FC0E2CD99F7900C925F4 /* ProfilePicView.swift in Sources */,
|
82D6FC0E2CD99F7900C925F4 /* ProfilePicView.swift in Sources */,
|
||||||
82D6FC0F2CD99F7900C925F4 /* ProfileView.swift in Sources */,
|
82D6FC0F2CD99F7900C925F4 /* ProfileView.swift in Sources */,
|
||||||
82D6FC102CD99F7900C925F4 /* ProfileNameView.swift in Sources */,
|
82D6FC102CD99F7900C925F4 /* ProfileNameView.swift in Sources */,
|
||||||
|
D7E5B2D52EA0188200CF47AC /* StreamPipelineDiagnostics.swift in Sources */,
|
||||||
5CB017212D2D985E00A9ED05 /* CoinosButton.swift in Sources */,
|
5CB017212D2D985E00A9ED05 /* CoinosButton.swift in Sources */,
|
||||||
82D6FC112CD99F7900C925F4 /* MaybeAnonPfpView.swift in Sources */,
|
82D6FC112CD99F7900C925F4 /* MaybeAnonPfpView.swift in Sources */,
|
||||||
82D6FC122CD99F7900C925F4 /* EventProfileName.swift in Sources */,
|
82D6FC122CD99F7900C925F4 /* EventProfileName.swift in Sources */,
|
||||||
@@ -6632,6 +6680,7 @@
|
|||||||
D73E5E242C6A97F4007EB227 /* FollowedNotify.swift in Sources */,
|
D73E5E242C6A97F4007EB227 /* FollowedNotify.swift in Sources */,
|
||||||
D73E5E252C6A97F4007EB227 /* FollowNotify.swift in Sources */,
|
D73E5E252C6A97F4007EB227 /* FollowNotify.swift in Sources */,
|
||||||
D73E5E262C6A97F4007EB227 /* LikedNotify.swift in Sources */,
|
D73E5E262C6A97F4007EB227 /* LikedNotify.swift in Sources */,
|
||||||
|
D72B6FA42E7DFB450050CD1D /* ProfilesManager.swift in Sources */,
|
||||||
D73E5E272C6A97F4007EB227 /* LocalNotificationNotify.swift in Sources */,
|
D73E5E272C6A97F4007EB227 /* LocalNotificationNotify.swift in Sources */,
|
||||||
D73E5F8B2C6AA6A2007EB227 /* UserStatusSheet.swift in Sources */,
|
D73E5F8B2C6AA6A2007EB227 /* UserStatusSheet.swift in Sources */,
|
||||||
D73E5E282C6A97F4007EB227 /* LoginNotify.swift in Sources */,
|
D73E5E282C6A97F4007EB227 /* LoginNotify.swift in Sources */,
|
||||||
@@ -6747,6 +6796,7 @@
|
|||||||
D73E5E8A2C6A97F4007EB227 /* PurpleStoreKitManager.swift in Sources */,
|
D73E5E8A2C6A97F4007EB227 /* PurpleStoreKitManager.swift in Sources */,
|
||||||
D733F9E72D92C76100317B11 /* UnownedNdbNote.swift in Sources */,
|
D733F9E72D92C76100317B11 /* UnownedNdbNote.swift in Sources */,
|
||||||
D73E5E8E2C6A97F4007EB227 /* ImageResizer.swift in Sources */,
|
D73E5E8E2C6A97F4007EB227 /* ImageResizer.swift in Sources */,
|
||||||
|
D72B6FA72E7E06AD0050CD1D /* ProfileObserver.swift in Sources */,
|
||||||
D78F080E2D7F78EF00FC6C75 /* Request.swift in Sources */,
|
D78F080E2D7F78EF00FC6C75 /* Request.swift in Sources */,
|
||||||
D73E5E8F2C6A97F4007EB227 /* PhotoCaptureProcessor.swift in Sources */,
|
D73E5E8F2C6A97F4007EB227 /* PhotoCaptureProcessor.swift in Sources */,
|
||||||
D773BC602C6D538500349F0A /* CommentItem.swift in Sources */,
|
D773BC602C6D538500349F0A /* CommentItem.swift in Sources */,
|
||||||
@@ -7042,6 +7092,7 @@
|
|||||||
D703D7502C6709F500A400EA /* NdbTxn.swift in Sources */,
|
D703D7502C6709F500A400EA /* NdbTxn.swift in Sources */,
|
||||||
D703D77E2C670C1100A400EA /* NostrKind.swift in Sources */,
|
D703D77E2C670C1100A400EA /* NostrKind.swift in Sources */,
|
||||||
D73E5F972C6AA7B7007EB227 /* SuggestedHashtagsView.swift in Sources */,
|
D73E5F972C6AA7B7007EB227 /* SuggestedHashtagsView.swift in Sources */,
|
||||||
|
D72C01322E78C10500AACB67 /* CondensedProfilePicturesViewModel.swift in Sources */,
|
||||||
D703D7B22C6710AF00A400EA /* ContentParsing.swift in Sources */,
|
D703D7B22C6710AF00A400EA /* ContentParsing.swift in Sources */,
|
||||||
D703D7522C670A1400A400EA /* Log.swift in Sources */,
|
D703D7522C670A1400A400EA /* Log.swift in Sources */,
|
||||||
D73E5E1B2C6A9672007EB227 /* LikeCounter.swift in Sources */,
|
D73E5E1B2C6A9672007EB227 /* LikeCounter.swift in Sources */,
|
||||||
@@ -7097,6 +7148,7 @@
|
|||||||
D703D7752C670BBF00A400EA /* Constants.swift in Sources */,
|
D703D7752C670BBF00A400EA /* Constants.swift in Sources */,
|
||||||
D73E5E172C6A962A007EB227 /* ImageUploadModel.swift in Sources */,
|
D73E5E172C6A962A007EB227 /* ImageUploadModel.swift in Sources */,
|
||||||
D703D76A2C670B2C00A400EA /* Bech32Object.swift in Sources */,
|
D703D76A2C670B2C00A400EA /* Bech32Object.swift in Sources */,
|
||||||
|
D7E5B2D32EA0188200CF47AC /* StreamPipelineDiagnostics.swift in Sources */,
|
||||||
D73E5E162C6A9619007EB227 /* PostView.swift in Sources */,
|
D73E5E162C6A9619007EB227 /* PostView.swift in Sources */,
|
||||||
D703D7872C670C7E00A400EA /* DamusPurpleEnvironment.swift in Sources */,
|
D703D7872C670C7E00A400EA /* DamusPurpleEnvironment.swift in Sources */,
|
||||||
D703D7892C670C8600A400EA /* DeepLPlan.swift in Sources */,
|
D703D7892C670C8600A400EA /* DeepLPlan.swift in Sources */,
|
||||||
|
|||||||
@@ -55,7 +55,6 @@
|
|||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
enableAddressSanitizer = "YES"
|
|
||||||
launchStyle = "0"
|
launchStyle = "0"
|
||||||
useCustomWorkingDirectory = "NO"
|
useCustomWorkingDirectory = "NO"
|
||||||
ignoresPersistentStateOnLaunch = "NO"
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
|||||||
@@ -25,6 +25,23 @@ enum AppAccessibilityIdentifiers: String {
|
|||||||
case sign_in_confirm_button
|
case sign_in_confirm_button
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: Sign Up / Create Account
|
||||||
|
// Prefix: `sign_up`
|
||||||
|
|
||||||
|
/// Button to navigate to create account view
|
||||||
|
case sign_up_option_button
|
||||||
|
/// Text field for entering name during account creation
|
||||||
|
case sign_up_name_field
|
||||||
|
/// Text field for entering bio during account creation
|
||||||
|
case sign_up_bio_field
|
||||||
|
/// Button to proceed to the next step after entering profile info
|
||||||
|
case sign_up_next_button
|
||||||
|
/// Button to save keys after account creation
|
||||||
|
case sign_up_save_keys_button
|
||||||
|
/// Button to skip saving keys
|
||||||
|
case sign_up_skip_save_keys_button
|
||||||
|
|
||||||
|
|
||||||
// MARK: Onboarding
|
// MARK: Onboarding
|
||||||
// Prefix: `onboarding`
|
// Prefix: `onboarding`
|
||||||
|
|
||||||
@@ -60,6 +77,12 @@ enum AppAccessibilityIdentifiers: String {
|
|||||||
/// The profile option in the side menu
|
/// The profile option in the side menu
|
||||||
case side_menu_profile_button
|
case side_menu_profile_button
|
||||||
|
|
||||||
|
/// The logout button in the side menu
|
||||||
|
case side_menu_logout_button
|
||||||
|
|
||||||
|
/// The logout confirmation button in the alert dialog
|
||||||
|
case side_menu_logout_confirm_button
|
||||||
|
|
||||||
|
|
||||||
// MARK: Items specific to the user's own profile
|
// MARK: Items specific to the user's own profile
|
||||||
// Prefix: `own_profile`
|
// Prefix: `own_profile`
|
||||||
|
|||||||
+165
-285
@@ -135,6 +135,7 @@ struct ContentView: View {
|
|||||||
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
|
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
|
||||||
@AppStorage("has_seen_suggested_users") private var hasSeenOnboardingSuggestions = false
|
@AppStorage("has_seen_suggested_users") private var hasSeenOnboardingSuggestions = false
|
||||||
let sub_id = UUID().description
|
let sub_id = UUID().description
|
||||||
|
@State var damusClosingTask: Task<Void, Never>? = nil
|
||||||
|
|
||||||
// connect retry timer
|
// connect retry timer
|
||||||
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||||
@@ -173,7 +174,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case .home:
|
case .home:
|
||||||
PostingTimelineView(damus_state: damus_state!, home: home, isSideBarOpened: $isSideBarOpened, active_sheet: $active_sheet, headerOffset: $headerOffset)
|
PostingTimelineView(damus_state: damus_state!, home: home, homeEvents: home.events, isSideBarOpened: $isSideBarOpened, active_sheet: $active_sheet, headerOffset: $headerOffset)
|
||||||
|
|
||||||
case .notifications:
|
case .notifications:
|
||||||
NotificationsView(state: damus, notifications: home.notifications, subtitle: $menu_subtitle)
|
NotificationsView(state: damus, notifications: home.notifications, subtitle: $menu_subtitle)
|
||||||
@@ -302,18 +303,20 @@ struct ContentView: View {
|
|||||||
.ignoresSafeArea(.keyboard)
|
.ignoresSafeArea(.keyboard)
|
||||||
.edgesIgnoringSafeArea(hide_bar ? [.bottom] : [])
|
.edgesIgnoringSafeArea(hide_bar ? [.bottom] : [])
|
||||||
.onAppear() {
|
.onAppear() {
|
||||||
self.connect()
|
Task {
|
||||||
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers)
|
await self.connect()
|
||||||
setup_notifications()
|
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers)
|
||||||
if !hasSeenOnboardingSuggestions || damus_state!.settings.always_show_onboarding_suggestions {
|
setup_notifications()
|
||||||
if damus_state.is_privkey_user {
|
if !hasSeenOnboardingSuggestions || damus_state!.settings.always_show_onboarding_suggestions {
|
||||||
active_sheet = .onboardingSuggestions
|
if damus_state.is_privkey_user {
|
||||||
hasSeenOnboardingSuggestions = true
|
active_sheet = .onboardingSuggestions
|
||||||
|
hasSeenOnboardingSuggestions = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.appDelegate?.state = damus_state
|
||||||
|
Task { // We probably don't need this to be a detached task. According to https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/#Defining-and-Calling-Asynchronous-Functions, awaits are only suspension points that do not block the thread.
|
||||||
|
await self.listenAndHandleLocalNotifications()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
self.appDelegate?.state = damus_state
|
|
||||||
Task { // We probably don't need this to be a detached task. According to https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/#Defining-and-Calling-Asynchronous-Functions, awaits are only suspension points that do not block the thread.
|
|
||||||
await self.listenAndHandleLocalNotifications()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(item: $active_sheet) { item in
|
.sheet(item: $active_sheet) { item in
|
||||||
@@ -375,7 +378,7 @@ struct ContentView: View {
|
|||||||
self.hide_bar = !show
|
self.hide_bar = !show
|
||||||
}
|
}
|
||||||
.onReceive(timer) { n in
|
.onReceive(timer) { n in
|
||||||
self.damus_state?.nostrNetwork.postbox.try_flushing_events()
|
Task{ await self.damus_state?.nostrNetwork.postbox.try_flushing_events() }
|
||||||
self.damus_state!.profiles.profile_data(self.damus_state!.pubkey).status.try_expire()
|
self.damus_state!.profiles.profile_data(self.damus_state!.pubkey).status.try_expire()
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.report)) { target in
|
.onReceive(handle_notify(.report)) { target in
|
||||||
@@ -386,43 +389,47 @@ struct ContentView: View {
|
|||||||
self.confirm_mute = true
|
self.confirm_mute = true
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.attached_wallet)) { nwc in
|
.onReceive(handle_notify(.attached_wallet)) { nwc in
|
||||||
// update the lightning address on our profile when we attach a
|
Task {
|
||||||
// wallet with an associated
|
try? await damus_state.nostrNetwork.userRelayList.load() // Reload relay list to apply changes
|
||||||
guard let ds = self.damus_state,
|
|
||||||
let lud16 = nwc.lud16,
|
// update the lightning address on our profile when we attach a
|
||||||
let keypair = ds.keypair.to_full(),
|
// wallet with an associated
|
||||||
let profile_txn = ds.profiles.lookup(id: ds.pubkey),
|
guard let ds = self.damus_state,
|
||||||
let profile = profile_txn.unsafeUnownedValue,
|
let lud16 = nwc.lud16,
|
||||||
lud16 != profile.lud16 else {
|
let keypair = ds.keypair.to_full(),
|
||||||
return
|
let profile_txn = ds.profiles.lookup(id: ds.pubkey),
|
||||||
|
let profile = profile_txn.unsafeUnownedValue,
|
||||||
|
lud16 != profile.lud16 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear zapper cache for old lud16
|
||||||
|
if profile.lud16 != nil {
|
||||||
|
// TODO: should this be somewhere else, where we process profile events!?
|
||||||
|
invalidate_zapper_cache(pubkey: keypair.pubkey, profiles: ds.profiles, lnurl: ds.lnurls)
|
||||||
|
}
|
||||||
|
|
||||||
|
let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: lud16, nip05: profile.nip05, damus_donation: profile.damus_donation, reactions: profile.reactions)
|
||||||
|
|
||||||
|
guard let ev = make_metadata_event(keypair: keypair, metadata: prof) else { return }
|
||||||
|
await ds.nostrNetwork.postbox.send(ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear zapper cache for old lud16
|
|
||||||
if profile.lud16 != nil {
|
|
||||||
// TODO: should this be somewhere else, where we process profile events!?
|
|
||||||
invalidate_zapper_cache(pubkey: keypair.pubkey, profiles: ds.profiles, lnurl: ds.lnurls)
|
|
||||||
}
|
|
||||||
|
|
||||||
let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: lud16, nip05: profile.nip05, damus_donation: profile.damus_donation, reactions: profile.reactions)
|
|
||||||
|
|
||||||
guard let ev = make_metadata_event(keypair: keypair, metadata: prof) else { return }
|
|
||||||
ds.nostrNetwork.postbox.send(ev)
|
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.broadcast)) { ev in
|
.onReceive(handle_notify(.broadcast)) { ev in
|
||||||
guard let ds = self.damus_state else { return }
|
guard let ds = self.damus_state else { return }
|
||||||
|
|
||||||
ds.nostrNetwork.postbox.send(ev)
|
Task { await ds.nostrNetwork.postbox.send(ev) }
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.unfollow)) { target in
|
.onReceive(handle_notify(.unfollow)) { target in
|
||||||
guard let state = self.damus_state else { return }
|
guard let state = self.damus_state else { return }
|
||||||
_ = handle_unfollow(state: state, unfollow: target.follow_ref)
|
Task { _ = await handle_unfollow(state: state, unfollow: target.follow_ref) }
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.unfollowed)) { unfollow in
|
.onReceive(handle_notify(.unfollowed)) { unfollow in
|
||||||
home.resubscribe(.unfollowing(unfollow))
|
home.resubscribe(.unfollowing(unfollow))
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.follow)) { target in
|
.onReceive(handle_notify(.follow)) { target in
|
||||||
guard let state = self.damus_state else { return }
|
guard let state = self.damus_state else { return }
|
||||||
handle_follow_notif(state: state, target: target)
|
Task { await handle_follow_notif(state: state, target: target) }
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.followed)) { _ in
|
.onReceive(handle_notify(.followed)) { _ in
|
||||||
home.resubscribe(.following)
|
home.resubscribe(.following)
|
||||||
@@ -433,8 +440,10 @@ struct ContentView: View {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !handle_post_notification(keypair: keypair, postbox: state.nostrNetwork.postbox, events: state.events, post: post) {
|
Task {
|
||||||
self.active_sheet = nil
|
if await !handle_post_notification(keypair: keypair, postbox: state.nostrNetwork.postbox, events: state.events, post: post) {
|
||||||
|
self.active_sheet = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.new_mutes)) { _ in
|
.onReceive(handle_notify(.new_mutes)) { _ in
|
||||||
@@ -453,7 +462,7 @@ struct ContentView: View {
|
|||||||
self.active_full_screen_item = item
|
self.active_full_screen_item = item
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.favoriteUpdated)) { _ in
|
.onReceive(handle_notify(.favoriteUpdated)) { _ in
|
||||||
home.refresh_home_filters()
|
home.resubscribe(.following)
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.zapping)) { zap_ev in
|
.onReceive(handle_notify(.zapping)) { zap_ev in
|
||||||
guard !zap_ev.is_custom else {
|
guard !zap_ev.is_custom else {
|
||||||
@@ -480,35 +489,36 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.disconnect_relays)) { () in
|
.onReceive(handle_notify(.disconnect_relays)) { () in
|
||||||
damus_state.nostrNetwork.pool.disconnect()
|
Task { await damus_state.nostrNetwork.disconnectRelays() }
|
||||||
}
|
}
|
||||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { obj in
|
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { obj in
|
||||||
print("txn: 📙 DAMUS ACTIVE NOTIFY")
|
print("txn: 📙 DAMUS ACTIVE NOTIFY")
|
||||||
if damus_state.ndb.reopen() {
|
Task {
|
||||||
print("txn: NOSTRDB REOPENED")
|
await damusClosingTask?.value // Wait for the closing task to finish before reopening things, to avoid race conditions
|
||||||
} else {
|
if damus_state.ndb.reopen() {
|
||||||
print("txn: NOSTRDB FAILED TO REOPEN closed:\(damus_state.ndb.is_closed)")
|
print("txn: NOSTRDB REOPENED")
|
||||||
}
|
} else {
|
||||||
if damus_state.purple.checkout_ids_in_progress.count > 0 {
|
print("txn: NOSTRDB FAILED TO REOPEN closed:\(damus_state.ndb.is_closed)")
|
||||||
// For extra assurance, run this after one second, to avoid race conditions if the app is also handling a damus purple welcome url.
|
}
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
if damus_state.purple.checkout_ids_in_progress.count > 0 {
|
||||||
Task {
|
// For extra assurance, run this after one second, to avoid race conditions if the app is also handling a damus purple welcome url.
|
||||||
let freshly_completed_checkout_ids = try? await damus_state.purple.check_status_of_checkouts_in_progress()
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||||
let there_is_a_completed_checkout: Bool = (freshly_completed_checkout_ids?.count ?? 0) > 0
|
Task {
|
||||||
let account_info = try await damus_state.purple.fetch_account(pubkey: self.keypair.pubkey)
|
let freshly_completed_checkout_ids = try? await damus_state.purple.check_status_of_checkouts_in_progress()
|
||||||
if there_is_a_completed_checkout == true && account_info?.active == true {
|
let there_is_a_completed_checkout: Bool = (freshly_completed_checkout_ids?.count ?? 0) > 0
|
||||||
if damus_state.purple.onboarding_status.user_has_never_seen_the_onboarding_before() {
|
let account_info = try await damus_state.purple.fetch_account(pubkey: self.keypair.pubkey)
|
||||||
// Show welcome sheet
|
if there_is_a_completed_checkout == true && account_info?.active == true {
|
||||||
self.active_sheet = .purple_onboarding
|
if damus_state.purple.onboarding_status.user_has_never_seen_the_onboarding_before() {
|
||||||
}
|
// Show welcome sheet
|
||||||
else {
|
self.active_sheet = .purple_onboarding
|
||||||
self.active_sheet = .purple(DamusPurpleURL.init(is_staging: damus_state.purple.environment == .staging, variant: .landing))
|
}
|
||||||
|
else {
|
||||||
|
self.active_sheet = .purple(DamusPurpleURL.init(is_staging: damus_state.purple.environment == .staging, variant: .landing))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Task {
|
|
||||||
await damus_state.purple.check_and_send_app_notifications_if_needed(handler: home.handle_damus_app_notification)
|
await damus_state.purple.check_and_send_app_notifications_if_needed(handler: home.handle_damus_app_notification)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -517,8 +527,21 @@ struct ContentView: View {
|
|||||||
switch phase {
|
switch phase {
|
||||||
case .background:
|
case .background:
|
||||||
print("txn: 📙 DAMUS BACKGROUNDED")
|
print("txn: 📙 DAMUS BACKGROUNDED")
|
||||||
Task { @MainActor in
|
let bgTask = this_app.beginBackgroundTask(withName: "Closing things down gracefully", expirationHandler: { [weak damus_state] in
|
||||||
|
Log.error("App background signal handling: RUNNING OUT OF TIME! JUST CLOSE NDB DIRECTLY!", for: .app_lifecycle)
|
||||||
|
// Background time about to expire, so close ndb directly.
|
||||||
|
// This may still cause a memory error crash if subscription tasks have not been properly closed yet, but that is less likely than a 0xdead10cc crash if we don't do anything here.
|
||||||
|
damus_state?.ndb.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
damusClosingTask = Task { @MainActor in
|
||||||
|
Log.debug("App background signal handling: App being backgrounded", for: .app_lifecycle)
|
||||||
|
let startTime = CFAbsoluteTimeGetCurrent()
|
||||||
|
await damus_state.nostrNetwork.handleAppBackgroundRequest() // Close ndb streaming tasks before closing ndb to avoid memory errors
|
||||||
|
Log.debug("App background signal handling: Nostr network closed after %.2f seconds", for: .app_lifecycle, CFAbsoluteTimeGetCurrent() - startTime)
|
||||||
damus_state.ndb.close()
|
damus_state.ndb.close()
|
||||||
|
Log.debug("App background signal handling: Ndb closed after %.2f seconds", for: .app_lifecycle, CFAbsoluteTimeGetCurrent() - startTime)
|
||||||
|
this_app.endBackgroundTask(bgTask)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case .inactive:
|
case .inactive:
|
||||||
@@ -526,26 +549,34 @@ struct ContentView: View {
|
|||||||
break
|
break
|
||||||
case .active:
|
case .active:
|
||||||
print("txn: 📙 DAMUS ACTIVE")
|
print("txn: 📙 DAMUS ACTIVE")
|
||||||
damus_state.nostrNetwork.pool.ping()
|
Task {
|
||||||
|
await damusClosingTask?.value // Wait for the closing task to finish before reopening things, to avoid race conditions
|
||||||
|
damusClosingTask = nil
|
||||||
|
damus_state.ndb.reopen()
|
||||||
|
// Pinging the network will automatically reconnect any dead websocket connections
|
||||||
|
await damus_state.nostrNetwork.ping()
|
||||||
|
}
|
||||||
@unknown default:
|
@unknown default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.onlyzaps_mode)) { hide in
|
.onReceive(handle_notify(.onlyzaps_mode)) { hide in
|
||||||
home.filter_events()
|
Task {
|
||||||
|
home.filter_events()
|
||||||
guard let ds = damus_state,
|
|
||||||
let profile_txn = ds.profiles.lookup(id: ds.pubkey),
|
guard let ds = damus_state,
|
||||||
let profile = profile_txn.unsafeUnownedValue,
|
let profile_txn = ds.profiles.lookup(id: ds.pubkey),
|
||||||
let keypair = ds.keypair.to_full()
|
let profile = profile_txn.unsafeUnownedValue,
|
||||||
else {
|
let keypair = ds.keypair.to_full()
|
||||||
return
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: profile.lud16, nip05: profile.nip05, damus_donation: profile.damus_donation, reactions: !hide)
|
||||||
|
|
||||||
|
guard let profile_ev = make_metadata_event(keypair: keypair, metadata: prof) else { return }
|
||||||
|
await ds.nostrNetwork.postbox.send(profile_ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: profile.lud16, nip05: profile.nip05, damus_donation: profile.damus_donation, reactions: !hide)
|
|
||||||
|
|
||||||
guard let profile_ev = make_metadata_event(keypair: keypair, metadata: prof) else { return }
|
|
||||||
ds.nostrNetwork.postbox.send(profile_ev)
|
|
||||||
}
|
}
|
||||||
.alert(NSLocalizedString("User muted", comment: "Alert message to indicate the user has been muted"), isPresented: $user_muted_confirm, actions: {
|
.alert(NSLocalizedString("User muted", comment: "Alert message to indicate the user has been muted"), isPresented: $user_muted_confirm, actions: {
|
||||||
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to muted a user was successful.")) {
|
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to muted a user was successful.")) {
|
||||||
@@ -568,20 +599,22 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Button(NSLocalizedString("Yes, Overwrite", comment: "Text of button that confirms to overwrite the existing mutelist.")) {
|
Button(NSLocalizedString("Yes, Overwrite", comment: "Text of button that confirms to overwrite the existing mutelist.")) {
|
||||||
guard let ds = damus_state,
|
Task {
|
||||||
let keypair = ds.keypair.to_full(),
|
guard let ds = damus_state,
|
||||||
let muting,
|
let keypair = ds.keypair.to_full(),
|
||||||
let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: muting)
|
let muting,
|
||||||
else {
|
let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: muting)
|
||||||
return
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ds.mutelist_manager.set_mutelist(mutelist)
|
||||||
|
await ds.nostrNetwork.postbox.send(mutelist)
|
||||||
|
|
||||||
|
confirm_overwrite_mutelist = false
|
||||||
|
confirm_mute = false
|
||||||
|
user_muted_confirm = true
|
||||||
}
|
}
|
||||||
|
|
||||||
ds.mutelist_manager.set_mutelist(mutelist)
|
|
||||||
ds.nostrNetwork.postbox.send(mutelist)
|
|
||||||
|
|
||||||
confirm_overwrite_mutelist = false
|
|
||||||
confirm_mute = false
|
|
||||||
user_muted_confirm = true
|
|
||||||
}
|
}
|
||||||
}, message: {
|
}, message: {
|
||||||
Text("No mute list found, create a new one? This will overwrite any previous mute lists.", comment: "Alert message prompt that asks if the user wants to create a new mute list, overwriting previous mute lists.")
|
Text("No mute list found, create a new one? This will overwrite any previous mute lists.", comment: "Alert message prompt that asks if the user wants to create a new mute list, overwriting previous mute lists.")
|
||||||
@@ -609,7 +642,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ds.mutelist_manager.set_mutelist(ev)
|
ds.mutelist_manager.set_mutelist(ev)
|
||||||
ds.nostrNetwork.postbox.send(ev)
|
Task { await ds.nostrNetwork.postbox.send(ev) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, message: {
|
}, message: {
|
||||||
@@ -661,7 +694,7 @@ struct ContentView: View {
|
|||||||
self.execute_open_action(openAction)
|
self.execute_open_action(openAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
func connect() {
|
func connect() async {
|
||||||
// nostrdb
|
// nostrdb
|
||||||
var mndb = Ndb()
|
var mndb = Ndb()
|
||||||
if mndb == nil {
|
if mndb == nil {
|
||||||
@@ -683,7 +716,7 @@ struct ContentView: View {
|
|||||||
|
|
||||||
let settings = UserSettingsStore.globally_load_for(pubkey: pubkey)
|
let settings = UserSettingsStore.globally_load_for(pubkey: pubkey)
|
||||||
|
|
||||||
let new_relay_filters = load_relay_filters(pubkey) == nil
|
let new_relay_filters = await load_relay_filters(pubkey) == nil
|
||||||
|
|
||||||
self.damus_state = DamusState(keypair: keypair,
|
self.damus_state = DamusState(keypair: keypair,
|
||||||
likes: EventCounter(our_pubkey: pubkey),
|
likes: EventCounter(our_pubkey: pubkey),
|
||||||
@@ -726,8 +759,7 @@ struct ContentView: View {
|
|||||||
// Purple API is an experimental feature. If not enabled, do not connect `StoreObserver` with Purple API to avoid leaking receipts
|
// Purple API is an experimental feature. If not enabled, do not connect `StoreObserver` with Purple API to avoid leaking receipts
|
||||||
}
|
}
|
||||||
|
|
||||||
damus_state.nostrNetwork.pool.register_handler(sub_id: sub_id, handler: home.handle_event)
|
|
||||||
damus_state.nostrNetwork.connect()
|
|
||||||
|
|
||||||
if #available(iOS 17, *) {
|
if #available(iOS 17, *) {
|
||||||
if damus_state.settings.developer_mode && damus_state.settings.reset_tips_on_launch {
|
if damus_state.settings.developer_mode && damus_state.settings.reset_tips_on_launch {
|
||||||
@@ -743,29 +775,36 @@ struct ContentView: View {
|
|||||||
Log.error("Failed to configure tips: %s", for: .tips, error.localizedDescription)
|
Log.error("Failed to configure tips: %s", for: .tips, error.localizedDescription)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await damus_state.nostrNetwork.connect()
|
||||||
|
// TODO: Move this to a better spot. Not sure what is the best signal to listen to for sending initial filters
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: {
|
||||||
|
self.home.send_initial_filters()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func music_changed(_ state: MusicState) {
|
func music_changed(_ state: MusicState) {
|
||||||
guard let damus_state else { return }
|
Task {
|
||||||
switch state {
|
guard let damus_state else { return }
|
||||||
case .playback_state:
|
switch state {
|
||||||
break
|
case .playback_state:
|
||||||
case .song(let song):
|
break
|
||||||
guard let song, let kp = damus_state.keypair.to_full() else { return }
|
case .song(let song):
|
||||||
|
guard let song, let kp = damus_state.keypair.to_full() else { return }
|
||||||
let pdata = damus_state.profiles.profile_data(damus_state.pubkey)
|
|
||||||
|
let pdata = damus_state.profiles.profile_data(damus_state.pubkey)
|
||||||
let desc = "\(song.title ?? "Unknown") - \(song.artist ?? "Unknown")"
|
|
||||||
let encodedDesc = desc.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
|
let desc = "\(song.title ?? "Unknown") - \(song.artist ?? "Unknown")"
|
||||||
let url = encodedDesc.flatMap { enc in
|
let encodedDesc = desc.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
|
||||||
URL(string: "spotify:search:\(enc)")
|
let url = encodedDesc.flatMap { enc in
|
||||||
|
URL(string: "spotify:search:\(enc)")
|
||||||
|
}
|
||||||
|
let music = UserStatus(type: .music, expires_at: Date.now.addingTimeInterval(song.playbackDuration), content: desc, created_at: UInt32(Date.now.timeIntervalSince1970), url: url)
|
||||||
|
|
||||||
|
pdata.status.music = music
|
||||||
|
|
||||||
|
guard let ev = music.to_note(keypair: kp) else { return }
|
||||||
|
await damus_state.nostrNetwork.postbox.send(ev)
|
||||||
}
|
}
|
||||||
let music = UserStatus(type: .music, expires_at: Date.now.addingTimeInterval(song.playbackDuration), content: desc, created_at: UInt32(Date.now.timeIntervalSince1970), url: url)
|
|
||||||
|
|
||||||
pdata.status.music = music
|
|
||||||
|
|
||||||
guard let ev = music.to_note(keypair: kp) else { return }
|
|
||||||
damus_state.nostrNetwork.postbox.send(ev)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -815,7 +854,7 @@ struct TopbarSideMenuButton: View {
|
|||||||
Button {
|
Button {
|
||||||
isSideBarOpened.toggle()
|
isSideBarOpened.toggle()
|
||||||
} label: {
|
} label: {
|
||||||
ProfilePicView(pubkey: damus_state.pubkey, size: 32, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
ProfilePicView(pubkey: damus_state.pubkey, size: 32, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation, damusState: damus_state)
|
||||||
.opacity(isSideBarOpened ? 0 : 1)
|
.opacity(isSideBarOpened ? 0 : 1)
|
||||||
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
||||||
.accessibilityHidden(true) // Knowing there is a profile picture here leads to no actionable outcome to VoiceOver users, so it is best not to show it
|
.accessibilityHidden(true) // Knowing there is a profile picture here leads to no actionable outcome to VoiceOver users, so it is best not to show it
|
||||||
@@ -917,7 +956,7 @@ func update_filters_with_since(last_of_kind: [UInt32: NostrEvent], filters: [Nos
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func setup_notifications() {
|
func setup_notifications() {
|
||||||
this_app.registerForRemoteNotifications()
|
this_app.registerForRemoteNotifications()
|
||||||
let center = UNUserNotificationCenter.current()
|
let center = UNUserNotificationCenter.current()
|
||||||
@@ -952,169 +991,11 @@ enum FindEventType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum FoundEvent {
|
enum FoundEvent {
|
||||||
|
// TODO: Why not return the profile record itself? Right now the code probably just wants to trigger ndb to ingest the profile record and be available at ndb in parallel, but it would be cleaner if the function that uses this simply does that ndb query on their behalf.
|
||||||
case profile(Pubkey)
|
case profile(Pubkey)
|
||||||
case event(NostrEvent)
|
case event(NostrEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds an event from NostrDB if it exists, or from the network
|
|
||||||
///
|
|
||||||
/// This is the callback version. There is also an asyc/await version of this function.
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - state: Damus state
|
|
||||||
/// - query_: The query, including the event being looked for, and the relays to use when looking
|
|
||||||
/// - callback: The function to call with results
|
|
||||||
func find_event(state: DamusState, query query_: FindEvent, callback: @escaping (FoundEvent?) -> ()) {
|
|
||||||
return find_event_with_subid(state: state, query: query_, subid: UUID().description, callback: callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Finds an event from NostrDB if it exists, or from the network
|
|
||||||
///
|
|
||||||
/// This is a the async/await version of `find_event`. Use this when using callbacks is impossible or cumbersome.
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - state: Damus state
|
|
||||||
/// - query_: The query, including the event being looked for, and the relays to use when looking
|
|
||||||
/// - callback: The function to call with results
|
|
||||||
func find_event(state: DamusState, query query_: FindEvent) async -> FoundEvent? {
|
|
||||||
await withCheckedContinuation { continuation in
|
|
||||||
find_event(state: state, query: query_) { event in
|
|
||||||
var already_resumed = false
|
|
||||||
if !already_resumed { // Ensure we do not resume twice, as it causes a crash
|
|
||||||
continuation.resume(returning: event)
|
|
||||||
already_resumed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: String, callback: @escaping (FoundEvent?) -> ()) {
|
|
||||||
|
|
||||||
var filter: NostrFilter? = nil
|
|
||||||
let find_from = query_.find_from
|
|
||||||
let query = query_.type
|
|
||||||
|
|
||||||
switch query {
|
|
||||||
case .profile(let pubkey):
|
|
||||||
if let profile_txn = state.ndb.lookup_profile(pubkey),
|
|
||||||
let record = profile_txn.unsafeUnownedValue,
|
|
||||||
record.profile != nil
|
|
||||||
{
|
|
||||||
callback(.profile(pubkey))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
filter = NostrFilter(kinds: [.metadata], limit: 1, authors: [pubkey])
|
|
||||||
|
|
||||||
case .event(let evid):
|
|
||||||
if let ev = state.events.lookup(evid) {
|
|
||||||
callback(.event(ev))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
filter = NostrFilter(ids: [evid], limit: 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
var attempts: Int = 0
|
|
||||||
var has_event = false
|
|
||||||
guard let filter else { return }
|
|
||||||
|
|
||||||
state.nostrNetwork.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in
|
|
||||||
guard case .nostr_event(let ev) = res else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard ev.subid == subid else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ev {
|
|
||||||
case .ok:
|
|
||||||
break
|
|
||||||
case .event(_, let ev):
|
|
||||||
has_event = true
|
|
||||||
state.nostrNetwork.pool.unsubscribe(sub_id: subid)
|
|
||||||
|
|
||||||
switch query {
|
|
||||||
case .profile:
|
|
||||||
if ev.known_kind == .metadata {
|
|
||||||
callback(.profile(ev.pubkey))
|
|
||||||
}
|
|
||||||
case .event:
|
|
||||||
callback(.event(ev))
|
|
||||||
}
|
|
||||||
case .eose:
|
|
||||||
if !has_event {
|
|
||||||
attempts += 1
|
|
||||||
if attempts >= state.nostrNetwork.pool.our_descriptors.count {
|
|
||||||
callback(nil) // If we could not find any events in any of the relays we are connected to, send back nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id]) // We are only finding an event once, so close subscription on eose
|
|
||||||
case .notice:
|
|
||||||
break
|
|
||||||
case .auth:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/// Finds a replaceable event based on an `naddr` address.
|
|
||||||
///
|
|
||||||
/// This is the callback version of the function. There is another function that makes use of async/await
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - damus_state: The Damus state
|
|
||||||
/// - naddr: the `naddr` address
|
|
||||||
/// - callback: A function to handle the found event
|
|
||||||
func naddrLookup(damus_state: DamusState, naddr: NAddr, callback: @escaping (NostrEvent?) -> ()) {
|
|
||||||
let nostrKinds: [NostrKind]? = NostrKind(rawValue: naddr.kind).map { [$0] }
|
|
||||||
|
|
||||||
let filter = NostrFilter(kinds: nostrKinds, authors: [naddr.author])
|
|
||||||
|
|
||||||
let subid = UUID().description
|
|
||||||
|
|
||||||
damus_state.nostrNetwork.pool.subscribe_to(sub_id: subid, filters: [filter], to: nil) { relay_id, res in
|
|
||||||
guard case .nostr_event(let ev) = res else {
|
|
||||||
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if case .event(_, let ev) = ev {
|
|
||||||
for tag in ev.tags {
|
|
||||||
if(tag.count >= 2 && tag[0].string() == "d"){
|
|
||||||
if (tag[1].string() == naddr.identifier){
|
|
||||||
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
|
||||||
callback(ev)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Finds a replaceable event based on an `naddr` address.
|
|
||||||
///
|
|
||||||
/// This is the async/await version of the function. Another version of this function which makes use of callback functions also exists .
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - damus_state: The Damus state
|
|
||||||
/// - naddr: the `naddr` address
|
|
||||||
/// - callback: A function to handle the found event
|
|
||||||
func naddrLookup(damus_state: DamusState, naddr: NAddr) async -> NostrEvent? {
|
|
||||||
await withCheckedContinuation { continuation in
|
|
||||||
var already_resumed = false
|
|
||||||
naddrLookup(damus_state: damus_state, naddr: naddr) { event in
|
|
||||||
if !already_resumed { // Ensure we do not resume twice, as it causes a crash
|
|
||||||
continuation.resume(returning: event)
|
|
||||||
already_resumed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func timeline_name(_ timeline: Timeline?) -> String {
|
func timeline_name(_ timeline: Timeline?) -> String {
|
||||||
guard let timeline else {
|
guard let timeline else {
|
||||||
return ""
|
return ""
|
||||||
@@ -1132,14 +1013,14 @@ func timeline_name(_ timeline: Timeline?) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func handle_unfollow(state: DamusState, unfollow: FollowRef) -> Bool {
|
func handle_unfollow(state: DamusState, unfollow: FollowRef) async -> Bool {
|
||||||
guard let keypair = state.keypair.to_full() else {
|
guard let keypair = state.keypair.to_full() else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
let old_contacts = state.contacts.event
|
let old_contacts = state.contacts.event
|
||||||
|
|
||||||
guard let ev = unfollow_reference(postbox: state.nostrNetwork.postbox, our_contacts: old_contacts, keypair: keypair, unfollow: unfollow)
|
guard let ev = await unfollow_reference(postbox: state.nostrNetwork.postbox, our_contacts: old_contacts, keypair: keypair, unfollow: unfollow)
|
||||||
else {
|
else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -1160,12 +1041,12 @@ func handle_unfollow(state: DamusState, unfollow: FollowRef) -> Bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func handle_follow(state: DamusState, follow: FollowRef) -> Bool {
|
func handle_follow(state: DamusState, follow: FollowRef) async -> Bool {
|
||||||
guard let keypair = state.keypair.to_full() else {
|
guard let keypair = state.keypair.to_full() else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let ev = follow_reference(box: state.nostrNetwork.postbox, our_contacts: state.contacts.event, keypair: keypair, follow: follow)
|
guard let ev = await follow_reference(box: state.nostrNetwork.postbox, our_contacts: state.contacts.event, keypair: keypair, follow: follow)
|
||||||
else {
|
else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -1185,7 +1066,7 @@ func handle_follow(state: DamusState, follow: FollowRef) -> Bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func handle_follow_notif(state: DamusState, target: FollowTarget) -> Bool {
|
func handle_follow_notif(state: DamusState, target: FollowTarget) async -> Bool {
|
||||||
switch target {
|
switch target {
|
||||||
case .pubkey(let pk):
|
case .pubkey(let pk):
|
||||||
state.contacts.add_friend_pubkey(pk)
|
state.contacts.add_friend_pubkey(pk)
|
||||||
@@ -1193,10 +1074,10 @@ func handle_follow_notif(state: DamusState, target: FollowTarget) -> Bool {
|
|||||||
state.contacts.add_friend_contact(ev)
|
state.contacts.add_friend_contact(ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
return handle_follow(state: state, follow: target.follow_ref)
|
return await handle_follow(state: state, follow: target.follow_ref)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: EventCache, post: NostrPostResult) -> Bool {
|
func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: EventCache, post: NostrPostResult) async -> Bool {
|
||||||
switch post {
|
switch post {
|
||||||
case .post(let post):
|
case .post(let post):
|
||||||
//let post = tup.0
|
//let post = tup.0
|
||||||
@@ -1205,17 +1086,17 @@ func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: Ev
|
|||||||
guard let new_ev = post.to_event(keypair: keypair) else {
|
guard let new_ev = post.to_event(keypair: keypair) else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
postbox.send(new_ev)
|
await postbox.send(new_ev)
|
||||||
for eref in new_ev.referenced_ids.prefix(3) {
|
for eref in new_ev.referenced_ids.prefix(3) {
|
||||||
// also broadcast at most 3 referenced events
|
// also broadcast at most 3 referenced events
|
||||||
if let ev = events.lookup(eref) {
|
if let ev = events.lookup(eref) {
|
||||||
postbox.send(ev)
|
await postbox.send(ev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for qref in new_ev.referenced_quote_ids.prefix(3) {
|
for qref in new_ev.referenced_quote_ids.prefix(3) {
|
||||||
// also broadcast at most 3 referenced quoted events
|
// also broadcast at most 3 referenced quoted events
|
||||||
if let ev = events.lookup(qref.note_id) {
|
if let ev = events.lookup(qref.note_id) {
|
||||||
postbox.send(ev)
|
await postbox.send(ev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
@@ -1269,4 +1150,3 @@ func logout(_ state: DamusState?)
|
|||||||
state?.close()
|
state?.close()
|
||||||
notify(.logout)
|
notify(.logout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ extension NIP65 {
|
|||||||
self.relays = Self.relayOrderedDictionary(from: relays)
|
self.relays = Self.relayOrderedDictionary(from: relays)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.relays = Self.relayOrderedDictionary(from: [])
|
||||||
|
}
|
||||||
|
|
||||||
init(relays: [RelayURL]) {
|
init(relays: [RelayURL]) {
|
||||||
let relayItemList = relays.map({ RelayItem(url: $0, rwConfiguration: .readWrite) })
|
let relayItemList = relays.map({ RelayItem(url: $0, rwConfiguration: .readWrite) })
|
||||||
self.relays = Self.relayOrderedDictionary(from: relayItemList)
|
self.relays = Self.relayOrderedDictionary(from: relayItemList)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class NostrNetworkManager {
|
|||||||
/// ## Implementation notes
|
/// ## Implementation notes
|
||||||
///
|
///
|
||||||
/// - This will be marked `private` in the future to prevent other code from accessing the relay pool directly. Code outside this layer should use a higher level interface
|
/// - This will be marked `private` in the future to prevent other code from accessing the relay pool directly. Code outside this layer should use a higher level interface
|
||||||
let pool: RelayPool // TODO: Make this private and make higher level interface for classes outside the NostrNetworkManager
|
private let pool: RelayPool // TODO: Make this private and make higher level interface for classes outside the NostrNetworkManager
|
||||||
/// A delegate that allows us to interact with the rest of app without introducing hard or circular dependencies
|
/// A delegate that allows us to interact with the rest of app without introducing hard or circular dependencies
|
||||||
private var delegate: Delegate
|
private var delegate: Delegate
|
||||||
/// Manages the user's relay list, controls RelayPool's connected relays
|
/// Manages the user's relay list, controls RelayPool's connected relays
|
||||||
@@ -33,34 +33,135 @@ class NostrNetworkManager {
|
|||||||
let postbox: PostBox
|
let postbox: PostBox
|
||||||
/// Handles subscriptions and functions to read or consume data from the Nostr network
|
/// Handles subscriptions and functions to read or consume data from the Nostr network
|
||||||
let reader: SubscriptionManager
|
let reader: SubscriptionManager
|
||||||
|
let profilesManager: ProfilesManager
|
||||||
|
|
||||||
init(delegate: Delegate) {
|
init(delegate: Delegate, addNdbToRelayPool: Bool = true) {
|
||||||
self.delegate = delegate
|
self.delegate = delegate
|
||||||
let pool = RelayPool(ndb: delegate.ndb, keypair: delegate.keypair)
|
let pool = RelayPool(ndb: addNdbToRelayPool ? delegate.ndb : nil, keypair: delegate.keypair)
|
||||||
self.pool = pool
|
self.pool = pool
|
||||||
let reader = SubscriptionManager(pool: pool, ndb: delegate.ndb)
|
let reader = SubscriptionManager(pool: pool, ndb: delegate.ndb, experimentalLocalRelayModelSupport: self.delegate.experimentalLocalRelayModelSupport)
|
||||||
let userRelayList = UserRelayListManager(delegate: delegate, pool: pool, reader: reader)
|
let userRelayList = UserRelayListManager(delegate: delegate, pool: pool, reader: reader)
|
||||||
self.reader = reader
|
self.reader = reader
|
||||||
self.userRelayList = userRelayList
|
self.userRelayList = userRelayList
|
||||||
self.postbox = PostBox(pool: pool)
|
self.postbox = PostBox(pool: pool)
|
||||||
|
self.profilesManager = ProfilesManager(subscriptionManager: reader, ndb: delegate.ndb)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Control functions
|
// MARK: - Control and lifecycle functions
|
||||||
|
|
||||||
/// Connects the app to the Nostr network
|
/// Connects the app to the Nostr network
|
||||||
func connect() {
|
func connect() async {
|
||||||
self.userRelayList.connect()
|
await self.userRelayList.connect() // Will load the user's list, apply it, and get RelayPool to connect to it.
|
||||||
|
await self.profilesManager.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func disconnectRelays() async {
|
||||||
|
await self.pool.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAppBackgroundRequest() async {
|
||||||
|
await self.reader.cancelAllTasks()
|
||||||
|
await self.pool.cleanQueuedRequestForSessionEnd()
|
||||||
|
}
|
||||||
|
|
||||||
|
func close() async {
|
||||||
|
await withTaskGroup { group in
|
||||||
|
// Spawn each cancellation task in parallel for faster execution speed
|
||||||
|
group.addTask {
|
||||||
|
await self.reader.cancelAllTasks()
|
||||||
|
}
|
||||||
|
group.addTask {
|
||||||
|
await self.profilesManager.stop()
|
||||||
|
}
|
||||||
|
// But await on each one to prevent race conditions
|
||||||
|
for await value in group { continue }
|
||||||
|
await pool.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ping() async {
|
||||||
|
await self.pool.ping()
|
||||||
}
|
}
|
||||||
|
|
||||||
func relaysForEvent(event: NostrEvent) -> [RelayURL] {
|
@MainActor
|
||||||
|
func relaysForEvent(event: NostrEvent) async -> [RelayURL] {
|
||||||
// TODO(tyiu) Ideally this list would be sorted by the event author's outbox relay preferences
|
// TODO(tyiu) Ideally this list would be sorted by the event author's outbox relay preferences
|
||||||
// and reliability of relays to maximize chances of others finding this event.
|
// and reliability of relays to maximize chances of others finding this event.
|
||||||
if let relays = pool.seen[event.id] {
|
if let relays = await pool.seen[event.id] {
|
||||||
return Array(relays)
|
return Array(relays)
|
||||||
}
|
}
|
||||||
|
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: ORGANIZE THESE
|
||||||
|
|
||||||
|
// MARK: - Communication with the Nostr Network
|
||||||
|
/// ## Implementation notes
|
||||||
|
///
|
||||||
|
/// - This class hides the relay pool on purpose to avoid other code from dealing with complex relay + nostrDB logic.
|
||||||
|
/// - Instead, we provide an easy to use interface so that normal code can just get the info they want.
|
||||||
|
/// - This is also to help us migrate to the relay model.
|
||||||
|
// TODO: Define a better interface. This is a temporary scaffold to replace direct relay pool access. After that is done, we can refactor this interface to be cleaner and reduce non-sense.
|
||||||
|
|
||||||
|
func sendToNostrDB(event: NostrEvent) async {
|
||||||
|
await self.pool.send_raw_to_local_ndb(.typical(.event(event)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func send(event: NostrEvent, to targetRelays: [RelayURL]? = nil, skipEphemeralRelays: Bool = true) async {
|
||||||
|
await self.pool.send(.event(event), to: targetRelays, skip_ephemeral: skipEphemeralRelays)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func getRelay(_ id: RelayURL) -> RelayPool.Relay? {
|
||||||
|
pool.get_relay(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
var connectedRelays: [RelayPool.Relay] {
|
||||||
|
self.pool.relays
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
var ourRelayDescriptors: [RelayPool.RelayDescriptor] {
|
||||||
|
self.pool.our_descriptors
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func relayURLsThatSawNote(id: NoteId) async -> Set<RelayURL>? {
|
||||||
|
return await self.pool.seen[id]
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func determineToRelays(filters: RelayFilters) -> [RelayURL] {
|
||||||
|
return self.pool.our_descriptors
|
||||||
|
.map { $0.url }
|
||||||
|
.filter { !filters.is_filtered(timeline: .search, relay_id: $0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: NWC
|
||||||
|
// TODO: Move this to NWCManager
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func nwcPay(url: WalletConnectURL, post: PostBox, invoice: String, delay: TimeInterval? = 5.0, on_flush: OnFlush? = nil, zap_request: NostrEvent? = nil) async -> NostrEvent? {
|
||||||
|
await WalletConnect.pay(url: url, pool: self.pool, post: post, invoice: invoice, zap_request: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a donation zap to the Damus team
|
||||||
|
func send_donation_zap(nwc: WalletConnectURL, percent: Int, base_msats: Int64) async {
|
||||||
|
let percent_f = Double(percent) / 100.0
|
||||||
|
let donations_msats = Int64(percent_f * Double(base_msats))
|
||||||
|
|
||||||
|
let payreq = LNUrlPayRequest(allowsNostr: true, commentAllowed: nil, nostrPubkey: "", callback: "https://sendsats.lol/@damus")
|
||||||
|
guard let invoice = await fetch_zap_invoice(payreq, zapreq: nil, msats: donations_msats, zap_type: .non_zap, comment: nil) else {
|
||||||
|
// we failed... oh well. no donation for us.
|
||||||
|
print("damus-donation failed to fetch invoice")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
print("damus-donation donating...")
|
||||||
|
await WalletConnect.pay(url: nwc, pool: self.pool, post: self.postbox, invoice: invoice, zap_request: nil, delay: nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -93,6 +194,9 @@ extension NostrNetworkManager {
|
|||||||
/// Whether the app is in developer mode
|
/// Whether the app is in developer mode
|
||||||
var developerMode: Bool { get }
|
var developerMode: Bool { get }
|
||||||
|
|
||||||
|
/// Whether the app has the experimental local relay model flag that streams data only from the local relay (ndb)
|
||||||
|
var experimentalLocalRelayModelSupport: Bool { get }
|
||||||
|
|
||||||
/// The cache of relay model information
|
/// The cache of relay model information
|
||||||
var relayModelCache: RelayModelCache { get }
|
var relayModelCache: RelayModelCache { get }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
//
|
||||||
|
// ProfilesManager.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2025-09-19.
|
||||||
|
//
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension NostrNetworkManager {
|
||||||
|
/// Efficiently manages getting profile metadata from the network and NostrDB without too many relay subscriptions
|
||||||
|
///
|
||||||
|
/// This is necessary because relays have a limit on how many subscriptions can be sent to relays at one given time.
|
||||||
|
actor ProfilesManager {
|
||||||
|
private var profileListenerTask: Task<Void, any Error>? = nil
|
||||||
|
private var subscriptionSwitcherTask: Task<Void, any Error>? = nil
|
||||||
|
private var subscriptionNeedsUpdate: Bool = false
|
||||||
|
private let subscriptionManager: SubscriptionManager
|
||||||
|
private let ndb: Ndb
|
||||||
|
private var streams: [Pubkey: [UUID: ProfileStreamInfo]]
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Initialization and deinitialization
|
||||||
|
|
||||||
|
init(subscriptionManager: SubscriptionManager, ndb: Ndb) {
|
||||||
|
self.subscriptionManager = subscriptionManager
|
||||||
|
self.ndb = ndb
|
||||||
|
self.streams = [:]
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.subscriptionSwitcherTask?.cancel()
|
||||||
|
self.profileListenerTask?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Task management
|
||||||
|
|
||||||
|
func load() {
|
||||||
|
self.restartProfileListenerTask()
|
||||||
|
self.subscriptionSwitcherTask?.cancel()
|
||||||
|
self.subscriptionSwitcherTask = Task {
|
||||||
|
while true {
|
||||||
|
try await Task.sleep(for: .seconds(1))
|
||||||
|
try Task.checkCancellation()
|
||||||
|
if subscriptionNeedsUpdate {
|
||||||
|
try Task.checkCancellation()
|
||||||
|
self.restartProfileListenerTask()
|
||||||
|
subscriptionNeedsUpdate = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stop() async {
|
||||||
|
await withTaskGroup { group in
|
||||||
|
// Spawn each cancellation in parallel for better execution speed
|
||||||
|
group.addTask {
|
||||||
|
await self.subscriptionSwitcherTask?.cancel()
|
||||||
|
try? await self.subscriptionSwitcherTask?.value
|
||||||
|
}
|
||||||
|
group.addTask {
|
||||||
|
await self.profileListenerTask?.cancel()
|
||||||
|
try? await self.profileListenerTask?.value
|
||||||
|
}
|
||||||
|
// But await for all of them to be done before returning to avoid race conditions
|
||||||
|
for await value in group { continue }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func restartProfileListenerTask() {
|
||||||
|
self.profileListenerTask?.cancel()
|
||||||
|
self.profileListenerTask = Task {
|
||||||
|
try await self.listenToProfileChanges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Listening and publishing of profile changes
|
||||||
|
|
||||||
|
private func listenToProfileChanges() async throws {
|
||||||
|
let pubkeys = Array(streams.keys)
|
||||||
|
guard pubkeys.count > 0 else { return }
|
||||||
|
let profileFilter = NostrFilter(kinds: [.metadata], authors: pubkeys)
|
||||||
|
try Task.checkCancellation()
|
||||||
|
for await ndbLender in self.subscriptionManager.streamIndefinitely(filters: [profileFilter], streamMode: .ndbFirst(optimizeNetworkFilter: false)) {
|
||||||
|
try Task.checkCancellation()
|
||||||
|
try? ndbLender.borrow { ev in
|
||||||
|
publishProfileUpdates(metadataEvent: ev)
|
||||||
|
}
|
||||||
|
try Task.checkCancellation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func publishProfileUpdates(metadataEvent: borrowing UnownedNdbNote) {
|
||||||
|
let now = UInt64(Date.now.timeIntervalSince1970)
|
||||||
|
ndb.write_profile_last_fetched(pubkey: metadataEvent.pubkey, fetched_at: now)
|
||||||
|
|
||||||
|
if let relevantStreams = streams[metadataEvent.pubkey] {
|
||||||
|
// If we have the user metadata event in ndb, then we should have the profile record as well.
|
||||||
|
guard let profile = ndb.lookup_profile(metadataEvent.pubkey) else { return }
|
||||||
|
for relevantStream in relevantStreams.values {
|
||||||
|
relevantStream.continuation.yield(profile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Streaming interface
|
||||||
|
|
||||||
|
func streamProfile(pubkey: Pubkey) -> AsyncStream<ProfileStreamItem> {
|
||||||
|
return AsyncStream<ProfileStreamItem> { continuation in
|
||||||
|
let stream = ProfileStreamInfo(continuation: continuation)
|
||||||
|
self.add(pubkey: pubkey, stream: stream)
|
||||||
|
|
||||||
|
continuation.onTermination = { @Sendable _ in
|
||||||
|
Task { await self.removeStream(pubkey: pubkey, id: stream.id) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Stream management
|
||||||
|
|
||||||
|
private func add(pubkey: Pubkey, stream: ProfileStreamInfo) {
|
||||||
|
if self.streams[pubkey] == nil {
|
||||||
|
self.streams[pubkey] = [:]
|
||||||
|
self.subscriptionNeedsUpdate = true
|
||||||
|
}
|
||||||
|
self.streams[pubkey]?[stream.id] = stream
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeStream(pubkey: Pubkey, id: UUID) {
|
||||||
|
self.streams[pubkey]?[id] = nil
|
||||||
|
if self.streams[pubkey]?.keys.count == 0 {
|
||||||
|
// We don't need to subscribe to this profile anymore
|
||||||
|
self.streams[pubkey] = nil
|
||||||
|
self.subscriptionNeedsUpdate = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Helper types
|
||||||
|
|
||||||
|
typealias ProfileStreamItem = NdbTxn<ProfileRecord?>
|
||||||
|
|
||||||
|
struct ProfileStreamInfo {
|
||||||
|
let id: UUID = UUID()
|
||||||
|
let continuation: AsyncStream<ProfileStreamItem>.Continuation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,9 @@
|
|||||||
//
|
//
|
||||||
// Created by Daniel D’Aquino on 2025-03-25.
|
// Created by Daniel D’Aquino on 2025-03-25.
|
||||||
//
|
//
|
||||||
|
import Foundation
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
extension NostrNetworkManager {
|
extension NostrNetworkManager {
|
||||||
/// Reads or fetches information from RelayPool and NostrDB, and provides an easier and unified higher-level interface.
|
/// Reads or fetches information from RelayPool and NostrDB, and provides an easier and unified higher-level interface.
|
||||||
@@ -14,48 +17,482 @@ extension NostrNetworkManager {
|
|||||||
class SubscriptionManager {
|
class SubscriptionManager {
|
||||||
private let pool: RelayPool
|
private let pool: RelayPool
|
||||||
private var ndb: Ndb
|
private var ndb: Ndb
|
||||||
|
private var taskManager: TaskManager
|
||||||
|
private let experimentalLocalRelayModelSupport: Bool
|
||||||
|
|
||||||
init(pool: RelayPool, ndb: Ndb) {
|
private static let logger = Logger(
|
||||||
|
subsystem: Constants.MAIN_APP_BUNDLE_IDENTIFIER,
|
||||||
|
category: "subscription_manager"
|
||||||
|
)
|
||||||
|
|
||||||
|
init(pool: RelayPool, ndb: Ndb, experimentalLocalRelayModelSupport: Bool) {
|
||||||
self.pool = pool
|
self.pool = pool
|
||||||
self.ndb = ndb
|
self.ndb = ndb
|
||||||
|
self.taskManager = TaskManager()
|
||||||
|
self.experimentalLocalRelayModelSupport = experimentalLocalRelayModelSupport
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Reading data from Nostr
|
// MARK: - Subscribing and Streaming data from Nostr
|
||||||
|
|
||||||
/// Subscribes to data from the user's relays
|
/// Streams notes until the EOSE signal
|
||||||
///
|
func streamExistingEvents(filters: [NostrFilter], to desiredRelays: [RelayURL]? = nil, timeout: Duration? = nil, streamMode: StreamMode? = nil, id: UUID? = nil) -> AsyncStream<NdbNoteLender> {
|
||||||
/// ## Implementation notes
|
let timeout = timeout ?? .seconds(10)
|
||||||
///
|
return AsyncStream<NdbNoteLender> { continuation in
|
||||||
/// - When we migrate to the local relay model, we should modify this function to stream directly from NostrDB
|
let streamingTask = Task {
|
||||||
///
|
outerLoop: for await item in self.advancedStream(filters: filters, to: desiredRelays, timeout: timeout, streamMode: streamMode, id: id) {
|
||||||
/// - Parameter filters: The nostr filters to specify what kind of data to subscribe to
|
try Task.checkCancellation()
|
||||||
/// - Returns: An async stream of nostr data
|
|
||||||
func subscribe(filters: [NostrFilter]) -> AsyncStream<StreamItem> {
|
|
||||||
return AsyncStream<StreamItem> { continuation in
|
|
||||||
let streamTask = Task {
|
|
||||||
for await item in self.pool.subscribe(filters: filters) {
|
|
||||||
switch item {
|
switch item {
|
||||||
case .eose: continuation.yield(.eose)
|
case .event(let lender):
|
||||||
case .event(let nostrEvent):
|
continuation.yield(lender)
|
||||||
// At this point of the pipeline, if the note is valid it should have been processed and verified by NostrDB,
|
case .eose:
|
||||||
// in which case we should pull the note from NostrDB to ensure validity.
|
break outerLoop
|
||||||
// However, NdbNotes are unowned, so we return a function where our callers can temporarily borrow the NostrDB note
|
case .ndbEose:
|
||||||
let noteId = nostrEvent.id
|
continue
|
||||||
let lender: NdbNoteLender = { lend in
|
case .networkEose:
|
||||||
guard let ndbNoteTxn = self.ndb.lookup_note(noteId) else {
|
continue
|
||||||
throw NdbNoteLenderError.errorLoadingNote
|
}
|
||||||
}
|
}
|
||||||
guard let unownedNote = UnownedNdbNote(ndbNoteTxn) else {
|
continuation.finish()
|
||||||
throw NdbNoteLenderError.errorLoadingNote
|
}
|
||||||
}
|
continuation.onTermination = { @Sendable _ in
|
||||||
lend(unownedNote)
|
streamingTask.cancel()
|
||||||
}
|
}
|
||||||
continuation.yield(.event(borrow: lender))
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribes to data from user's relays, for a maximum period of time — after which the stream will end.
|
||||||
|
///
|
||||||
|
/// This is useful when waiting for some specific data from Nostr, but not indefinitely.
|
||||||
|
func timedStream(filters: [NostrFilter], to desiredRelays: [RelayURL]? = nil, timeout: Duration, streamMode: StreamMode? = nil, id: UUID? = nil) -> AsyncStream<NdbNoteLender> {
|
||||||
|
return AsyncStream<NdbNoteLender> { continuation in
|
||||||
|
let streamingTask = Task {
|
||||||
|
for await item in self.advancedStream(filters: filters, to: desiredRelays, timeout: timeout, streamMode: streamMode, id: id) {
|
||||||
|
try Task.checkCancellation()
|
||||||
|
switch item {
|
||||||
|
case .event(lender: let lender):
|
||||||
|
continuation.yield(lender)
|
||||||
|
case .eose: break
|
||||||
|
case .ndbEose: break
|
||||||
|
case .networkEose: break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continuation.finish()
|
||||||
|
}
|
||||||
|
continuation.onTermination = { @Sendable _ in
|
||||||
|
streamingTask.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Subscribes to notes indefinitely
|
||||||
|
///
|
||||||
|
/// This is useful when simply streaming all events indefinitely
|
||||||
|
func streamIndefinitely(filters: [NostrFilter], to desiredRelays: [RelayURL]? = nil, streamMode: StreamMode? = nil, id: UUID? = nil) -> AsyncStream<NdbNoteLender> {
|
||||||
|
return AsyncStream<NdbNoteLender> { continuation in
|
||||||
|
let streamingTask = Task {
|
||||||
|
for await item in self.advancedStream(filters: filters, to: desiredRelays, streamMode: streamMode, id: id) {
|
||||||
|
try Task.checkCancellation()
|
||||||
|
switch item {
|
||||||
|
case .event(lender: let lender):
|
||||||
|
continuation.yield(lender)
|
||||||
|
case .eose:
|
||||||
|
break
|
||||||
|
case .ndbEose:
|
||||||
|
break
|
||||||
|
case .networkEose:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continuation.onTermination = { @Sendable _ in
|
continuation.onTermination = { @Sendable _ in
|
||||||
streamTask.cancel() // Close the RelayPool stream when caller stops streaming
|
streamingTask.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func advancedStream(filters: [NostrFilter], to desiredRelays: [RelayURL]? = nil, timeout: Duration? = nil, streamMode: StreamMode? = nil, id: UUID? = nil) -> AsyncStream<StreamItem> {
|
||||||
|
let id = id ?? UUID()
|
||||||
|
let streamMode = streamMode ?? defaultStreamMode()
|
||||||
|
return AsyncStream<StreamItem> { continuation in
|
||||||
|
let startTime = CFAbsoluteTimeGetCurrent()
|
||||||
|
Self.logger.debug("Session subscription \(id.uuidString, privacy: .public): Started")
|
||||||
|
var ndbEOSEIssued = false
|
||||||
|
var networkEOSEIssued = false
|
||||||
|
|
||||||
|
// This closure function issues (yields) an EOSE signal to the stream if all relevant conditions are met
|
||||||
|
let yieldEOSEIfReady = {
|
||||||
|
let connectedToNetwork = self.pool.network_monitor.currentPath.status == .satisfied
|
||||||
|
// In normal mode: Issuing EOSE requires EOSE from both NDB and the network, since they are all considered separate relays
|
||||||
|
// In experimental local relay model mode: Issuing EOSE requires only EOSE from NDB, since that is the only relay that "matters"
|
||||||
|
let canIssueEOSE = switch streamMode {
|
||||||
|
case .ndbFirst, .ndbOnly: (ndbEOSEIssued)
|
||||||
|
case .ndbAndNetworkParallel: (ndbEOSEIssued && (networkEOSEIssued || !connectedToNetwork))
|
||||||
|
}
|
||||||
|
|
||||||
|
if canIssueEOSE {
|
||||||
|
Self.logger.debug("Session subscription \(id.uuidString, privacy: .public): Issued EOSE for session. Elapsed: \(CFAbsoluteTimeGetCurrent() - startTime, format: .fixed(precision: 2), privacy: .public) seconds")
|
||||||
|
logStreamPipelineStats("SubscriptionManager_Advanced_Stream_\(id)", "Consumer_\(id)")
|
||||||
|
continuation.yield(.eose)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var networkStreamTask: Task<Void, any Error>? = nil
|
||||||
|
var latestNoteTimestampSeen: UInt32? = nil
|
||||||
|
|
||||||
|
let startNetworkStreamTask = {
|
||||||
|
guard streamMode.shouldStreamFromNetwork else { return }
|
||||||
|
networkStreamTask = Task {
|
||||||
|
while !Task.isCancelled {
|
||||||
|
let optimizedFilters = filters.map {
|
||||||
|
var optimizedFilter = $0
|
||||||
|
// Shift the since filter 2 minutes (120 seconds) before the last note timestamp
|
||||||
|
if let latestTimestamp = latestNoteTimestampSeen {
|
||||||
|
optimizedFilter.since = latestTimestamp > 120 ? latestTimestamp - 120 : 0
|
||||||
|
}
|
||||||
|
return optimizedFilter
|
||||||
|
}
|
||||||
|
for await item in self.multiSessionNetworkStream(filters: optimizedFilters, to: desiredRelays, streamMode: streamMode, id: id) {
|
||||||
|
try Task.checkCancellation()
|
||||||
|
logStreamPipelineStats("SubscriptionManager_Network_Stream_\(id)", "SubscriptionManager_Advanced_Stream_\(id)")
|
||||||
|
switch item {
|
||||||
|
case .event(let lender):
|
||||||
|
logStreamPipelineStats("SubscriptionManager_Advanced_Stream_\(id)", "Consumer_\(id)")
|
||||||
|
continuation.yield(item)
|
||||||
|
case .eose:
|
||||||
|
break // Should not happen
|
||||||
|
case .ndbEose:
|
||||||
|
break // Should not happen
|
||||||
|
case .networkEose:
|
||||||
|
logStreamPipelineStats("SubscriptionManager_Advanced_Stream_\(id)", "Consumer_\(id)")
|
||||||
|
continuation.yield(item)
|
||||||
|
networkEOSEIssued = true
|
||||||
|
yieldEOSEIfReady()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if streamMode.optimizeNetworkFilter == false && streamMode.shouldStreamFromNetwork {
|
||||||
|
// Start streaming from the network straight away
|
||||||
|
startNetworkStreamTask()
|
||||||
|
}
|
||||||
|
|
||||||
|
let ndbStreamTask = Task {
|
||||||
|
while !Task.isCancelled {
|
||||||
|
for await item in self.multiSessionNdbStream(filters: filters, to: desiredRelays, streamMode: streamMode, id: id) {
|
||||||
|
try Task.checkCancellation()
|
||||||
|
logStreamPipelineStats("SubscriptionManager_Ndb_MultiSession_Stream_\(id)", "SubscriptionManager_Advanced_Stream_\(id)")
|
||||||
|
switch item {
|
||||||
|
case .event(let lender):
|
||||||
|
logStreamPipelineStats("SubscriptionManager_Advanced_Stream_\(id)", "Consumer_\(id)")
|
||||||
|
try? lender.borrow({ event in
|
||||||
|
if let latestTimestamp = latestNoteTimestampSeen {
|
||||||
|
latestNoteTimestampSeen = max(latestTimestamp, event.createdAt)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
latestNoteTimestampSeen = event.createdAt
|
||||||
|
}
|
||||||
|
})
|
||||||
|
continuation.yield(item)
|
||||||
|
case .eose:
|
||||||
|
break // Should not happen
|
||||||
|
case .ndbEose:
|
||||||
|
logStreamPipelineStats("SubscriptionManager_Advanced_Stream_\(id)", "Consumer_\(id)")
|
||||||
|
continuation.yield(item)
|
||||||
|
ndbEOSEIssued = true
|
||||||
|
if streamMode.optimizeNetworkFilter && streamMode.shouldStreamFromNetwork {
|
||||||
|
startNetworkStreamTask()
|
||||||
|
}
|
||||||
|
yieldEOSEIfReady()
|
||||||
|
case .networkEose:
|
||||||
|
break // Should not happen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
continuation.onTermination = { @Sendable _ in
|
||||||
|
networkStreamTask?.cancel()
|
||||||
|
ndbStreamTask.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func multiSessionNetworkStream(filters: [NostrFilter], to desiredRelays: [RelayURL]? = nil, streamMode: StreamMode? = nil, id: UUID? = nil) -> AsyncStream<StreamItem> {
|
||||||
|
let id = id ?? UUID()
|
||||||
|
let streamMode = streamMode ?? defaultStreamMode()
|
||||||
|
return AsyncStream<StreamItem> { continuation in
|
||||||
|
let startTime = CFAbsoluteTimeGetCurrent()
|
||||||
|
Self.logger.debug("Network subscription \(id.uuidString, privacy: .public): Started")
|
||||||
|
|
||||||
|
let streamTask = Task {
|
||||||
|
while await !self.pool.open {
|
||||||
|
Self.logger.info("\(id.uuidString, privacy: .public): RelayPool closed. Sleeping for 1 second before resuming.")
|
||||||
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
for await item in await self.pool.subscribe(filters: filters, to: desiredRelays, id: id) {
|
||||||
|
try Task.checkCancellation()
|
||||||
|
logStreamPipelineStats("RelayPool_Handler_\(id)", "SubscriptionManager_Network_Stream_\(id)")
|
||||||
|
switch item {
|
||||||
|
case .event(let event):
|
||||||
|
switch streamMode {
|
||||||
|
case .ndbFirst, .ndbOnly:
|
||||||
|
break // NO-OP
|
||||||
|
case .ndbAndNetworkParallel:
|
||||||
|
continuation.yield(.event(lender: NdbNoteLender(ownedNdbNote: event)))
|
||||||
|
}
|
||||||
|
case .eose:
|
||||||
|
Self.logger.debug("Session subscription \(id.uuidString, privacy: .public): Received EOSE from the network. Elapsed: \(CFAbsoluteTimeGetCurrent() - startTime, format: .fixed(precision: 2), privacy: .public) seconds")
|
||||||
|
continuation.yield(.networkEose)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Self.logger.error("Network subscription \(id.uuidString, privacy: .public): Streaming error: \(error.localizedDescription, privacy: .public)")
|
||||||
|
}
|
||||||
|
Self.logger.debug("Network subscription \(id.uuidString, privacy: .public): Network streaming ended")
|
||||||
|
continuation.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
continuation.onTermination = { @Sendable _ in
|
||||||
|
streamTask.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func multiSessionNdbStream(filters: [NostrFilter], to desiredRelays: [RelayURL]? = nil, streamMode: StreamMode? = nil, id: UUID? = nil) -> AsyncStream<StreamItem> {
|
||||||
|
return AsyncStream<StreamItem> { continuation in
|
||||||
|
let subscriptionId = id ?? UUID()
|
||||||
|
let startTime = CFAbsoluteTimeGetCurrent()
|
||||||
|
Self.logger.info("Starting multi-session NDB subscription \(subscriptionId.uuidString, privacy: .public): \(filters.debugDescription, privacy: .private)")
|
||||||
|
let multiSessionStreamingTask = Task {
|
||||||
|
while !Task.isCancelled {
|
||||||
|
do {
|
||||||
|
guard !self.ndb.is_closed else {
|
||||||
|
Self.logger.info("\(subscriptionId.uuidString, privacy: .public): Ndb closed. Sleeping for 1 second before resuming.")
|
||||||
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
Self.logger.info("\(subscriptionId.uuidString, privacy: .public): Streaming from NDB.")
|
||||||
|
for await item in self.sessionNdbStream(filters: filters, to: desiredRelays, streamMode: streamMode, id: id) {
|
||||||
|
try Task.checkCancellation()
|
||||||
|
logStreamPipelineStats("SubscriptionManager_Ndb_Session_Stream_\(id?.uuidString ?? "NoID")", "SubscriptionManager_Ndb_MultiSession_Stream_\(id?.uuidString ?? "NoID")")
|
||||||
|
continuation.yield(item)
|
||||||
|
}
|
||||||
|
Self.logger.info("\(subscriptionId.uuidString, privacy: .public): Session subscription ended. Sleeping for 1 second before resuming.")
|
||||||
|
try await Task.sleep(nanoseconds: 1_000_000_000)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Self.logger.error("Session subscription \(subscriptionId.uuidString, privacy: .public): Error: \(error.localizedDescription, privacy: .public)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self.logger.info("\(subscriptionId.uuidString, privacy: .public): Terminated.")
|
||||||
|
}
|
||||||
|
continuation.onTermination = { @Sendable _ in
|
||||||
|
Self.logger.info("\(subscriptionId.uuidString, privacy: .public): Cancelled multi-session NDB stream.")
|
||||||
|
multiSessionStreamingTask.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func sessionNdbStream(filters: [NostrFilter], to desiredRelays: [RelayURL]? = nil, streamMode: StreamMode? = nil, id: UUID? = nil) -> AsyncStream<StreamItem> {
|
||||||
|
let id = id ?? UUID()
|
||||||
|
//let streamMode = streamMode ?? defaultStreamMode()
|
||||||
|
return AsyncStream<StreamItem> { continuation in
|
||||||
|
let startTime = CFAbsoluteTimeGetCurrent()
|
||||||
|
Self.logger.debug("Session subscription \(id.uuidString, privacy: .public): Started")
|
||||||
|
|
||||||
|
let ndbStreamTask = Task {
|
||||||
|
do {
|
||||||
|
for await item in try self.ndb.subscribe(filters: try filters.map({ try NdbFilter(from: $0) })) {
|
||||||
|
try Task.checkCancellation()
|
||||||
|
switch item {
|
||||||
|
case .eose:
|
||||||
|
Self.logger.debug("Session subscription \(id.uuidString, privacy: .public): Received EOSE from nostrdb. Elapsed: \(CFAbsoluteTimeGetCurrent() - startTime, format: .fixed(precision: 2), privacy: .public) seconds")
|
||||||
|
continuation.yield(.ndbEose)
|
||||||
|
case .event(let noteKey):
|
||||||
|
let lender = NdbNoteLender(ndb: self.ndb, noteKey: noteKey)
|
||||||
|
try Task.checkCancellation()
|
||||||
|
guard let desiredRelays else {
|
||||||
|
continuation.yield(.event(lender: lender)) // If no desired relays are specified, return all notes we see.
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if try ndb.was(noteKey: noteKey, seenOnAnyOf: desiredRelays) {
|
||||||
|
continuation.yield(.event(lender: lender)) // If desired relays were specified and this note was seen there, return it.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Self.logger.error("Session subscription \(id.uuidString, privacy: .public): NDB streaming error: \(error.localizedDescription, privacy: .public)")
|
||||||
|
}
|
||||||
|
Self.logger.debug("Session subscription \(id.uuidString, privacy: .public): NDB streaming ended")
|
||||||
|
continuation.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
Task {
|
||||||
|
// Add the ndb streaming task to the task manager so that it can be cancelled when the app is backgrounded
|
||||||
|
let ndbStreamTaskId = await self.taskManager.add(task: ndbStreamTask)
|
||||||
|
|
||||||
|
continuation.onTermination = { @Sendable _ in
|
||||||
|
Task {
|
||||||
|
await self.taskManager.cancelAndCleanUp(taskId: ndbStreamTaskId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Utility functions
|
||||||
|
|
||||||
|
private func defaultStreamMode() -> StreamMode {
|
||||||
|
self.experimentalLocalRelayModelSupport ? .ndbFirst(optimizeNetworkFilter: false) : .ndbAndNetworkParallel(optimizeNetworkFilter: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Finding specific data from Nostr
|
||||||
|
|
||||||
|
/// Finds a non-replaceable event based on a note ID
|
||||||
|
func lookup(noteId: NoteId, to targetRelays: [RelayURL]? = nil, timeout: Duration? = nil) async throws -> NdbNoteLender? {
|
||||||
|
let filter = NostrFilter(ids: [noteId], limit: 1)
|
||||||
|
|
||||||
|
// Since note ids point to immutable objects, we can do a simple ndb lookup first
|
||||||
|
if let noteKey = self.ndb.lookup_note_key(noteId) {
|
||||||
|
return NdbNoteLender(ndb: self.ndb, noteKey: noteKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not available in local ndb, stream from network
|
||||||
|
outerLoop: for await item in await self.pool.subscribe(filters: [NostrFilter(ids: [noteId], limit: 1)], to: targetRelays, eoseTimeout: timeout) {
|
||||||
|
switch item {
|
||||||
|
case .event(let event):
|
||||||
|
return NdbNoteLender(ownedNdbNote: event)
|
||||||
|
case .eose:
|
||||||
|
break outerLoop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func query(filters: [NostrFilter], to: [RelayURL]? = nil, timeout: Duration? = nil) async -> [NostrEvent] {
|
||||||
|
var events: [NostrEvent] = []
|
||||||
|
for await noteLender in self.streamExistingEvents(filters: filters, to: to, timeout: timeout) {
|
||||||
|
noteLender.justUseACopy({ events.append($0) })
|
||||||
|
}
|
||||||
|
return events
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds a replaceable event based on an `naddr` address.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - naddr: the `naddr` address
|
||||||
|
func lookup(naddr: NAddr, to targetRelays: [RelayURL]? = nil, timeout: Duration? = nil) async -> NostrEvent? {
|
||||||
|
var nostrKinds: [NostrKind]? = NostrKind(rawValue: naddr.kind).map { [$0] }
|
||||||
|
|
||||||
|
let filter = NostrFilter(kinds: nostrKinds, authors: [naddr.author])
|
||||||
|
|
||||||
|
for await noteLender in self.streamExistingEvents(filters: [filter], to: targetRelays, timeout: timeout) {
|
||||||
|
// TODO: This can be refactored to borrow the note instead of copying it. But we need to implement `referenced_params` on `UnownedNdbNote` to do so
|
||||||
|
guard let event = noteLender.justGetACopy() else { continue }
|
||||||
|
if event.referenced_params.first?.param.string() == naddr.identifier {
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Improve this. This is mostly intact to keep compatibility with its predecessor, but we can do better
|
||||||
|
func findEvent(query: FindEvent) async -> FoundEvent? {
|
||||||
|
var filter: NostrFilter? = nil
|
||||||
|
let find_from = query.find_from
|
||||||
|
let query = query.type
|
||||||
|
|
||||||
|
switch query {
|
||||||
|
case .profile(let pubkey):
|
||||||
|
if let profile_txn = self.ndb.lookup_profile(pubkey),
|
||||||
|
let record = profile_txn.unsafeUnownedValue,
|
||||||
|
record.profile != nil
|
||||||
|
{
|
||||||
|
return .profile(pubkey)
|
||||||
|
}
|
||||||
|
filter = NostrFilter(kinds: [.metadata], limit: 1, authors: [pubkey])
|
||||||
|
case .event(let evid):
|
||||||
|
if let event = self.ndb.lookup_note(evid)?.unsafeUnownedValue?.to_owned() {
|
||||||
|
return .event(event)
|
||||||
|
}
|
||||||
|
filter = NostrFilter(ids: [evid], limit: 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var attempts: Int = 0
|
||||||
|
var has_event = false
|
||||||
|
guard let filter else { return nil }
|
||||||
|
|
||||||
|
for await noteLender in self.streamExistingEvents(filters: [filter], to: find_from) {
|
||||||
|
let foundEvent: FoundEvent? = try? noteLender.borrow({ event in
|
||||||
|
switch query {
|
||||||
|
case .profile:
|
||||||
|
if event.known_kind == .metadata {
|
||||||
|
return .profile(event.pubkey)
|
||||||
|
}
|
||||||
|
case .event:
|
||||||
|
return .event(event.toOwned())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if let foundEvent {
|
||||||
|
return foundEvent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Task management
|
||||||
|
|
||||||
|
func cancelAllTasks() async {
|
||||||
|
await self.taskManager.cancelAllTasks()
|
||||||
|
}
|
||||||
|
|
||||||
|
actor TaskManager {
|
||||||
|
private var tasks: [UUID: Task<Void, Never>] = [:]
|
||||||
|
|
||||||
|
private static let logger = Logger(
|
||||||
|
subsystem: "com.jb55.damus",
|
||||||
|
category: "subscription_manager.task_manager"
|
||||||
|
)
|
||||||
|
|
||||||
|
func add(task: Task<Void, Never>) -> UUID {
|
||||||
|
let taskId = UUID()
|
||||||
|
self.tasks[taskId] = task
|
||||||
|
return taskId
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelAndCleanUp(taskId: UUID) async {
|
||||||
|
self.tasks[taskId]?.cancel()
|
||||||
|
await self.tasks[taskId]?.value
|
||||||
|
self.tasks[taskId] = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelAllTasks() async {
|
||||||
|
await withTaskGroup { group in
|
||||||
|
Self.logger.info("Cancelling all SubscriptionManager tasks")
|
||||||
|
// Start each task cancellation in parallel for faster execution
|
||||||
|
for (taskId, _) in self.tasks {
|
||||||
|
Self.logger.info("Cancelling SubscriptionManager task \(taskId.uuidString, privacy: .public)")
|
||||||
|
group.addTask {
|
||||||
|
await self.cancelAndCleanUp(taskId: taskId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// However, wait until all cancellations are complete to avoid race conditions.
|
||||||
|
for await value in group {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
Self.logger.info("Cancelled all SubscriptionManager tasks")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,8 +500,62 @@ extension NostrNetworkManager {
|
|||||||
|
|
||||||
enum StreamItem {
|
enum StreamItem {
|
||||||
/// An event which can be borrowed from NostrDB
|
/// An event which can be borrowed from NostrDB
|
||||||
case event(borrow: NdbNoteLender)
|
case event(lender: NdbNoteLender)
|
||||||
/// The end of stored events
|
/// The canonical generic "end of stored events", which depends on the stream mode. See `StreamMode` to see when this event is fired in relation to other EOSEs
|
||||||
case eose
|
case eose
|
||||||
|
/// "End of stored events" from NostrDB.
|
||||||
|
case ndbEose
|
||||||
|
/// "End of stored events" from all relays in `RelayPool`.
|
||||||
|
case networkEose
|
||||||
|
|
||||||
|
var debugDescription: String {
|
||||||
|
switch self {
|
||||||
|
case .event(lender: let lender):
|
||||||
|
let detailedDescription = try? lender.borrow({ event in
|
||||||
|
"Note with ID: \(event.id.hex())"
|
||||||
|
})
|
||||||
|
return detailedDescription ?? "Some note"
|
||||||
|
case .eose:
|
||||||
|
return "EOSE"
|
||||||
|
case .ndbEose:
|
||||||
|
return "NDB EOSE"
|
||||||
|
case .networkEose:
|
||||||
|
return "NETWORK EOSE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The mode of streaming
|
||||||
|
enum StreamMode {
|
||||||
|
/// Returns notes exclusively through NostrDB, treating it as the only channel for information in the pipeline. Generic EOSE is fired when EOSE is received from NostrDB
|
||||||
|
/// `optimizeNetworkFilter`: Returns notes from ndb, then streams from the network with an added "since" filter set to the latest note stored on ndb.
|
||||||
|
case ndbFirst(optimizeNetworkFilter: Bool)
|
||||||
|
/// Returns notes from both NostrDB and the network, in parallel, treating it with similar importance against the network relays. Generic EOSE is fired when EOSE is received from both the network and NostrDB
|
||||||
|
/// `optimizeNetworkFilter`: Returns notes from ndb, then streams from the network with an added "since" filter set to the latest note stored on ndb.
|
||||||
|
case ndbAndNetworkParallel(optimizeNetworkFilter: Bool)
|
||||||
|
/// Ignores the network. Used for testing purposes
|
||||||
|
case ndbOnly
|
||||||
|
|
||||||
|
var optimizeNetworkFilter: Bool {
|
||||||
|
switch self {
|
||||||
|
case .ndbFirst(optimizeNetworkFilter: let optimizeNetworkFilter):
|
||||||
|
return optimizeNetworkFilter
|
||||||
|
case .ndbAndNetworkParallel(optimizeNetworkFilter: let optimizeNetworkFilter):
|
||||||
|
return optimizeNetworkFilter
|
||||||
|
case .ndbOnly:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var shouldStreamFromNetwork: Bool {
|
||||||
|
switch self {
|
||||||
|
case .ndbFirst:
|
||||||
|
return true
|
||||||
|
case .ndbAndNetworkParallel:
|
||||||
|
return true
|
||||||
|
case .ndbOnly:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,72 +122,68 @@ extension NostrNetworkManager {
|
|||||||
|
|
||||||
// MARK: - Listening to and handling relay updates from the network
|
// MARK: - Listening to and handling relay updates from the network
|
||||||
|
|
||||||
func connect() {
|
func connect() async {
|
||||||
self.load()
|
await self.load()
|
||||||
|
|
||||||
self.relayListObserverTask?.cancel()
|
self.relayListObserverTask?.cancel()
|
||||||
self.relayListObserverTask = Task { await self.listenAndHandleRelayUpdates() }
|
self.relayListObserverTask = Task { await self.listenAndHandleRelayUpdates() }
|
||||||
self.walletUpdatesObserverTask?.cancel()
|
self.walletUpdatesObserverTask?.cancel()
|
||||||
self.walletUpdatesObserverTask = handle_notify(.attached_wallet).sink { _ in self.load() }
|
self.walletUpdatesObserverTask = handle_notify(.attached_wallet).sink { _ in Task { await self.load() } }
|
||||||
}
|
}
|
||||||
|
|
||||||
func listenAndHandleRelayUpdates() async {
|
func listenAndHandleRelayUpdates() async {
|
||||||
let filter = NostrFilter(kinds: [.relay_list], authors: [delegate.keypair.pubkey])
|
let filter = NostrFilter(kinds: [.relay_list], authors: [delegate.keypair.pubkey])
|
||||||
for await item in self.reader.subscribe(filters: [filter]) {
|
for await noteLender in self.reader.streamIndefinitely(filters: [filter]) {
|
||||||
switch item {
|
let currentRelayListCreationDate = self.getUserCurrentRelayListCreationDate()
|
||||||
case .event(borrow: let borrow): // Signature validity already ensured at this point
|
try? await noteLender.borrow({ note in
|
||||||
let currentRelayListCreationDate = self.getUserCurrentRelayListCreationDate()
|
guard note.pubkey == self.delegate.keypair.pubkey else { return } // Ensure this new list was ours
|
||||||
try? borrow { note in
|
guard note.createdAt > (currentRelayListCreationDate ?? 0) else { return } // Ensure this is a newer list
|
||||||
guard note.pubkey == self.delegate.keypair.pubkey else { return } // Ensure this new list was ours
|
guard let relayList = try? NIP65.RelayList(event: note) else { return } // Ensure it is a valid NIP-65 list
|
||||||
guard note.createdAt > (currentRelayListCreationDate ?? 0) else { return } // Ensure this is a newer list
|
|
||||||
guard let relayList = try? NIP65.RelayList(event: note) else { return } // Ensure it is a valid NIP-65 list
|
try? await self.set(userRelayList: relayList) // Set the validated list
|
||||||
|
})
|
||||||
try? self.set(userRelayList: relayList) // Set the validated list
|
|
||||||
}
|
|
||||||
case .eose: continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Editing the user's relay list
|
// MARK: - Editing the user's relay list
|
||||||
|
|
||||||
func upsert(relay: NIP65.RelayList.RelayItem, force: Bool = false, overwriteExisting: Bool = false) throws(UpdateError) {
|
func upsert(relay: NIP65.RelayList.RelayItem, force: Bool = false, overwriteExisting: Bool = false) async throws(UpdateError) {
|
||||||
guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList }
|
guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList }
|
||||||
guard !currentUserRelayList.relays.keys.contains(relay.url) || overwriteExisting else { throw .relayAlreadyExists }
|
guard !currentUserRelayList.relays.keys.contains(relay.url) || overwriteExisting else { throw .relayAlreadyExists }
|
||||||
var newList = currentUserRelayList.relays
|
var newList = currentUserRelayList.relays
|
||||||
newList[relay.url] = relay
|
newList[relay.url] = relay
|
||||||
try self.set(userRelayList: NIP65.RelayList(relays: Array(newList.values)))
|
try await self.set(userRelayList: NIP65.RelayList(relays: Array(newList.values)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func insert(relay: NIP65.RelayList.RelayItem, force: Bool = false) throws(UpdateError) {
|
func insert(relay: NIP65.RelayList.RelayItem, force: Bool = false) async throws(UpdateError) {
|
||||||
guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList }
|
guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList }
|
||||||
guard currentUserRelayList.relays[relay.url] == nil else { throw .relayAlreadyExists }
|
guard currentUserRelayList.relays[relay.url] == nil else { throw .relayAlreadyExists }
|
||||||
try self.upsert(relay: relay, force: force)
|
try await self.upsert(relay: relay, force: force)
|
||||||
}
|
}
|
||||||
|
|
||||||
func remove(relayURL: RelayURL, force: Bool = false) throws(UpdateError) {
|
func remove(relayURL: RelayURL, force: Bool = false) async throws(UpdateError) {
|
||||||
guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList }
|
guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList }
|
||||||
guard currentUserRelayList.relays.keys.contains(relayURL) || force else { throw .noSuchRelay }
|
guard currentUserRelayList.relays.keys.contains(relayURL) || force else { throw .noSuchRelay }
|
||||||
var newList = currentUserRelayList.relays
|
var newList = currentUserRelayList.relays
|
||||||
newList[relayURL] = nil
|
newList[relayURL] = nil
|
||||||
try self.set(userRelayList: NIP65.RelayList(relays: Array(newList.values)))
|
try await self.set(userRelayList: NIP65.RelayList(relays: Array(newList.values)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func set(userRelayList: NIP65.RelayList) throws(UpdateError) {
|
func set(userRelayList: NIP65.RelayList) async throws(UpdateError) {
|
||||||
guard let fullKeypair = delegate.keypair.to_full() else { throw .notAuthorizedToChangeRelayList }
|
guard let fullKeypair = delegate.keypair.to_full() else { throw .notAuthorizedToChangeRelayList }
|
||||||
guard let relayListEvent = userRelayList.toNostrEvent(keypair: fullKeypair) else { throw .cannotFormRelayListEvent }
|
guard let relayListEvent = userRelayList.toNostrEvent(keypair: fullKeypair) else { throw .cannotFormRelayListEvent }
|
||||||
|
|
||||||
self.apply(newRelayList: self.computeRelaysToConnectTo(with: userRelayList))
|
await self.apply(newRelayList: self.computeRelaysToConnectTo(with: userRelayList))
|
||||||
|
|
||||||
self.pool.send(.event(relayListEvent)) // This will send to NostrDB as well, which will locally save that NIP-65 event
|
await self.pool.send(.event(relayListEvent)) // This will send to NostrDB as well, which will locally save that NIP-65 event
|
||||||
self.delegate.latestRelayListEventIdHex = relayListEvent.id.hex() // Make sure we are able to recall this event from NostrDB
|
self.delegate.latestRelayListEventIdHex = relayListEvent.id.hex() // Make sure we are able to recall this event from NostrDB
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Syncing our saved user relay list with the active `RelayPool`
|
// MARK: - Syncing our saved user relay list with the active `RelayPool`
|
||||||
|
|
||||||
/// Loads the current user relay list
|
/// Loads the current user relay list
|
||||||
func load() {
|
func load() async {
|
||||||
self.apply(newRelayList: self.relaysToConnectTo())
|
await self.apply(newRelayList: self.relaysToConnectTo())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads a new relay list into the active relay pool, making sure it matches the specified relay list.
|
/// Loads a new relay list into the active relay pool, making sure it matches the specified relay list.
|
||||||
@@ -201,7 +197,8 @@ extension NostrNetworkManager {
|
|||||||
///
|
///
|
||||||
/// - This is `private` because syncing the user's saved relay list with the relay pool is `NostrNetworkManager`'s responsibility,
|
/// - This is `private` because syncing the user's saved relay list with the relay pool is `NostrNetworkManager`'s responsibility,
|
||||||
/// so we do not want other classes to forcibly load this.
|
/// so we do not want other classes to forcibly load this.
|
||||||
private func apply(newRelayList: [RelayPool.RelayDescriptor]) {
|
@MainActor
|
||||||
|
private func apply(newRelayList: [RelayPool.RelayDescriptor]) async {
|
||||||
let currentRelayList = self.pool.relays.map({ $0.descriptor })
|
let currentRelayList = self.pool.relays.map({ $0.descriptor })
|
||||||
|
|
||||||
var changed = false
|
var changed = false
|
||||||
@@ -221,28 +218,39 @@ extension NostrNetworkManager {
|
|||||||
let relaysToRemove = currentRelayURLs.subtracting(newRelayURLs)
|
let relaysToRemove = currentRelayURLs.subtracting(newRelayURLs)
|
||||||
let relaysToAdd = newRelayURLs.subtracting(currentRelayURLs)
|
let relaysToAdd = newRelayURLs.subtracting(currentRelayURLs)
|
||||||
|
|
||||||
// Remove relays not in the new list
|
await withTaskGroup { taskGroup in
|
||||||
relaysToRemove.forEach { url in
|
// Remove relays not in the new list
|
||||||
pool.remove_relay(url)
|
relaysToRemove.forEach { url in
|
||||||
changed = true
|
taskGroup.addTask(operation: { await self.pool.remove_relay(url) })
|
||||||
}
|
changed = true
|
||||||
|
}
|
||||||
|
|
||||||
// Add new relays from the new list
|
// Add new relays from the new list
|
||||||
relaysToAdd.forEach { url in
|
relaysToAdd.forEach { url in
|
||||||
guard let descriptor = newRelayList.first(where: { $0.url == url }) else { return }
|
guard let descriptor = newRelayList.first(where: { $0.url == url }) else { return }
|
||||||
add_new_relay(
|
taskGroup.addTask(operation: {
|
||||||
model_cache: delegate.relayModelCache,
|
await add_new_relay(
|
||||||
relay_filters: delegate.relayFilters,
|
model_cache: self.delegate.relayModelCache,
|
||||||
pool: pool,
|
relay_filters: self.delegate.relayFilters,
|
||||||
descriptor: descriptor,
|
pool: self.pool,
|
||||||
new_relay_filters: new_relay_filters,
|
descriptor: descriptor,
|
||||||
logging_enabled: delegate.developerMode
|
new_relay_filters: new_relay_filters,
|
||||||
)
|
logging_enabled: self.delegate.developerMode
|
||||||
changed = true
|
)
|
||||||
|
})
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
for await value in taskGroup { continue }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always tell RelayPool to connect whether or not we are already connected.
|
||||||
|
// This is because:
|
||||||
|
// 1. Internally it won't redo the connection because of internal checks
|
||||||
|
// 2. Even if the relay list has not changed, relays may have been disconnected from app lifecycle or other events
|
||||||
|
await pool.connect()
|
||||||
|
|
||||||
if changed {
|
if changed {
|
||||||
pool.connect()
|
|
||||||
notify(.relays_changed)
|
notify(.relays_changed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -280,8 +288,8 @@ fileprivate extension NIP65.RelayList {
|
|||||||
/// - descriptor: The description of the relay being added
|
/// - descriptor: The description of the relay being added
|
||||||
/// - new_relay_filters: Whether to insert new relay filters
|
/// - new_relay_filters: Whether to insert new relay filters
|
||||||
/// - logging_enabled: Whether logging is enabled
|
/// - logging_enabled: Whether logging is enabled
|
||||||
fileprivate func add_new_relay(model_cache: RelayModelCache, relay_filters: RelayFilters, pool: RelayPool, descriptor: RelayPool.RelayDescriptor, new_relay_filters: Bool, logging_enabled: Bool) {
|
fileprivate func add_new_relay(model_cache: RelayModelCache, relay_filters: RelayFilters, pool: RelayPool, descriptor: RelayPool.RelayDescriptor, new_relay_filters: Bool, logging_enabled: Bool) async {
|
||||||
try? pool.add_relay(descriptor)
|
try? await pool.add_relay(descriptor)
|
||||||
let url = descriptor.url
|
let url = descriptor.url
|
||||||
|
|
||||||
let relay_id = url
|
let relay_id = url
|
||||||
@@ -299,7 +307,7 @@ fileprivate func add_new_relay(model_cache: RelayModelCache, relay_filters: Rela
|
|||||||
model_cache.insert(model: model)
|
model_cache.insert(model: model)
|
||||||
|
|
||||||
if logging_enabled {
|
if logging_enabled {
|
||||||
pool.setLog(model.log, for: relay_id)
|
Task { await pool.setLog(model.log, for: relay_id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// if this is the first time adding filters, we should filter non-paid relays
|
// if this is the first time adding filters, we should filter non-paid relays
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
//
|
||||||
|
// ProfileObserver.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2025-09-19.
|
||||||
|
//
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class ProfileObserver: ObservableObject {
|
||||||
|
private let pubkey: Pubkey
|
||||||
|
private var observerTask: Task<Void, any Error>? = nil
|
||||||
|
private let damusState: DamusState
|
||||||
|
|
||||||
|
init(pubkey: Pubkey, damusState: DamusState) {
|
||||||
|
self.pubkey = pubkey
|
||||||
|
self.damusState = damusState
|
||||||
|
self.watchProfileChanges()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func watchProfileChanges() {
|
||||||
|
observerTask?.cancel()
|
||||||
|
observerTask = Task {
|
||||||
|
for await _ in await damusState.nostrNetwork.profilesManager.streamProfile(pubkey: self.pubkey) {
|
||||||
|
try Task.checkCancellation()
|
||||||
|
DispatchQueue.main.async { self.objectWillChange.send() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
observerTask?.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,6 +35,15 @@ enum NostrConnectionEvent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var subId: String? {
|
||||||
|
switch self {
|
||||||
|
case .ws_connection_event(_):
|
||||||
|
return nil
|
||||||
|
case .nostr_event(let event):
|
||||||
|
return event.subid
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class RelayConnection: ObservableObject {
|
final class RelayConnection: ObservableObject {
|
||||||
@@ -48,13 +57,13 @@ final class RelayConnection: ObservableObject {
|
|||||||
private lazy var socket = WebSocket(relay_url.url)
|
private lazy var socket = WebSocket(relay_url.url)
|
||||||
private var subscriptionToken: AnyCancellable?
|
private var subscriptionToken: AnyCancellable?
|
||||||
|
|
||||||
private var handleEvent: (NostrConnectionEvent) -> ()
|
private var handleEvent: (NostrConnectionEvent) async -> ()
|
||||||
private var processEvent: (WebSocketEvent) -> ()
|
private var processEvent: (WebSocketEvent) -> ()
|
||||||
private let relay_url: RelayURL
|
private let relay_url: RelayURL
|
||||||
var log: RelayLog?
|
var log: RelayLog?
|
||||||
|
|
||||||
init(url: RelayURL,
|
init(url: RelayURL,
|
||||||
handleEvent: @escaping (NostrConnectionEvent) -> (),
|
handleEvent: @escaping (NostrConnectionEvent) async -> (),
|
||||||
processUnverifiedWSEvent: @escaping (WebSocketEvent) -> ())
|
processUnverifiedWSEvent: @escaping (WebSocketEvent) -> ())
|
||||||
{
|
{
|
||||||
self.relay_url = url
|
self.relay_url = url
|
||||||
@@ -95,12 +104,12 @@ final class RelayConnection: ObservableObject {
|
|||||||
.sink { [weak self] completion in
|
.sink { [weak self] completion in
|
||||||
switch completion {
|
switch completion {
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
self?.receive(event: .error(error))
|
Task { await self?.receive(event: .error(error)) }
|
||||||
case .finished:
|
case .finished:
|
||||||
self?.receive(event: .disconnected(.normalClosure, nil))
|
Task { await self?.receive(event: .disconnected(.normalClosure, nil)) }
|
||||||
}
|
}
|
||||||
} receiveValue: { [weak self] event in
|
} receiveValue: { [weak self] event in
|
||||||
self?.receive(event: event)
|
Task { await self?.receive(event: event) }
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.connect()
|
socket.connect()
|
||||||
@@ -138,7 +147,7 @@ final class RelayConnection: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func receive(event: WebSocketEvent) {
|
private func receive(event: WebSocketEvent) async {
|
||||||
assert(!Thread.isMainThread, "This code must not be executed on the main thread")
|
assert(!Thread.isMainThread, "This code must not be executed on the main thread")
|
||||||
processEvent(event)
|
processEvent(event)
|
||||||
switch event {
|
switch event {
|
||||||
@@ -149,7 +158,7 @@ final class RelayConnection: ObservableObject {
|
|||||||
self.isConnecting = false
|
self.isConnecting = false
|
||||||
}
|
}
|
||||||
case .message(let message):
|
case .message(let message):
|
||||||
self.receive(message: message)
|
await self.receive(message: message)
|
||||||
case .disconnected(let closeCode, let reason):
|
case .disconnected(let closeCode, let reason):
|
||||||
if closeCode != .normalClosure {
|
if closeCode != .normalClosure {
|
||||||
Log.error("⚠️ Warning: RelayConnection (%d) closed with code: %s", for: .networking, String(describing: closeCode), String(describing: reason))
|
Log.error("⚠️ Warning: RelayConnection (%d) closed with code: %s", for: .networking, String(describing: closeCode), String(describing: reason))
|
||||||
@@ -176,10 +185,8 @@ final class RelayConnection: ObservableObject {
|
|||||||
self.reconnect_with_backoff()
|
self.reconnect_with_backoff()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DispatchQueue.main.async {
|
guard let ws_connection_event = NostrConnectionEvent.WSConnectionEvent.from(full_ws_event: event) else { return }
|
||||||
guard let ws_connection_event = NostrConnectionEvent.WSConnectionEvent.from(full_ws_event: event) else { return }
|
await self.handleEvent(.ws_connection_event(ws_connection_event))
|
||||||
self.handleEvent(.ws_connection_event(ws_connection_event))
|
|
||||||
}
|
|
||||||
|
|
||||||
if let description = event.description {
|
if let description = event.description {
|
||||||
log?.add(description)
|
log?.add(description)
|
||||||
@@ -213,21 +220,19 @@ final class RelayConnection: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func receive(message: URLSessionWebSocketTask.Message) {
|
private func receive(message: URLSessionWebSocketTask.Message) async {
|
||||||
switch message {
|
switch message {
|
||||||
case .string(let messageString):
|
case .string(let messageString):
|
||||||
// NOTE: Once we switch to the local relay model,
|
// NOTE: Once we switch to the local relay model,
|
||||||
// we will not need to verify nostr events at this point.
|
// we will not need to verify nostr events at this point.
|
||||||
if let ev = decode_and_verify_nostr_response(txt: messageString) {
|
if let ev = decode_and_verify_nostr_response(txt: messageString) {
|
||||||
DispatchQueue.main.async {
|
await self.handleEvent(.nostr_event(ev))
|
||||||
self.handleEvent(.nostr_event(ev))
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
print("failed to decode event \(messageString)")
|
print("failed to decode event \(messageString)")
|
||||||
case .data(let messageData):
|
case .data(let messageData):
|
||||||
if let messageString = String(data: messageData, encoding: .utf8) {
|
if let messageString = String(data: messageData, encoding: .utf8) {
|
||||||
receive(message: .string(messageString))
|
await receive(message: .string(messageString))
|
||||||
}
|
}
|
||||||
@unknown default:
|
@unknown default:
|
||||||
print("An unexpected URLSessionWebSocketTask.Message was received.")
|
print("An unexpected URLSessionWebSocketTask.Message was received.")
|
||||||
|
|||||||
+255
-107
@@ -10,7 +10,10 @@ import Network
|
|||||||
|
|
||||||
struct RelayHandler {
|
struct RelayHandler {
|
||||||
let sub_id: String
|
let sub_id: String
|
||||||
let callback: (RelayURL, NostrConnectionEvent) -> ()
|
/// The filters that this handler will handle. Set this to `nil` if you want your handler to receive all events coming from the relays.
|
||||||
|
let filters: [NostrFilter]?
|
||||||
|
let to: [RelayURL]?
|
||||||
|
var handler: AsyncStream<(RelayURL, NostrConnectionEvent)>.Continuation
|
||||||
}
|
}
|
||||||
|
|
||||||
struct QueuedRequest {
|
struct QueuedRequest {
|
||||||
@@ -19,94 +22,140 @@ struct QueuedRequest {
|
|||||||
let skip_ephemeral: Bool
|
let skip_ephemeral: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct SeenEvent: Hashable {
|
||||||
|
let relay_id: RelayURL
|
||||||
|
let evid: NoteId
|
||||||
|
}
|
||||||
|
|
||||||
/// Establishes and manages connections and subscriptions to a list of relays.
|
/// Establishes and manages connections and subscriptions to a list of relays.
|
||||||
class RelayPool {
|
actor RelayPool {
|
||||||
|
@MainActor
|
||||||
private(set) var relays: [Relay] = []
|
private(set) var relays: [Relay] = []
|
||||||
|
var open: Bool = false
|
||||||
var handlers: [RelayHandler] = []
|
var handlers: [RelayHandler] = []
|
||||||
var request_queue: [QueuedRequest] = []
|
var request_queue: [QueuedRequest] = []
|
||||||
var seen: [NoteId: Set<RelayURL>] = [:]
|
var seen: [NoteId: Set<RelayURL>] = [:]
|
||||||
var counts: [RelayURL: UInt64] = [:]
|
var counts: [RelayURL: UInt64] = [:]
|
||||||
var ndb: Ndb
|
var ndb: Ndb?
|
||||||
/// The keypair used to authenticate with relays
|
/// The keypair used to authenticate with relays
|
||||||
var keypair: Keypair?
|
var keypair: Keypair?
|
||||||
var message_received_function: (((String, RelayDescriptor)) -> Void)?
|
var message_received_function: (((String, RelayDescriptor)) -> Void)?
|
||||||
var message_sent_function: (((String, Relay)) -> Void)?
|
var message_sent_function: (((String, Relay)) -> Void)?
|
||||||
|
var delegate: Delegate?
|
||||||
|
private(set) var signal: SignalModel = SignalModel()
|
||||||
|
|
||||||
private let network_monitor = NWPathMonitor()
|
let network_monitor = NWPathMonitor()
|
||||||
private let network_monitor_queue = DispatchQueue(label: "io.damus.network_monitor")
|
private let network_monitor_queue = DispatchQueue(label: "io.damus.network_monitor")
|
||||||
private var last_network_status: NWPath.Status = .unsatisfied
|
private var last_network_status: NWPath.Status = .unsatisfied
|
||||||
|
|
||||||
|
/// The limit of maximum concurrent subscriptions. Any subscriptions beyond this limit will be paused until subscriptions clear
|
||||||
|
/// This is to avoid error states and undefined behaviour related to hitting subscription limits on the relays, by letting those wait instead — with the principle that although slower is not ideal, it is better than completely broken.
|
||||||
|
static let MAX_CONCURRENT_SUBSCRIPTION_LIMIT = 14 // This number is only an educated guess based on some local experiments.
|
||||||
|
|
||||||
func close() {
|
func close() async {
|
||||||
disconnect()
|
await disconnect()
|
||||||
relays = []
|
await clearRelays()
|
||||||
|
open = false
|
||||||
handlers = []
|
handlers = []
|
||||||
request_queue = []
|
request_queue = []
|
||||||
seen.removeAll()
|
await clearSeen()
|
||||||
counts = [:]
|
counts = [:]
|
||||||
keypair = nil
|
keypair = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func clearRelays() {
|
||||||
|
relays = []
|
||||||
|
}
|
||||||
|
|
||||||
|
private func clearSeen() {
|
||||||
|
seen.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
init(ndb: Ndb, keypair: Keypair? = nil) {
|
init(ndb: Ndb?, keypair: Keypair? = nil) {
|
||||||
self.ndb = ndb
|
self.ndb = ndb
|
||||||
self.keypair = keypair
|
self.keypair = keypair
|
||||||
|
|
||||||
network_monitor.pathUpdateHandler = { [weak self] path in
|
network_monitor.pathUpdateHandler = { [weak self] path in
|
||||||
if (path.status == .satisfied || path.status == .requiresConnection) && self?.last_network_status != path.status {
|
Task { await self?.pathUpdateHandler(path: path) }
|
||||||
DispatchQueue.main.async {
|
|
||||||
self?.connect_to_disconnected()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let self, path.status != self.last_network_status {
|
|
||||||
for relay in self.relays {
|
|
||||||
relay.connection.log?.add("Network state: \(path.status)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self?.last_network_status = path.status
|
|
||||||
}
|
}
|
||||||
network_monitor.start(queue: network_monitor_queue)
|
network_monitor.start(queue: network_monitor_queue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func pathUpdateHandler(path: NWPath) async {
|
||||||
|
if (path.status == .satisfied || path.status == .requiresConnection) && self.last_network_status != path.status {
|
||||||
|
await self.connect_to_disconnected()
|
||||||
|
}
|
||||||
|
|
||||||
|
if path.status != self.last_network_status {
|
||||||
|
for relay in await self.relays {
|
||||||
|
relay.connection.log?.add("Network state: \(path.status)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.last_network_status = path.status
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
var our_descriptors: [RelayDescriptor] {
|
var our_descriptors: [RelayDescriptor] {
|
||||||
return all_descriptors.filter { d in !d.ephemeral }
|
return all_descriptors.filter { d in !d.ephemeral }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
var all_descriptors: [RelayDescriptor] {
|
var all_descriptors: [RelayDescriptor] {
|
||||||
relays.map { r in r.descriptor }
|
relays.map { r in r.descriptor }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
var num_connected: Int {
|
var num_connected: Int {
|
||||||
return relays.reduce(0) { n, r in n + (r.connection.isConnected ? 1 : 0) }
|
return relays.reduce(0) { n, r in n + (r.connection.isConnected ? 1 : 0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func remove_handler(sub_id: String) {
|
func remove_handler(sub_id: String) {
|
||||||
self.handlers = handlers.filter { $0.sub_id != sub_id }
|
self.handlers = handlers.filter {
|
||||||
print("removing \(sub_id) handler, current: \(handlers.count)")
|
if $0.sub_id != sub_id {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$0.handler.finish()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.debug("Removing %s handler, current: %d", for: .networking, sub_id, handlers.count)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ping() {
|
func ping() async {
|
||||||
Log.info("Pinging %d relays", for: .networking, relays.count)
|
Log.info("Pinging %d relays", for: .networking, await relays.count)
|
||||||
for relay in relays {
|
for relay in await relays {
|
||||||
relay.connection.ping()
|
relay.connection.ping()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func register_handler(sub_id: String, handler: @escaping (RelayURL, NostrConnectionEvent) -> ()) {
|
func register_handler(sub_id: String, filters: [NostrFilter]?, to relays: [RelayURL]? = nil, handler: AsyncStream<(RelayURL, NostrConnectionEvent)>.Continuation) async {
|
||||||
for handler in handlers {
|
while handlers.count > Self.MAX_CONCURRENT_SUBSCRIPTION_LIMIT {
|
||||||
// don't add duplicate handlers
|
Log.debug("%s: Too many subscriptions, waiting for subscription pool to clear", for: .networking, sub_id)
|
||||||
if handler.sub_id == sub_id {
|
try? await Task.sleep(for: .seconds(1))
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
self.handlers.append(RelayHandler(sub_id: sub_id, callback: handler))
|
Log.debug("%s: Subscription pool cleared", for: .networking, sub_id)
|
||||||
print("registering \(sub_id) handler, current: \(self.handlers.count)")
|
handlers = handlers.filter({ handler in
|
||||||
|
if handler.sub_id == sub_id {
|
||||||
|
Log.error("Duplicate handler detected for the same subscription ID. Overriding.", for: .networking)
|
||||||
|
handler.handler.finish()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
self.handlers.append(RelayHandler(sub_id: sub_id, filters: filters, to: relays, handler: handler))
|
||||||
|
Log.debug("Registering %s handler, current: %d", for: .networking, sub_id, self.handlers.count)
|
||||||
}
|
}
|
||||||
|
|
||||||
func remove_relay(_ relay_id: RelayURL) {
|
@MainActor
|
||||||
|
func remove_relay(_ relay_id: RelayURL) async {
|
||||||
var i: Int = 0
|
var i: Int = 0
|
||||||
|
|
||||||
self.disconnect(to: [relay_id])
|
await self.disconnect(to: [relay_id])
|
||||||
|
|
||||||
for relay in relays {
|
for relay in relays {
|
||||||
if relay.id == relay_id {
|
if relay.id == relay_id {
|
||||||
@@ -119,35 +168,40 @@ class RelayPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func add_relay(_ desc: RelayDescriptor) throws(RelayError) {
|
func add_relay(_ desc: RelayDescriptor) async throws(RelayError) {
|
||||||
let relay_id = desc.url
|
let relay_id = desc.url
|
||||||
if get_relay(relay_id) != nil {
|
if await get_relay(relay_id) != nil {
|
||||||
throw RelayError.RelayAlreadyExists
|
throw RelayError.RelayAlreadyExists
|
||||||
}
|
}
|
||||||
let conn = RelayConnection(url: desc.url, handleEvent: { event in
|
let conn = RelayConnection(url: desc.url, handleEvent: { event in
|
||||||
self.handle_event(relay_id: relay_id, event: event)
|
await self.handle_event(relay_id: relay_id, event: event)
|
||||||
}, processUnverifiedWSEvent: { wsev in
|
}, processUnverifiedWSEvent: { wsev in
|
||||||
guard case .message(let msg) = wsev,
|
guard case .message(let msg) = wsev,
|
||||||
case .string(let str) = msg
|
case .string(let str) = msg
|
||||||
else { return }
|
else { return }
|
||||||
|
|
||||||
let _ = self.ndb.process_event(str)
|
let _ = self.ndb?.processEvent(str, originRelayURL: relay_id)
|
||||||
self.message_received_function?((str, desc))
|
self.message_received_function?((str, desc))
|
||||||
})
|
})
|
||||||
let relay = Relay(descriptor: desc, connection: conn)
|
let relay = Relay(descriptor: desc, connection: conn)
|
||||||
|
await self.appendRelayToList(relay: relay)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func appendRelayToList(relay: Relay) {
|
||||||
self.relays.append(relay)
|
self.relays.append(relay)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setLog(_ log: RelayLog, for relay_id: RelayURL) {
|
func setLog(_ log: RelayLog, for relay_id: RelayURL) async {
|
||||||
// add the current network state to the log
|
// add the current network state to the log
|
||||||
log.add("Network state: \(network_monitor.currentPath.status)")
|
log.add("Network state: \(network_monitor.currentPath.status)")
|
||||||
|
|
||||||
get_relay(relay_id)?.connection.log = log
|
await get_relay(relay_id)?.connection.log = log
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This is used to retry dead connections
|
/// This is used to retry dead connections
|
||||||
func connect_to_disconnected() {
|
func connect_to_disconnected() async {
|
||||||
for relay in relays {
|
for relay in await relays {
|
||||||
let c = relay.connection
|
let c = relay.connection
|
||||||
|
|
||||||
let is_connecting = c.isConnecting
|
let is_connecting = c.isConnecting
|
||||||
@@ -164,38 +218,71 @@ class RelayPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func reconnect(to: [RelayURL]? = nil) {
|
func reconnect(to targetRelays: [RelayURL]? = nil) async {
|
||||||
let relays = to.map{ get_relays($0) } ?? self.relays
|
let relays = await getRelays(targetRelays: targetRelays)
|
||||||
for relay in relays {
|
for relay in relays {
|
||||||
// don't try to reconnect to broken relays
|
// don't try to reconnect to broken relays
|
||||||
relay.connection.reconnect()
|
relay.connection.reconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func connect(to: [RelayURL]? = nil) {
|
func connect(to targetRelays: [RelayURL]? = nil) async {
|
||||||
let relays = to.map{ get_relays($0) } ?? self.relays
|
let relays = await getRelays(targetRelays: targetRelays)
|
||||||
for relay in relays {
|
for relay in relays {
|
||||||
relay.connection.connect()
|
relay.connection.connect()
|
||||||
}
|
}
|
||||||
|
// Mark as open last, to prevent other classes from pulling data before the relays are actually connected
|
||||||
|
open = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func disconnect(to: [RelayURL]? = nil) {
|
func disconnect(to targetRelays: [RelayURL]? = nil) async {
|
||||||
let relays = to.map{ get_relays($0) } ?? self.relays
|
// Mark as closed first, to prevent other classes from pulling data while the relays are being disconnected
|
||||||
|
open = false
|
||||||
|
let relays = await getRelays(targetRelays: targetRelays)
|
||||||
for relay in relays {
|
for relay in relays {
|
||||||
relay.connection.disconnect()
|
relay.connection.disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func getRelays(targetRelays: [RelayURL]? = nil) -> [Relay] {
|
||||||
|
targetRelays.map{ get_relays($0) } ?? self.relays
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes queued up requests that should not persist between app sessions (i.e. when the app goes to background then back to foreground)
|
||||||
|
func cleanQueuedRequestForSessionEnd() {
|
||||||
|
request_queue = request_queue.filter { request in
|
||||||
|
guard case .typical(let typicalRequest) = request.req else { return true }
|
||||||
|
switch typicalRequest {
|
||||||
|
case .subscribe(_):
|
||||||
|
return true
|
||||||
|
case .unsubscribe(_):
|
||||||
|
return false // Do not persist unsubscribe requests to prevent them to race against subscribe requests when we come back to the foreground.
|
||||||
|
case .event(_):
|
||||||
|
return true
|
||||||
|
case .auth(_):
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func unsubscribe(sub_id: String, to: [RelayURL]? = nil) {
|
func unsubscribe(sub_id: String, to: [RelayURL]? = nil) async {
|
||||||
if to == nil {
|
if to == nil {
|
||||||
self.remove_handler(sub_id: sub_id)
|
self.remove_handler(sub_id: sub_id)
|
||||||
}
|
}
|
||||||
self.send(.unsubscribe(sub_id), to: to)
|
await self.send(.unsubscribe(sub_id), to: to)
|
||||||
}
|
}
|
||||||
|
|
||||||
func subscribe(sub_id: String, filters: [NostrFilter], handler: @escaping (RelayURL, NostrConnectionEvent) -> (), to: [RelayURL]? = nil) {
|
func subscribe(sub_id: String, filters: [NostrFilter], handler: AsyncStream<(RelayURL, NostrConnectionEvent)>.Continuation, to: [RelayURL]? = nil) {
|
||||||
register_handler(sub_id: sub_id, handler: handler)
|
Task {
|
||||||
send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to)
|
await register_handler(sub_id: sub_id, filters: filters, to: to, handler: handler)
|
||||||
|
|
||||||
|
// When the caller specifies no relays, it is implied that the user wants to use the ones in the user relay list. Skip ephemeral relays in that case.
|
||||||
|
// When the caller specifies specific relays, do not skip ephemeral relays to respect the exact list given by the caller.
|
||||||
|
let shouldSkipEphemeralRelays = to == nil ? true : false
|
||||||
|
|
||||||
|
await send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to, skip_ephemeral: shouldSkipEphemeralRelays)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Subscribes to data from the `RelayPool` based on a filter and a list of desired relays.
|
/// Subscribes to data from the `RelayPool` based on a filter and a list of desired relays.
|
||||||
@@ -203,48 +290,71 @@ class RelayPool {
|
|||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - filters: The filters specifying the desired content.
|
/// - filters: The filters specifying the desired content.
|
||||||
/// - desiredRelays: The desired relays which to subsctibe to. If `nil`, it defaults to the `RelayPool`'s default list
|
/// - desiredRelays: The desired relays which to subsctibe to. If `nil`, it defaults to the `RelayPool`'s default list
|
||||||
/// - eoseTimeout: The maximum timeout which to give up waiting for the eoseSignal, in seconds
|
/// - eoseTimeout: The maximum timeout which to give up waiting for the eoseSignal
|
||||||
/// - Returns: Returns an async stream that callers can easily consume via a for-loop
|
/// - Returns: Returns an async stream that callers can easily consume via a for-loop
|
||||||
func subscribe(filters: [NostrFilter], to desiredRelays: [RelayURL]? = nil, eoseTimeout: TimeInterval = 10) -> AsyncStream<StreamItem> {
|
func subscribe(filters: [NostrFilter], to desiredRelays: [RelayURL]? = nil, eoseTimeout: Duration? = nil, id: UUID? = nil) async -> AsyncStream<StreamItem> {
|
||||||
let desiredRelays = desiredRelays ?? self.relays.map({ $0.descriptor.url })
|
let eoseTimeout = eoseTimeout ?? .seconds(5)
|
||||||
|
let desiredRelays = await getRelays(targetRelays: desiredRelays)
|
||||||
|
let startTime = CFAbsoluteTimeGetCurrent()
|
||||||
return AsyncStream<StreamItem> { continuation in
|
return AsyncStream<StreamItem> { continuation in
|
||||||
let sub_id = UUID().uuidString
|
let id = id ?? UUID()
|
||||||
|
let sub_id = id.uuidString
|
||||||
var seenEvents: Set<NoteId> = []
|
var seenEvents: Set<NoteId> = []
|
||||||
var relaysWhoFinishedInitialResults: Set<RelayURL> = []
|
var relaysWhoFinishedInitialResults: Set<RelayURL> = []
|
||||||
var eoseSent = false
|
var eoseSent = false
|
||||||
self.subscribe(sub_id: sub_id, filters: filters, handler: { (relayUrl, connectionEvent) in
|
let upstreamStream = AsyncStream<(RelayURL, NostrConnectionEvent)> { upstreamContinuation in
|
||||||
switch connectionEvent {
|
self.subscribe(sub_id: sub_id, filters: filters, handler: upstreamContinuation, to: desiredRelays.map({ $0.descriptor.url }))
|
||||||
case .ws_connection_event(let ev):
|
}
|
||||||
// Websocket events such as connect/disconnect/error are already handled in `RelayConnection`. Do not perform any handling here.
|
let upstreamStreamingTask = Task {
|
||||||
// For the future, perhaps we should abstract away `.ws_connection_event` in `RelayPool`? Seems like something to be handled on the `RelayConnection` layer.
|
for await (relayUrl, connectionEvent) in upstreamStream {
|
||||||
break
|
try Task.checkCancellation()
|
||||||
case .nostr_event(let nostrResponse):
|
switch connectionEvent {
|
||||||
guard nostrResponse.subid == sub_id else { return } // Do not stream items that do not belong in this subscription
|
case .ws_connection_event(let ev):
|
||||||
switch nostrResponse {
|
// Websocket events such as connect/disconnect/error are already handled in `RelayConnection`. Do not perform any handling here.
|
||||||
case .event(_, let nostrEvent):
|
// For the future, perhaps we should abstract away `.ws_connection_event` in `RelayPool`? Seems like something to be handled on the `RelayConnection` layer.
|
||||||
if seenEvents.contains(nostrEvent.id) { break } // Don't send two of the same events.
|
break
|
||||||
continuation.yield(with: .success(.event(nostrEvent)))
|
case .nostr_event(let nostrResponse):
|
||||||
seenEvents.insert(nostrEvent.id)
|
guard nostrResponse.subid == sub_id else { return } // Do not stream items that do not belong in this subscription
|
||||||
case .notice(let note):
|
switch nostrResponse {
|
||||||
break // We do not support handling these yet
|
case .event(_, let nostrEvent):
|
||||||
case .eose(_):
|
if seenEvents.contains(nostrEvent.id) { break } // Don't send two of the same events.
|
||||||
relaysWhoFinishedInitialResults.insert(relayUrl)
|
continuation.yield(with: .success(.event(nostrEvent)))
|
||||||
if relaysWhoFinishedInitialResults == Set(desiredRelays) {
|
seenEvents.insert(nostrEvent.id)
|
||||||
continuation.yield(with: .success(.eose))
|
case .notice(let note):
|
||||||
eoseSent = true
|
break // We do not support handling these yet
|
||||||
|
case .eose(_):
|
||||||
|
relaysWhoFinishedInitialResults.insert(relayUrl)
|
||||||
|
let desiredAndConnectedRelays = desiredRelays.filter({ $0.connection.isConnected }).map({ $0.descriptor.url })
|
||||||
|
Log.debug("RelayPool subscription %s: EOSE from %s. EOSE count: %d/%d. Elapsed: %.2f seconds.", for: .networking, id.uuidString, relayUrl.absoluteString, relaysWhoFinishedInitialResults.count, Set(desiredAndConnectedRelays).count, CFAbsoluteTimeGetCurrent() - startTime)
|
||||||
|
if relaysWhoFinishedInitialResults == Set(desiredAndConnectedRelays) {
|
||||||
|
continuation.yield(with: .success(.eose))
|
||||||
|
eoseSent = true
|
||||||
|
}
|
||||||
|
case .ok(_): break // No need to handle this, we are not sending an event to the relay
|
||||||
|
case .auth(_): break // Handled in a separate function in RelayPool
|
||||||
}
|
}
|
||||||
case .ok(_): break // No need to handle this, we are not sending an event to the relay
|
|
||||||
case .auth(_): break // Handled in a separate function in RelayPool
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, to: desiredRelays)
|
}
|
||||||
Task {
|
let timeoutTask = Task {
|
||||||
try? await Task.sleep(nanoseconds: 1_000_000_000 * UInt64(eoseTimeout))
|
try? await Task.sleep(for: eoseTimeout)
|
||||||
if !eoseSent { continuation.yield(with: .success(.eose)) }
|
if !eoseSent { continuation.yield(with: .success(.eose)) }
|
||||||
}
|
}
|
||||||
continuation.onTermination = { @Sendable _ in
|
continuation.onTermination = { @Sendable termination in
|
||||||
self.unsubscribe(sub_id: sub_id, to: desiredRelays)
|
switch termination {
|
||||||
self.remove_handler(sub_id: sub_id)
|
case .finished:
|
||||||
|
Log.debug("RelayPool subscription %s finished. Closing.", for: .networking, sub_id)
|
||||||
|
case .cancelled:
|
||||||
|
Log.debug("RelayPool subscription %s cancelled. Closing.", for: .networking, sub_id)
|
||||||
|
@unknown default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
Task {
|
||||||
|
await self.unsubscribe(sub_id: sub_id, to: desiredRelays.map({ $0.descriptor.url }))
|
||||||
|
await self.remove_handler(sub_id: sub_id)
|
||||||
|
}
|
||||||
|
timeoutTask.cancel()
|
||||||
|
upstreamStreamingTask.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -256,9 +366,12 @@ class RelayPool {
|
|||||||
case eose
|
case eose
|
||||||
}
|
}
|
||||||
|
|
||||||
func subscribe_to(sub_id: String, filters: [NostrFilter], to: [RelayURL]?, handler: @escaping (RelayURL, NostrConnectionEvent) -> ()) {
|
func subscribe_to(sub_id: String, filters: [NostrFilter], to: [RelayURL]?, handler: AsyncStream<(RelayURL, NostrConnectionEvent)>.Continuation) {
|
||||||
register_handler(sub_id: sub_id, handler: handler)
|
Task {
|
||||||
send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to)
|
await register_handler(sub_id: sub_id, filters: filters, to: to, handler: handler)
|
||||||
|
|
||||||
|
await send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func count_queued(relay: RelayURL) -> Int {
|
func count_queued(relay: RelayURL) -> Int {
|
||||||
@@ -271,7 +384,7 @@ class RelayPool {
|
|||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func queue_req(r: NostrRequestType, relay: RelayURL, skip_ephemeral: Bool) {
|
func queue_req(r: NostrRequestType, relay: RelayURL, skip_ephemeral: Bool) {
|
||||||
let count = count_queued(relay: relay)
|
let count = count_queued(relay: relay)
|
||||||
guard count <= 10 else {
|
guard count <= 10 else {
|
||||||
@@ -288,15 +401,15 @@ class RelayPool {
|
|||||||
switch req {
|
switch req {
|
||||||
case .typical(let r):
|
case .typical(let r):
|
||||||
if case .event = r, let rstr = make_nostr_req(r) {
|
if case .event = r, let rstr = make_nostr_req(r) {
|
||||||
let _ = ndb.process_client_event(rstr)
|
let _ = ndb?.process_client_event(rstr)
|
||||||
}
|
}
|
||||||
case .custom(let string):
|
case .custom(let string):
|
||||||
let _ = ndb.process_client_event(string)
|
let _ = ndb?.process_client_event(string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func send_raw(_ req: NostrRequestType, to: [RelayURL]? = nil, skip_ephemeral: Bool = true) {
|
func send_raw(_ req: NostrRequestType, to: [RelayURL]? = nil, skip_ephemeral: Bool = true) async {
|
||||||
let relays = to.map{ get_relays($0) } ?? self.relays
|
let relays = await getRelays(targetRelays: to)
|
||||||
|
|
||||||
self.send_raw_to_local_ndb(req) // Always send Nostr events and data to NostrDB for a local copy
|
self.send_raw_to_local_ndb(req) // Always send Nostr events and data to NostrDB for a local copy
|
||||||
|
|
||||||
@@ -314,7 +427,7 @@ class RelayPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard relay.connection.isConnected else {
|
guard relay.connection.isConnected else {
|
||||||
queue_req(r: req, relay: relay.id, skip_ephemeral: skip_ephemeral)
|
Task { await queue_req(r: req, relay: relay.id, skip_ephemeral: skip_ephemeral) }
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,15 +437,17 @@ class RelayPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func send(_ req: NostrRequest, to: [RelayURL]? = nil, skip_ephemeral: Bool = true) {
|
func send(_ req: NostrRequest, to: [RelayURL]? = nil, skip_ephemeral: Bool = true) async {
|
||||||
send_raw(.typical(req), to: to, skip_ephemeral: skip_ephemeral)
|
await send_raw(.typical(req), to: to, skip_ephemeral: skip_ephemeral)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func get_relays(_ ids: [RelayURL]) -> [Relay] {
|
func get_relays(_ ids: [RelayURL]) -> [Relay] {
|
||||||
// don't include ephemeral relays in the default list to query
|
// don't include ephemeral relays in the default list to query
|
||||||
relays.filter { ids.contains($0.id) }
|
relays.filter { ids.contains($0.id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func get_relay(_ id: RelayURL) -> Relay? {
|
func get_relay(_ id: RelayURL) -> Relay? {
|
||||||
relays.first(where: { $0.id == id })
|
relays.first(where: { $0.id == id })
|
||||||
}
|
}
|
||||||
@@ -345,7 +460,7 @@ class RelayPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
print("running queueing request: \(req.req) for \(relay_id)")
|
print("running queueing request: \(req.req) for \(relay_id)")
|
||||||
self.send_raw(req.req, to: [relay_id], skip_ephemeral: false)
|
Task { await self.send_raw(req.req, to: [relay_id], skip_ephemeral: false) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,27 +476,48 @@ class RelayPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resubscribeAll(relayId: RelayURL) async {
|
||||||
|
for handler in self.handlers {
|
||||||
|
guard let filters = handler.filters else { continue }
|
||||||
|
// When the caller specifies no relays, it is implied that the user wants to use the ones in the user relay list. Skip ephemeral relays in that case.
|
||||||
|
// When the caller specifies specific relays, do not skip ephemeral relays to respect the exact list given by the caller.
|
||||||
|
let shouldSkipEphemeralRelays = handler.to == nil ? true : false
|
||||||
|
|
||||||
|
if let handlerTargetRelays = handler.to,
|
||||||
|
!handlerTargetRelays.contains(where: { $0 == relayId }) {
|
||||||
|
// Not part of the target relays, skip
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.debug("%s: Sending resubscribe request to %s", for: .networking, handler.sub_id, relayId.absoluteString)
|
||||||
|
await send(.subscribe(.init(filters: filters, sub_id: handler.sub_id)), to: [relayId], skip_ephemeral: shouldSkipEphemeralRelays)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func handle_event(relay_id: RelayURL, event: NostrConnectionEvent) {
|
func handle_event(relay_id: RelayURL, event: NostrConnectionEvent) async {
|
||||||
record_seen(relay_id: relay_id, event: event)
|
record_seen(relay_id: relay_id, event: event)
|
||||||
|
|
||||||
// run req queue when we reconnect
|
// When we reconnect, do two things
|
||||||
|
// - Send messages that were stored in the queue
|
||||||
|
// - Re-subscribe to filters we had subscribed before
|
||||||
if case .ws_connection_event(let ws) = event {
|
if case .ws_connection_event(let ws) = event {
|
||||||
if case .connected = ws {
|
if case .connected = ws {
|
||||||
run_queue(relay_id)
|
run_queue(relay_id)
|
||||||
|
await self.resubscribeAll(relayId: relay_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle auth
|
// Handle auth
|
||||||
if case let .nostr_event(nostrResponse) = event,
|
if case let .nostr_event(nostrResponse) = event,
|
||||||
case let .auth(challenge_string) = nostrResponse {
|
case let .auth(challenge_string) = nostrResponse {
|
||||||
if let relay = get_relay(relay_id) {
|
if let relay = await get_relay(relay_id) {
|
||||||
print("received auth request from \(relay.descriptor.url.id)")
|
print("received auth request from \(relay.descriptor.url.id)")
|
||||||
relay.authentication_state = .pending
|
relay.authentication_state = .pending
|
||||||
if let keypair {
|
if let keypair {
|
||||||
if let fullKeypair = keypair.to_full() {
|
if let fullKeypair = keypair.to_full() {
|
||||||
if let authRequest = make_auth_request(keypair: fullKeypair, challenge_string: challenge_string, relay: relay) {
|
if let authRequest = make_auth_request(keypair: fullKeypair, challenge_string: challenge_string, relay: relay) {
|
||||||
send(.auth(authRequest), to: [relay_id], skip_ephemeral: false)
|
await send(.auth(authRequest), to: [relay_id], skip_ephemeral: false)
|
||||||
relay.authentication_state = .verified
|
relay.authentication_state = .verified
|
||||||
} else {
|
} else {
|
||||||
print("failed to make auth request")
|
print("failed to make auth request")
|
||||||
@@ -400,13 +536,25 @@ class RelayPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for handler in handlers {
|
for handler in handlers {
|
||||||
handler.callback(relay_id, event)
|
// We send data to the handlers if:
|
||||||
|
// - the subscription ID matches, or
|
||||||
|
// - the handler filters is `nil`, which is used in some cases as a blanket "give me all notes" (e.g. during signup)
|
||||||
|
guard handler.sub_id == event.subId || handler.filters == nil else { continue }
|
||||||
|
logStreamPipelineStats("RelayPool_\(relay_id.absoluteString)", "RelayPool_Handler_\(handler.sub_id)")
|
||||||
|
handler.handler.yield((relay_id, event))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func add_rw_relay(_ pool: RelayPool, _ url: RelayURL) {
|
func add_rw_relay(_ pool: RelayPool, _ url: RelayURL) async {
|
||||||
try? pool.add_relay(RelayPool.RelayDescriptor(url: url, info: .readWrite))
|
try? await pool.add_relay(RelayPool.RelayDescriptor(url: url, info: .readWrite))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extension RelayPool {
|
||||||
|
protocol Delegate {
|
||||||
|
func latestRelayListChanged(_ newEvent: NdbNote)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct RelayURL: Hashable, Equatable, Codable, CodingKeyRepresentable, Identifiable, Comparable, CustomStringConvertible {
|
public struct RelayURL: Hashable, Equatable, Codable, CodingKeyRepresentable, Identifiable, Comparable, CustomStringConvertible, Sendable {
|
||||||
private(set) var url: URL
|
private(set) var url: URL
|
||||||
|
|
||||||
public var id: URL {
|
public var id: URL {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import Foundation
|
|||||||
import LinkPresentation
|
import LinkPresentation
|
||||||
import EmojiPicker
|
import EmojiPicker
|
||||||
|
|
||||||
class DamusState: HeadlessDamusState {
|
class DamusState: HeadlessDamusState, ObservableObject {
|
||||||
let keypair: Keypair
|
let keypair: Keypair
|
||||||
let likes: EventCounter
|
let likes: EventCounter
|
||||||
let boosts: EventCounter
|
let boosts: EventCounter
|
||||||
@@ -40,7 +40,7 @@ class DamusState: HeadlessDamusState {
|
|||||||
let favicon_cache: FaviconCache
|
let favicon_cache: FaviconCache
|
||||||
private(set) var nostrNetwork: NostrNetworkManager
|
private(set) var nostrNetwork: NostrNetworkManager
|
||||||
|
|
||||||
init(keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, contactCards: ContactCard, 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, replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider, favicon_cache: FaviconCache) {
|
init(keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, contactCards: ContactCard, 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, replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider, favicon_cache: FaviconCache, addNdbToRelayPool: Bool = true) {
|
||||||
self.keypair = keypair
|
self.keypair = keypair
|
||||||
self.likes = likes
|
self.likes = likes
|
||||||
self.boosts = boosts
|
self.boosts = boosts
|
||||||
@@ -74,7 +74,9 @@ class DamusState: HeadlessDamusState {
|
|||||||
self.favicon_cache = FaviconCache()
|
self.favicon_cache = FaviconCache()
|
||||||
|
|
||||||
let networkManagerDelegate = NostrNetworkManagerDelegate(settings: settings, contacts: contacts, ndb: ndb, keypair: keypair, relayModelCache: relay_model_cache, relayFilters: relay_filters)
|
let networkManagerDelegate = NostrNetworkManagerDelegate(settings: settings, contacts: contacts, ndb: ndb, keypair: keypair, relayModelCache: relay_model_cache, relayFilters: relay_filters)
|
||||||
self.nostrNetwork = NostrNetworkManager(delegate: networkManagerDelegate)
|
let nostrNetwork = NostrNetworkManager(delegate: networkManagerDelegate, addNdbToRelayPool: addNdbToRelayPool)
|
||||||
|
self.nostrNetwork = nostrNetwork
|
||||||
|
self.wallet.nostrNetwork = nostrNetwork
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -125,7 +127,7 @@ class DamusState: HeadlessDamusState {
|
|||||||
events: EventCache(ndb: ndb),
|
events: EventCache(ndb: ndb),
|
||||||
bookmarks: BookmarksManager(pubkey: pubkey),
|
bookmarks: BookmarksManager(pubkey: pubkey),
|
||||||
replies: ReplyCounter(our_pubkey: pubkey),
|
replies: ReplyCounter(our_pubkey: pubkey),
|
||||||
wallet: WalletModel(settings: settings),
|
wallet: WalletModel(settings: settings), // nostrNetwork is connected after initialization
|
||||||
nav: navigationCoordinator,
|
nav: navigationCoordinator,
|
||||||
music: MusicController(onChange: { _ in }),
|
music: MusicController(onChange: { _ in }),
|
||||||
video: DamusVideoCoordinator(),
|
video: DamusVideoCoordinator(),
|
||||||
@@ -167,8 +169,10 @@ class DamusState: HeadlessDamusState {
|
|||||||
try await self.push_notification_client.revoke_token()
|
try await self.push_notification_client.revoke_token()
|
||||||
}
|
}
|
||||||
wallet.disconnect()
|
wallet.disconnect()
|
||||||
nostrNetwork.pool.close()
|
Task {
|
||||||
ndb.close()
|
await nostrNetwork.close() // Close ndb streaming tasks before closing ndb to avoid memory errors
|
||||||
|
ndb.close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static var empty: DamusState {
|
static var empty: DamusState {
|
||||||
@@ -223,6 +227,7 @@ fileprivate extension DamusState {
|
|||||||
var latestContactListEvent: NostrEvent? { self.contacts.event }
|
var latestContactListEvent: NostrEvent? { self.contacts.event }
|
||||||
var bootstrapRelays: [RelayURL] { get_default_bootstrap_relays() }
|
var bootstrapRelays: [RelayURL] { get_default_bootstrap_relays() }
|
||||||
var developerMode: Bool { self.settings.developer_mode }
|
var developerMode: Bool { self.settings.developer_mode }
|
||||||
|
var experimentalLocalRelayModelSupport: Bool { self.settings.enable_experimental_local_relay_model }
|
||||||
var relayModelCache: RelayModelCache
|
var relayModelCache: RelayModelCache
|
||||||
var relayFilters: RelayFilters
|
var relayFilters: RelayFilters
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ class ActionBarModel: ObservableObject {
|
|||||||
self.relays = relays
|
self.relays = relays
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(damus: DamusState, evid: NoteId) {
|
@MainActor
|
||||||
|
func update(damus: DamusState, evid: NoteId) async {
|
||||||
self.likes = damus.likes.counts[evid] ?? 0
|
self.likes = damus.likes.counts[evid] ?? 0
|
||||||
self.boosts = damus.boosts.counts[evid] ?? 0
|
self.boosts = damus.boosts.counts[evid] ?? 0
|
||||||
self.zaps = damus.zaps.event_counts[evid] ?? 0
|
self.zaps = damus.zaps.event_counts[evid] ?? 0
|
||||||
@@ -58,7 +59,7 @@ class ActionBarModel: ObservableObject {
|
|||||||
self.our_zap = damus.zaps.our_zaps[evid]?.first
|
self.our_zap = damus.zaps.our_zaps[evid]?.first
|
||||||
self.our_reply = damus.replies.our_reply(evid)
|
self.our_reply = damus.replies.our_reply(evid)
|
||||||
self.our_quote_repost = damus.quote_reposts.our_events[evid]
|
self.our_quote_repost = damus.quote_reposts.our_events[evid]
|
||||||
self.relays = (damus.nostrNetwork.pool.seen[evid] ?? []).count
|
self.relays = (await damus.nostrNetwork.relayURLsThatSawNote(id: evid) ?? []).count
|
||||||
self.objectWillChange.send()
|
self.objectWillChange.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,10 +36,17 @@ struct EventActionBar: View {
|
|||||||
self.swipe_context = swipe_context
|
self.swipe_context = swipe_context
|
||||||
}
|
}
|
||||||
|
|
||||||
var lnurl: String? {
|
@State var lnurl: String? = nil
|
||||||
damus_state.profiles.lookup_with_timestamp(event.pubkey)?.map({ pr in
|
|
||||||
|
// Fetching an LNURL is expensive enough that it can cause a hitch. Use a special backgroundable function to fetch the value.
|
||||||
|
// Fetch on `.onAppear`
|
||||||
|
nonisolated func fetchLNURL() {
|
||||||
|
let lnurl = damus_state.profiles.lookup_with_timestamp(event.pubkey)?.map({ pr in
|
||||||
pr?.lnurl
|
pr?.lnurl
|
||||||
}).value
|
}).value
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.lnurl = lnurl
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var show_like: Bool {
|
var show_like: Bool {
|
||||||
@@ -82,8 +89,10 @@ struct EventActionBar: View {
|
|||||||
|
|
||||||
var like_swipe_button: some View {
|
var like_swipe_button: some View {
|
||||||
SwipeAction(image: "shaka", backgroundColor: DamusColors.adaptableGrey) {
|
SwipeAction(image: "shaka", backgroundColor: DamusColors.adaptableGrey) {
|
||||||
send_like(emoji: damus_state.settings.default_emoji_reaction)
|
Task {
|
||||||
self.swipe_context?.state.wrappedValue = .closed
|
await send_like(emoji: damus_state.settings.default_emoji_reaction)
|
||||||
|
self.swipe_context?.state.wrappedValue = .closed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.swipeButtonStyle()
|
.swipeButtonStyle()
|
||||||
.accessibilityLabel(NSLocalizedString("React with default reaction emoji", comment: "Accessibility label for react button"))
|
.accessibilityLabel(NSLocalizedString("React with default reaction emoji", comment: "Accessibility label for react button"))
|
||||||
@@ -131,7 +140,7 @@ struct EventActionBar: View {
|
|||||||
if bar.liked {
|
if bar.liked {
|
||||||
//notify(.delete, bar.our_like)
|
//notify(.delete, bar.our_like)
|
||||||
} else {
|
} else {
|
||||||
send_like(emoji: emoji)
|
Task { await send_like(emoji: emoji) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,8 +227,15 @@ struct EventActionBar: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var event_relay_url_strings: [RelayURL] {
|
@State var event_relay_url_strings: [RelayURL] = []
|
||||||
let relays = damus_state.nostrNetwork.relaysForEvent(event: event)
|
|
||||||
|
func updateEventRelayURLStrings() async {
|
||||||
|
let newValue = await fetchEventRelayURLStrings()
|
||||||
|
self.event_relay_url_strings = newValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchEventRelayURLStrings() async -> [RelayURL] {
|
||||||
|
let relays = await damus_state.nostrNetwork.relaysForEvent(event: event)
|
||||||
if !relays.isEmpty {
|
if !relays.isEmpty {
|
||||||
return relays.prefix(Constants.MAX_SHARE_RELAYS).map { $0 }
|
return relays.prefix(Constants.MAX_SHARE_RELAYS).map { $0 }
|
||||||
}
|
}
|
||||||
@@ -230,7 +246,11 @@ struct EventActionBar: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
self.content
|
self.content
|
||||||
.onAppear {
|
.onAppear {
|
||||||
self.bar.update(damus: damus_state, evid: self.event.id)
|
Task.detached(priority: .background, operation: {
|
||||||
|
await self.bar.update(damus: damus_state, evid: self.event.id)
|
||||||
|
self.fetchLNURL()
|
||||||
|
await self.updateEventRelayURLStrings()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $show_share_action, onDismiss: { self.show_share_action = false }) {
|
.sheet(isPresented: $show_share_action, onDismiss: { self.show_share_action = false }) {
|
||||||
if #available(iOS 16.0, *) {
|
if #available(iOS 16.0, *) {
|
||||||
@@ -258,7 +278,10 @@ struct EventActionBar: View {
|
|||||||
}
|
}
|
||||||
.onReceive(handle_notify(.update_stats)) { target in
|
.onReceive(handle_notify(.update_stats)) { target in
|
||||||
guard target == self.event.id else { return }
|
guard target == self.event.id else { return }
|
||||||
self.bar.update(damus: self.damus_state, evid: target)
|
Task {
|
||||||
|
await self.bar.update(damus: self.damus_state, evid: target)
|
||||||
|
await self.updateEventRelayURLStrings()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.liked)) { liked in
|
.onReceive(handle_notify(.liked)) { liked in
|
||||||
if liked.id != event.id {
|
if liked.id != event.id {
|
||||||
@@ -271,9 +294,9 @@ struct EventActionBar: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func send_like(emoji: String) {
|
func send_like(emoji: String) async {
|
||||||
guard let keypair = damus_state.keypair.to_full(),
|
guard let keypair = damus_state.keypair.to_full(),
|
||||||
let like_ev = make_like_event(keypair: keypair, liked: event, content: emoji, relayURL: damus_state.nostrNetwork.relaysForEvent(event: event).first) else {
|
let like_ev = await make_like_event(keypair: keypair, liked: event, content: emoji, relayURL: damus_state.nostrNetwork.relaysForEvent(event: event).first) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,7 +304,7 @@ struct EventActionBar: View {
|
|||||||
|
|
||||||
generator.impactOccurred()
|
generator.impactOccurred()
|
||||||
|
|
||||||
damus_state.nostrNetwork.postbox.send(like_ev)
|
await damus_state.nostrNetwork.postbox.send(like_ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Helper structures
|
// MARK: Helper structures
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ struct EventDetailBar: View {
|
|||||||
let target_pk: Pubkey
|
let target_pk: Pubkey
|
||||||
|
|
||||||
@ObservedObject var bar: ActionBarModel
|
@ObservedObject var bar: ActionBarModel
|
||||||
|
@State var relays: [RelayURL] = []
|
||||||
|
|
||||||
init(state: DamusState, target: NoteId, target_pk: Pubkey) {
|
init(state: DamusState, target: NoteId, target_pk: Pubkey) {
|
||||||
self.state = state
|
self.state = state
|
||||||
@@ -61,7 +62,6 @@ struct EventDetailBar: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if bar.relays > 0 {
|
if bar.relays > 0 {
|
||||||
let relays = Array(state.nostrNetwork.pool.seen[target] ?? [])
|
|
||||||
NavigationLink(value: Route.UserRelays(relays: relays)) {
|
NavigationLink(value: Route.UserRelays(relays: relays)) {
|
||||||
let nounString = pluralizedString(key: "relays_count", count: bar.relays)
|
let nounString = pluralizedString(key: "relays_count", count: bar.relays)
|
||||||
let noun = Text(nounString).foregroundColor(.gray)
|
let noun = Text(nounString).foregroundColor(.gray)
|
||||||
@@ -70,6 +70,18 @@ struct EventDetailBar: View {
|
|||||||
.buttonStyle(PlainButtonStyle())
|
.buttonStyle(PlainButtonStyle())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
|
Task { await self.updateSeenRelays() }
|
||||||
|
}
|
||||||
|
.onReceive(handle_notify(.update_stats)) { noteId in
|
||||||
|
guard noteId == target else { return }
|
||||||
|
Task { await self.updateSeenRelays() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSeenRelays() async {
|
||||||
|
let relays = await Array(state.nostrNetwork.relayURLsThatSawNote(id: target) ?? [])
|
||||||
|
self.relays = relays
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,8 +27,15 @@ struct ShareAction: View {
|
|||||||
self._show_share = show_share
|
self._show_share = show_share
|
||||||
}
|
}
|
||||||
|
|
||||||
var event_relay_url_strings: [RelayURL] {
|
@State var event_relay_url_strings: [RelayURL] = []
|
||||||
let relays = userProfile.damus.nostrNetwork.relaysForEvent(event: event)
|
|
||||||
|
func updateEventRelayURLStrings() async {
|
||||||
|
let newValue = await fetchEventRelayURLStrings()
|
||||||
|
self.event_relay_url_strings = newValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchEventRelayURLStrings() async -> [RelayURL] {
|
||||||
|
let relays = await userProfile.damus.nostrNetwork.relaysForEvent(event: event)
|
||||||
if !relays.isEmpty {
|
if !relays.isEmpty {
|
||||||
return relays.prefix(Constants.MAX_SHARE_RELAYS).map { $0 }
|
return relays.prefix(Constants.MAX_SHARE_RELAYS).map { $0 }
|
||||||
}
|
}
|
||||||
@@ -80,8 +87,13 @@ struct ShareAction: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onReceive(handle_notify(.update_stats), perform: { noteId in
|
||||||
|
guard noteId == event.id else { return }
|
||||||
|
Task { await self.updateEventRelayURLStrings() }
|
||||||
|
})
|
||||||
.onAppear() {
|
.onAppear() {
|
||||||
userProfile.subscribeToFindRelays()
|
userProfile.subscribeToFindRelays()
|
||||||
|
Task { await self.updateEventRelayURLStrings() }
|
||||||
}
|
}
|
||||||
.onDisappear() {
|
.onDisappear() {
|
||||||
userProfile.unsubscribeFindRelays()
|
userProfile.unsubscribeFindRelays()
|
||||||
|
|||||||
@@ -57,13 +57,13 @@ struct ReportView: View {
|
|||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
|
||||||
func do_send_report() {
|
func do_send_report() async {
|
||||||
guard let selected_report_type,
|
guard let selected_report_type,
|
||||||
let ev = NostrEvent(content: report_message, keypair: keypair.to_keypair(), kind: 1984, tags: target.reportTags(type: selected_report_type)) else {
|
let ev = NostrEvent(content: report_message, keypair: keypair.to_keypair(), kind: 1984, tags: target.reportTags(type: selected_report_type)) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
postbox.send(ev)
|
await postbox.send(ev)
|
||||||
|
|
||||||
report_sent = true
|
report_sent = true
|
||||||
report_id = bech32_note_id(ev.id)
|
report_id = bech32_note_id(ev.id)
|
||||||
@@ -116,7 +116,7 @@ struct ReportView: View {
|
|||||||
|
|
||||||
Section(content: {
|
Section(content: {
|
||||||
Button(send_report_button_text) {
|
Button(send_report_button_text) {
|
||||||
do_send_report()
|
Task { await do_send_report() }
|
||||||
}
|
}
|
||||||
.disabled(selected_report_type == nil)
|
.disabled(selected_report_type == nil)
|
||||||
}, footer: {
|
}, footer: {
|
||||||
|
|||||||
@@ -19,13 +19,15 @@ struct RepostAction: View {
|
|||||||
|
|
||||||
Button {
|
Button {
|
||||||
dismiss()
|
dismiss()
|
||||||
|
|
||||||
guard let keypair = self.damus_state.keypair.to_full(),
|
Task {
|
||||||
let boost = make_boost_event(keypair: keypair, boosted: self.event, relayURL: damus_state.nostrNetwork.relaysForEvent(event: self.event).first) else {
|
guard let keypair = self.damus_state.keypair.to_full(),
|
||||||
return
|
let boost = await make_boost_event(keypair: keypair, boosted: self.event, relayURL: damus_state.nostrNetwork.relaysForEvent(event: self.event).first) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await damus_state.nostrNetwork.postbox.send(boost)
|
||||||
}
|
}
|
||||||
|
|
||||||
damus_state.nostrNetwork.postbox.send(boost)
|
|
||||||
} label: {
|
} label: {
|
||||||
Label(NSLocalizedString("Repost", comment: "Button to repost a note"), image: "repost")
|
Label(NSLocalizedString("Repost", comment: "Button to repost a note"), image: "repost")
|
||||||
.frame(maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .leading)
|
.frame(maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .leading)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ struct Reposted: View {
|
|||||||
|
|
||||||
// Show profile picture of the reposter only if the reposter is not the author of the reposted note.
|
// Show profile picture of the reposter only if the reposter is not the author of the reposted note.
|
||||||
if pubkey != target.pubkey {
|
if pubkey != target.pubkey {
|
||||||
ProfilePicView(pubkey: pubkey, size: eventview_pfp_size(.small), highlight: .none, profiles: damus.profiles, disable_animation: damus.settings.disable_animation)
|
ProfilePicView(pubkey: pubkey, size: eventview_pfp_size(.small), highlight: .none, profiles: damus.profiles, disable_animation: damus.settings.disable_animation, damusState: damus)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
show_profile_action_sheet_if_enabled(damus_state: damus, pubkey: pubkey)
|
show_profile_action_sheet_if_enabled(damus_state: damus, pubkey: pubkey)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ struct ChatEventView: View {
|
|||||||
|
|
||||||
var profile_picture_view: some View {
|
var profile_picture_view: some View {
|
||||||
VStack {
|
VStack {
|
||||||
ProfilePicView(pubkey: event.pubkey, size: 32, highlight: .none, profiles: damus_state.profiles, disable_animation: disable_animation)
|
ProfilePicView(pubkey: event.pubkey, size: 32, highlight: .none, profiles: damus_state.profiles, disable_animation: disable_animation, damusState: damus_state)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
show_profile_action_sheet_if_enabled(damus_state: damus_state, pubkey: event.pubkey)
|
show_profile_action_sheet_if_enabled(damus_state: damus_state, pubkey: event.pubkey)
|
||||||
}
|
}
|
||||||
@@ -197,8 +197,10 @@ struct ChatEventView: View {
|
|||||||
}
|
}
|
||||||
.onChange(of: selected_emoji) { newSelectedEmoji in
|
.onChange(of: selected_emoji) { newSelectedEmoji in
|
||||||
if let newSelectedEmoji {
|
if let newSelectedEmoji {
|
||||||
send_like(emoji: newSelectedEmoji.value)
|
Task {
|
||||||
popover_state = .closed
|
await send_like(emoji: newSelectedEmoji.value)
|
||||||
|
popover_state = .closed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,9 +235,9 @@ struct ChatEventView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func send_like(emoji: String) {
|
func send_like(emoji: String) async {
|
||||||
guard let keypair = damus_state.keypair.to_full(),
|
guard let keypair = damus_state.keypair.to_full(),
|
||||||
let like_ev = make_like_event(keypair: keypair, liked: event, content: emoji, relayURL: damus_state.nostrNetwork.relaysForEvent(event: event).first) else {
|
let like_ev = make_like_event(keypair: keypair, liked: event, content: emoji, relayURL: await damus_state.nostrNetwork.relaysForEvent(event: event).first) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +246,7 @@ struct ChatEventView: View {
|
|||||||
let generator = UIImpactFeedbackGenerator(style: .medium)
|
let generator = UIImpactFeedbackGenerator(style: .medium)
|
||||||
generator.impactOccurred()
|
generator.impactOccurred()
|
||||||
|
|
||||||
damus_state.nostrNetwork.postbox.send(like_ev)
|
await damus_state.nostrNetwork.postbox.send(like_ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
var action_bar: some View {
|
var action_bar: some View {
|
||||||
|
|||||||
@@ -56,12 +56,7 @@ class ThreadModel: ObservableObject {
|
|||||||
/// The damus state, needed to access the relay pool and load the thread events
|
/// The damus state, needed to access the relay pool and load the thread events
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
|
|
||||||
private let profiles_subid = UUID().description
|
private var listener: Task<Void, Never>?
|
||||||
private let base_subid = UUID().description
|
|
||||||
private let meta_subid = UUID().description
|
|
||||||
private var subids: [String] {
|
|
||||||
return [profiles_subid, base_subid, meta_subid]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: Initialization
|
// MARK: Initialization
|
||||||
@@ -86,17 +81,6 @@ class ThreadModel: ObservableObject {
|
|||||||
|
|
||||||
// MARK: Relay pool subscription management
|
// MARK: Relay pool subscription management
|
||||||
|
|
||||||
/// Unsubscribe from events in the relay pool. Call this when unloading the view
|
|
||||||
func unsubscribe() {
|
|
||||||
self.damus_state.nostrNetwork.pool.remove_handler(sub_id: base_subid)
|
|
||||||
self.damus_state.nostrNetwork.pool.remove_handler(sub_id: meta_subid)
|
|
||||||
self.damus_state.nostrNetwork.pool.remove_handler(sub_id: profiles_subid)
|
|
||||||
self.damus_state.nostrNetwork.pool.unsubscribe(sub_id: base_subid)
|
|
||||||
self.damus_state.nostrNetwork.pool.unsubscribe(sub_id: meta_subid)
|
|
||||||
self.damus_state.nostrNetwork.pool.unsubscribe(sub_id: profiles_subid)
|
|
||||||
Log.info("unsubscribing to thread %s with sub_id %s", for: .render, original_event.id.hex(), base_subid)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Subscribe to events in this thread. Call this when loading the view.
|
/// Subscribe to events in this thread. Call this when loading the view.
|
||||||
func subscribe() {
|
func subscribe() {
|
||||||
var meta_events = NostrFilter()
|
var meta_events = NostrFilter()
|
||||||
@@ -127,10 +111,19 @@ class ThreadModel: ObservableObject {
|
|||||||
|
|
||||||
let base_filters = [event_filter, ref_events]
|
let base_filters = [event_filter, ref_events]
|
||||||
let meta_filters = [meta_events, quote_events]
|
let meta_filters = [meta_events, quote_events]
|
||||||
|
|
||||||
Log.info("subscribing to thread %s with sub_id %s", for: .render, original_event.id.hex(), base_subid)
|
self.listener?.cancel()
|
||||||
damus_state.nostrNetwork.pool.subscribe(sub_id: base_subid, filters: base_filters, handler: handle_event)
|
self.listener = Task {
|
||||||
damus_state.nostrNetwork.pool.subscribe(sub_id: meta_subid, filters: meta_filters, handler: handle_event)
|
Log.info("subscribing to thread %s ", for: .render, original_event.id.hex())
|
||||||
|
for await event in damus_state.nostrNetwork.reader.streamIndefinitely(filters: base_filters + meta_filters) {
|
||||||
|
event.justUseACopy({ handle_event(ev: $0) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unsubscribe() {
|
||||||
|
self.listener?.cancel()
|
||||||
|
self.listener = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds an event to this thread.
|
/// Adds an event to this thread.
|
||||||
@@ -175,34 +168,25 @@ class ThreadModel: ObservableObject {
|
|||||||
///
|
///
|
||||||
/// Marked as private because it is this class' responsibility to load events, not the view's. Simplify the interface
|
/// Marked as private because it is this class' responsibility to load events, not the view's. Simplify the interface
|
||||||
@MainActor
|
@MainActor
|
||||||
private func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
private func handle_event(ev: NostrEvent) {
|
||||||
let (sub_id, done) = handle_subid_event(pool: damus_state.nostrNetwork.pool, relay_id: relay_id, ev: ev) { sid, ev in
|
if ev.known_kind == .zap {
|
||||||
guard subids.contains(sid) else {
|
process_zap_event(state: damus_state, ev: ev) { zap in
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
} else if ev.is_textlike {
|
||||||
if ev.known_kind == .zap {
|
// handle thread quote reposts, we just count them instead of
|
||||||
process_zap_event(state: damus_state, ev: ev) { zap in
|
// adding them to the thread
|
||||||
|
if let target = ev.is_quote_repost, target == self.selected_event.id {
|
||||||
}
|
//let _ = self.damus_state.quote_reposts.add_event(ev, target: target)
|
||||||
} else if ev.is_textlike {
|
} else {
|
||||||
// handle thread quote reposts, we just count them instead of
|
self.add_event(ev, keypair: damus_state.keypair)
|
||||||
// adding them to the thread
|
|
||||||
if let target = ev.is_quote_repost, target == self.selected_event.id {
|
|
||||||
//let _ = self.damus_state.quote_reposts.add_event(ev, target: target)
|
|
||||||
} else {
|
|
||||||
self.add_event(ev, keypair: damus_state.keypair)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if ev.known_kind == .boost {
|
||||||
guard done, let sub_id, subids.contains(sub_id) else {
|
damus_state.boosts.add_event(ev, target: original_event.id)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
else if ev.known_kind == .like {
|
||||||
if sub_id == self.base_subid {
|
damus_state.likes.add_event(ev, target: original_event.id)
|
||||||
guard let txn = NdbTxn(ndb: damus_state.ndb) else { return }
|
|
||||||
load_profiles(context: "thread", profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(Array(event_map.events)), damus_state: damus_state, txn: txn)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,12 +15,18 @@ struct ReplyQuoteView: View {
|
|||||||
@ObservedObject var thread: ThreadModel
|
@ObservedObject var thread: ThreadModel
|
||||||
let options: EventViewOptions
|
let options: EventViewOptions
|
||||||
|
|
||||||
|
@State var can_show_event = true
|
||||||
|
|
||||||
|
func update_should_show_event(event: NdbNote) async {
|
||||||
|
self.can_show_event = await should_show_event(event: event, damus_state: state)
|
||||||
|
}
|
||||||
|
|
||||||
func content(event: NdbNote) -> some View {
|
func content(event: NdbNote) -> some View {
|
||||||
ZStack(alignment: .leading) {
|
ZStack(alignment: .leading) {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
if should_show_event(event: event, damus_state: state) {
|
if can_show_event {
|
||||||
ProfilePicView(pubkey: event.pubkey, size: 14, highlight: .reply, profiles: state.profiles, disable_animation: false)
|
ProfilePicView(pubkey: event.pubkey, size: 14, highlight: .reply, profiles: state.profiles, disable_animation: false, damusState: state)
|
||||||
let blur_images = should_blur_images(settings: state.settings, contacts: state.contacts, ev: event, our_pubkey: state.pubkey)
|
let blur_images = should_blur_images(settings: state.settings, contacts: state.contacts, ev: event, our_pubkey: state.pubkey)
|
||||||
NoteContentView(damus_state: state, event: event, blur_images: blur_images, size: .small, options: options)
|
NoteContentView(damus_state: state, event: event, blur_images: blur_images, size: .small, options: options)
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
@@ -56,6 +62,9 @@ struct ReplyQuoteView: View {
|
|||||||
Group {
|
Group {
|
||||||
if let event = state.events.lookup(event_id) {
|
if let event = state.events.lookup(event_id) {
|
||||||
self.content(event: event)
|
self.content(event: event)
|
||||||
|
.onAppear {
|
||||||
|
Task { await self.update_should_show_event(event: event) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ class ContactCardManager: ContactCard {
|
|||||||
favorites.remove(target)
|
favorites.remove(target)
|
||||||
}
|
}
|
||||||
|
|
||||||
postbox.send(ev)
|
Task { await postbox.send(ev) }
|
||||||
latestContactCardEvents[target] = ev
|
latestContactCardEvents[target] = ev
|
||||||
notify(.favoriteUpdated())
|
notify(.favoriteUpdated())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ struct DMChatView: View, KeyboardReadable {
|
|||||||
var Header: some View {
|
var Header: some View {
|
||||||
return NavigationLink(value: Route.ProfileByKey(pubkey: pubkey)) {
|
return NavigationLink(value: Route.ProfileByKey(pubkey: pubkey)) {
|
||||||
HStack {
|
HStack {
|
||||||
ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation, damusState: damus_state)
|
||||||
|
|
||||||
ProfileName(pubkey: pubkey, damus: damus_state)
|
ProfileName(pubkey: pubkey, damus: damus_state)
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,7 @@ struct DMChatView: View, KeyboardReadable {
|
|||||||
Button(
|
Button(
|
||||||
role: .none,
|
role: .none,
|
||||||
action: {
|
action: {
|
||||||
send_message()
|
Task { await send_message() }
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Label("", image: "send")
|
Label("", image: "send")
|
||||||
@@ -124,7 +124,7 @@ struct DMChatView: View, KeyboardReadable {
|
|||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
func send_message() {
|
func send_message() async {
|
||||||
let tags = [["p", pubkey.hex()]]
|
let tags = [["p", pubkey.hex()]]
|
||||||
guard let post_blocks = parse_post_blocks(content: dms.draft)?.blocks else {
|
guard let post_blocks = parse_post_blocks(content: dms.draft)?.blocks else {
|
||||||
return
|
return
|
||||||
@@ -138,7 +138,7 @@ struct DMChatView: View, KeyboardReadable {
|
|||||||
|
|
||||||
dms.draft = ""
|
dms.draft = ""
|
||||||
|
|
||||||
damus_state.nostrNetwork.postbox.send(dm)
|
await damus_state.nostrNetwork.postbox.send(dm)
|
||||||
|
|
||||||
handle_incoming_dm(ev: dm, our_pubkey: damus_state.pubkey, dms: damus_state.dms, prev_events: NewEventsBits())
|
handle_incoming_dm(ev: dm, our_pubkey: damus_state.pubkey, dms: damus_state.dms, prev_events: NewEventsBits())
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ struct EventLoaderView<Content: View>: View {
|
|||||||
let event_id: NoteId
|
let event_id: NoteId
|
||||||
@State var event: NostrEvent?
|
@State var event: NostrEvent?
|
||||||
@State var subscription_uuid: String = UUID().description
|
@State var subscription_uuid: String = UUID().description
|
||||||
|
@State var loadingTask: Task<Void, Never>? = nil
|
||||||
let content: (NostrEvent) -> Content
|
let content: (NostrEvent) -> Content
|
||||||
|
|
||||||
init(damus_state: DamusState, event_id: NoteId, @ViewBuilder content: @escaping (NostrEvent) -> Content) {
|
init(damus_state: DamusState, event_id: NoteId, @ViewBuilder content: @escaping (NostrEvent) -> Content) {
|
||||||
@@ -24,40 +25,19 @@ struct EventLoaderView<Content: View>: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func unsubscribe() {
|
func unsubscribe() {
|
||||||
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subscription_uuid)
|
self.loadingTask?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
func subscribe(filters: [NostrFilter]) {
|
func subscribe() {
|
||||||
damus_state.nostrNetwork.pool.register_handler(sub_id: subscription_uuid, handler: handle_event)
|
self.loadingTask?.cancel()
|
||||||
damus_state.nostrNetwork.pool.send(.subscribe(.init(filters: filters, sub_id: subscription_uuid)))
|
self.loadingTask = Task {
|
||||||
}
|
let lender = try? await damus_state.nostrNetwork.reader.lookup(noteId: self.event_id)
|
||||||
|
lender?.justUseACopy({ event = $0 })
|
||||||
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
|
||||||
guard case .nostr_event(let nostr_response) = ev else {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
guard case .event(let id, let nostr_event) = nostr_response else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard id == subscription_uuid else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if event != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
event = nostr_event
|
|
||||||
|
|
||||||
unsubscribe()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func load() {
|
func load() {
|
||||||
subscribe(filters: [
|
subscribe()
|
||||||
NostrFilter(ids: [self.event_id], limit: 1)
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|||||||
@@ -64,8 +64,8 @@ struct MenuItems: View {
|
|||||||
self.profileModel = profileModel
|
self.profileModel = profileModel
|
||||||
}
|
}
|
||||||
|
|
||||||
var event_relay_url_strings: [RelayURL] {
|
func event_relay_url_strings() async -> [RelayURL] {
|
||||||
let relays = damus_state.nostrNetwork.relaysForEvent(event: event)
|
let relays = await damus_state.nostrNetwork.relaysForEvent(event: event)
|
||||||
if !relays.isEmpty {
|
if !relays.isEmpty {
|
||||||
return relays.prefix(Constants.MAX_SHARE_RELAYS).map { $0 }
|
return relays.prefix(Constants.MAX_SHARE_RELAYS).map { $0 }
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ struct MenuItems: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
UIPasteboard.general.string = Bech32Object.encode(.nevent(NEvent(event: event, relays: event_relay_url_strings)))
|
Task { UIPasteboard.general.string = Bech32Object.encode(.nevent(NEvent(event: event, relays: await event_relay_url_strings()))) }
|
||||||
} label: {
|
} label: {
|
||||||
Label(NSLocalizedString("Copy note ID", comment: "Context menu option for copying the ID of the note."), image: "note-book")
|
Label(NSLocalizedString("Copy note ID", comment: "Context menu option for copying the ID of the note."), image: "note-book")
|
||||||
}
|
}
|
||||||
@@ -122,7 +122,7 @@ struct MenuItems: View {
|
|||||||
if let full_keypair = self.damus_state.keypair.to_full(),
|
if let full_keypair = self.damus_state.keypair.to_full(),
|
||||||
let new_mutelist_ev = toggle_from_mutelist(keypair: full_keypair, prev: damus_state.mutelist_manager.event, to_toggle: .thread(event.thread_id(), duration?.date_from_now)) {
|
let new_mutelist_ev = toggle_from_mutelist(keypair: full_keypair, prev: damus_state.mutelist_manager.event, to_toggle: .thread(event.thread_id(), duration?.date_from_now)) {
|
||||||
damus_state.mutelist_manager.set_mutelist(new_mutelist_ev)
|
damus_state.mutelist_manager.set_mutelist(new_mutelist_ev)
|
||||||
damus_state.nostrNetwork.postbox.send(new_mutelist_ev)
|
Task { await damus_state.nostrNetwork.postbox.send(new_mutelist_ev) }
|
||||||
}
|
}
|
||||||
let muted = damus_state.mutelist_manager.is_event_muted(event)
|
let muted = damus_state.mutelist_manager.is_event_muted(event)
|
||||||
isMutedThread = muted
|
isMutedThread = muted
|
||||||
@@ -152,7 +152,7 @@ struct MenuItems: View {
|
|||||||
profileModel.subscribeToFindRelays()
|
profileModel.subscribeToFindRelays()
|
||||||
}
|
}
|
||||||
.onDisappear() {
|
.onDisappear() {
|
||||||
profileModel.unsubscribeFindRelays()
|
profileModel.findRelaysListener?.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ struct EventProfile: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: .center, spacing: 10) {
|
HStack(alignment: .center, spacing: 10) {
|
||||||
ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles, disable_animation: disable_animation, show_zappability: true)
|
ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles, disable_animation: disable_animation, show_zappability: true, damusState: damus_state)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
show_profile_action_sheet_if_enabled(damus_state: damus_state, pubkey: pubkey)
|
show_profile_action_sheet_if_enabled(damus_state: damus_state, pubkey: pubkey)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ func format_date(date: Date, time_style: DateFormatter.Style = .short) -> String
|
|||||||
|
|
||||||
func make_actionbar_model(ev: NoteId, damus: DamusState) -> ActionBarModel {
|
func make_actionbar_model(ev: NoteId, damus: DamusState) -> ActionBarModel {
|
||||||
let model = ActionBarModel.empty()
|
let model = ActionBarModel.empty()
|
||||||
model.update(damus: damus, evid: ev)
|
Task { await model.update(damus: damus, evid: ev) }
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ class EventsModel: ObservableObject {
|
|||||||
let state: DamusState
|
let state: DamusState
|
||||||
let target: NoteId
|
let target: NoteId
|
||||||
let kind: QueryKind
|
let kind: QueryKind
|
||||||
let sub_id = UUID().uuidString
|
|
||||||
let profiles_id = UUID().uuidString
|
let profiles_id = UUID().uuidString
|
||||||
var events: EventHolder
|
var events: EventHolder
|
||||||
@Published var loading: Bool
|
@Published var loading: Bool
|
||||||
|
var loadingTask: Task<Void, Never>?
|
||||||
|
|
||||||
enum QueryKind {
|
enum QueryKind {
|
||||||
case kind(NostrKind)
|
case kind(NostrKind)
|
||||||
@@ -68,42 +68,40 @@ class EventsModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func subscribe() {
|
func subscribe() {
|
||||||
state.nostrNetwork.pool.subscribe(sub_id: sub_id,
|
loadingTask?.cancel()
|
||||||
filters: [get_filter()],
|
loadingTask = Task {
|
||||||
handler: handle_nostr_event)
|
DispatchQueue.main.async { self.loading = true }
|
||||||
|
outerLoop: for await item in state.nostrNetwork.reader.advancedStream(filters: [get_filter()]) {
|
||||||
|
switch item {
|
||||||
|
case .event(let lender):
|
||||||
|
Task {
|
||||||
|
await lender.justUseACopy({ event in
|
||||||
|
if await events.insert(event) {
|
||||||
|
DispatchQueue.main.async { self.objectWillChange.send() }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
case .eose:
|
||||||
|
break outerLoop
|
||||||
|
case .ndbEose:
|
||||||
|
DispatchQueue.main.async { self.loading = false }
|
||||||
|
break
|
||||||
|
case .networkEose:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DispatchQueue.main.async { self.loading = false }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func unsubscribe() {
|
func unsubscribe() {
|
||||||
state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
|
loadingTask?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
private func handle_event(relay_id: RelayURL, ev: NostrEvent) {
|
private func handle_event(relay_id: RelayURL, ev: NostrEvent) {
|
||||||
if events.insert(ev) {
|
if events.insert(ev) {
|
||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_nostr_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
|
||||||
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)
|
|
||||||
case .notice:
|
|
||||||
break
|
|
||||||
case .ok:
|
|
||||||
break
|
|
||||||
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.all_events), damus_state: state, txn: txn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class LoadableNostrEventViewModel: ObservableObject {
|
|||||||
|
|
||||||
/// Asynchronously find an event from NostrDB or from the network (if not available on NostrDB)
|
/// Asynchronously find an event from NostrDB or from the network (if not available on NostrDB)
|
||||||
private func loadEvent(noteId: NoteId) async -> NostrEvent? {
|
private func loadEvent(noteId: NoteId) async -> NostrEvent? {
|
||||||
let res = await find_event(state: damus_state, query: .event(evid: noteId))
|
let res = await damus_state.nostrNetwork.reader.findEvent(query: .event(evid: noteId))
|
||||||
guard let res, case .event(let ev) = res else { return nil }
|
guard let res, case .event(let ev) = res else { return nil }
|
||||||
return ev
|
return ev
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ class LoadableNostrEventViewModel: ObservableObject {
|
|||||||
return .unknown_or_unsupported_kind
|
return .unknown_or_unsupported_kind
|
||||||
}
|
}
|
||||||
case .naddr(let naddr):
|
case .naddr(let naddr):
|
||||||
guard let event = await naddrLookup(damus_state: damus_state, naddr: naddr) else { return .not_found }
|
guard let event = await damus_state.nostrNetwork.reader.lookup(naddr: naddr) else { return .not_found }
|
||||||
return .loaded(route: Route.Thread(thread: ThreadModel(event: event, damus_state: damus_state)))
|
return .loaded(route: Route.Thread(thread: ThreadModel(event: event, damus_state: damus_state)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,12 +83,6 @@ func render_immediately_available_note_content(ndb: Ndb, ev: NostrEvent, profile
|
|||||||
|
|
||||||
actor ContentRenderer {
|
actor ContentRenderer {
|
||||||
func render_note_content(ndb: Ndb, ev: NostrEvent, profiles: Profiles, keypair: Keypair) async -> NoteArtifacts {
|
func render_note_content(ndb: Ndb, ev: NostrEvent, profiles: Profiles, keypair: Keypair) async -> NoteArtifacts {
|
||||||
if ev.known_kind == .dm {
|
|
||||||
// Use the enhanced render_immediately_available_note_content which now handles DMs properly
|
|
||||||
// by decrypting and parsing the content with ndb_parse_content
|
|
||||||
return render_immediately_available_note_content(ndb: ndb, ev: ev, profiles: profiles, keypair: keypair)
|
|
||||||
}
|
|
||||||
let result = try? await ndb.waitFor(noteId: ev.id, timeout: 3)
|
|
||||||
return render_immediately_available_note_content(ndb: ndb, ev: ev, profiles: profiles, keypair: keypair)
|
return render_immediately_available_note_content(ndb: ndb, ev: ev, profiles: profiles, keypair: keypair)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ struct SelectedEventView: View {
|
|||||||
}
|
}
|
||||||
.onReceive(handle_notify(.update_stats)) { target in
|
.onReceive(handle_notify(.update_stats)) { target in
|
||||||
guard target == self.event.id else { return }
|
guard target == self.event.id else { return }
|
||||||
self.bar.update(damus: self.damus, evid: target)
|
Task { await self.bar.update(damus: self.damus, evid: target) }
|
||||||
}
|
}
|
||||||
.compositingGroup()
|
.compositingGroup()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class FollowPackModel: ObservableObject {
|
|||||||
@Published var loading: Bool = false
|
@Published var loading: Bool = false
|
||||||
|
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
let subid = UUID().description
|
var listener: Task<Void, Never>? = nil
|
||||||
let limit: UInt32 = 500
|
let limit: UInt32 = 500
|
||||||
|
|
||||||
init(damus_state: DamusState) {
|
init(damus_state: DamusState) {
|
||||||
@@ -25,52 +25,36 @@ class FollowPackModel: ObservableObject {
|
|||||||
|
|
||||||
func subscribe(follow_pack_users: [Pubkey]) {
|
func subscribe(follow_pack_users: [Pubkey]) {
|
||||||
loading = true
|
loading = true
|
||||||
let to_relays = determine_to_relays(pool: damus_state.nostrNetwork.pool, filters: damus_state.relay_filters)
|
self.listener?.cancel()
|
||||||
|
self.listener = Task {
|
||||||
|
await self.listenForUpdates(follow_pack_users: follow_pack_users)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unsubscribe(to: RelayURL? = nil) {
|
||||||
|
loading = false
|
||||||
|
self.listener?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
func listenForUpdates(follow_pack_users: [Pubkey]) async {
|
||||||
|
let to_relays = await damus_state.nostrNetwork.determineToRelays(filters: damus_state.relay_filters)
|
||||||
var filter = NostrFilter(kinds: [.text, .chat])
|
var filter = NostrFilter(kinds: [.text, .chat])
|
||||||
filter.until = UInt32(Date.now.timeIntervalSince1970)
|
filter.until = UInt32(Date.now.timeIntervalSince1970)
|
||||||
filter.authors = follow_pack_users
|
filter.authors = follow_pack_users
|
||||||
filter.limit = 500
|
filter.limit = 500
|
||||||
|
|
||||||
damus_state.nostrNetwork.pool.subscribe(sub_id: subid, filters: [filter], handler: handle_event, to: to_relays)
|
for await event in damus_state.nostrNetwork.reader.streamIndefinitely(filters: [filter], to: to_relays) {
|
||||||
}
|
await event.justUseACopy({ event in
|
||||||
|
let should_show_event = await should_show_event(state: damus_state, ev: event)
|
||||||
func unsubscribe(to: RelayURL? = nil) {
|
if event.is_textlike && should_show_event && !event.is_reply()
|
||||||
loading = false
|
{
|
||||||
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: to.map { [$0] })
|
if await self.events.insert(event) {
|
||||||
}
|
DispatchQueue.main.async {
|
||||||
|
self.objectWillChange.send()
|
||||||
func handle_event(relay_id: RelayURL, conn_ev: NostrConnectionEvent) {
|
}
|
||||||
guard case .nostr_event(let event) = conn_ev else {
|
}
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch event {
|
|
||||||
case .event(let sub_id, let ev):
|
|
||||||
guard sub_id == self.subid else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if ev.is_textlike && should_show_event(state: damus_state, ev: ev) && !ev.is_reply()
|
|
||||||
{
|
|
||||||
if self.events.insert(ev) {
|
|
||||||
self.objectWillChange.send()
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
case .notice(let msg):
|
|
||||||
print("follow pack notice: \(msg)")
|
|
||||||
case .ok:
|
|
||||||
break
|
|
||||||
case .eose(let sub_id):
|
|
||||||
loading = false
|
|
||||||
|
|
||||||
if sub_id == self.subid {
|
|
||||||
unsubscribe(to: relay_id)
|
|
||||||
|
|
||||||
guard let txn = NdbTxn(ndb: damus_state.ndb) else { return }
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
case .auth:
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ struct FollowPackPreviewBody: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
ProfilePicView(pubkey: event.event.pubkey, size: 25, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation, show_zappability: true)
|
ProfilePicView(pubkey: event.event.pubkey, size: 25, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation, show_zappability: true, damusState: state)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
state.nav.push(route: Route.ProfileByKey(pubkey: event.event.pubkey))
|
state.nav.push(route: Route.ProfileByKey(pubkey: event.event.pubkey))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ struct FollowPackTimelineView<Content: View>: View {
|
|||||||
.coordinateSpace(name: "scroll")
|
.coordinateSpace(name: "scroll")
|
||||||
.onReceive(handle_notify(.scroll_to_top)) { () in
|
.onReceive(handle_notify(.scroll_to_top)) { () in
|
||||||
events.flush()
|
events.flush()
|
||||||
self.events.should_queue = false
|
self.events.set_should_queue(false)
|
||||||
scroll_to_event(scroller: scroller, id: "startblock", delay: 0.0, animate: true, anchor: .top)
|
scroll_to_event(scroller: scroller, id: "startblock", delay: 0.0, animate: true, anchor: .top)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ struct FollowPackView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
ProfilePicView(pubkey: event.event.pubkey, size: 25, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation, show_zappability: true)
|
ProfilePicView(pubkey: event.event.pubkey, size: 25, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation, show_zappability: true, damusState: state)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
state.nav.push(route: Route.ProfileByKey(pubkey: event.event.pubkey))
|
state.nav.push(route: Route.ProfileByKey(pubkey: event.event.pubkey))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,17 +9,17 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
func follow_reference(box: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, follow: FollowRef) -> NostrEvent? {
|
func follow_reference(box: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, follow: FollowRef) async -> NostrEvent? {
|
||||||
guard let ev = follow_user_event(our_contacts: our_contacts, keypair: keypair, follow: follow) else {
|
guard let ev = follow_user_event(our_contacts: our_contacts, keypair: keypair, follow: follow) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
box.send(ev)
|
await box.send(ev)
|
||||||
|
|
||||||
return ev
|
return ev
|
||||||
}
|
}
|
||||||
|
|
||||||
func unfollow_reference(postbox: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, unfollow: FollowRef) -> NostrEvent? {
|
func unfollow_reference(postbox: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, unfollow: FollowRef) async -> NostrEvent? {
|
||||||
guard let cs = our_contacts else {
|
guard let cs = our_contacts else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,7 @@ func unfollow_reference(postbox: PostBox, our_contacts: NostrEvent?, keypair: Fu
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
postbox.send(ev)
|
await postbox.send(ev)
|
||||||
|
|
||||||
return ev
|
return ev
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ class FollowersModel: ObservableObject {
|
|||||||
@Published var contacts: [Pubkey]? = nil
|
@Published var contacts: [Pubkey]? = nil
|
||||||
var has_contact: Set<Pubkey> = Set()
|
var has_contact: Set<Pubkey> = Set()
|
||||||
|
|
||||||
let sub_id: String = UUID().description
|
var listener: Task<Void, Never>? = nil
|
||||||
let profiles_id: String = UUID().description
|
var profilesListener: Task<Void, Never>? = nil
|
||||||
|
|
||||||
var count: Int? {
|
var count: Int? {
|
||||||
guard let contacts = self.contacts else {
|
guard let contacts = self.contacts else {
|
||||||
@@ -36,14 +36,22 @@ class FollowersModel: ObservableObject {
|
|||||||
func subscribe() {
|
func subscribe() {
|
||||||
let filter = get_filter()
|
let filter = get_filter()
|
||||||
let filters = [filter]
|
let filters = [filter]
|
||||||
//print_filters(relay_id: "following", filters: [filters])
|
self.listener?.cancel()
|
||||||
self.damus_state.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
|
self.listener = Task {
|
||||||
|
for await lender in damus_state.nostrNetwork.reader.streamIndefinitely(filters: filters) {
|
||||||
|
lender.justUseACopy({ self.handle_event(ev: $0) })
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func unsubscribe() {
|
func unsubscribe() {
|
||||||
self.damus_state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
|
self.listener?.cancel()
|
||||||
|
self.profilesListener?.cancel()
|
||||||
|
self.listener = nil
|
||||||
|
self.profilesListener = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func handle_contact_event(_ ev: NostrEvent) {
|
func handle_contact_event(_ ev: NostrEvent) {
|
||||||
if has_contact.contains(ev.pubkey) {
|
if has_contact.contains(ev.pubkey) {
|
||||||
return
|
return
|
||||||
@@ -52,47 +60,10 @@ class FollowersModel: ObservableObject {
|
|||||||
contacts?.append(ev.pubkey)
|
contacts?.append(ev.pubkey)
|
||||||
has_contact.insert(ev.pubkey)
|
has_contact.insert(ev.pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func load_profiles<Y>(relay_id: RelayURL, txn: NdbTxn<Y>) {
|
func handle_event(ev: NostrEvent) {
|
||||||
let authors = find_profiles_to_fetch_from_keys(profiles: damus_state.profiles, pks: contacts ?? [], txn: txn)
|
if ev.known_kind == .contacts {
|
||||||
if authors.isEmpty {
|
Task { await handle_contact_event(ev) }
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let filter = NostrFilter(kinds: [.metadata],
|
|
||||||
authors: authors)
|
|
||||||
damus_state.nostrNetwork.pool.subscribe_to(sub_id: profiles_id, filters: [filter], to: [relay_id], handler: handle_event)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
|
||||||
guard case .nostr_event(let nev) = ev else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch nev {
|
|
||||||
case .event(let sub_id, let ev):
|
|
||||||
guard sub_id == self.sub_id || sub_id == self.profiles_id else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ev.known_kind == .contacts {
|
|
||||||
handle_contact_event(ev)
|
|
||||||
}
|
|
||||||
case .notice(let msg):
|
|
||||||
print("followingmodel notice: \(msg)")
|
|
||||||
|
|
||||||
case .eose(let sub_id):
|
|
||||||
if sub_id == self.sub_id {
|
|
||||||
guard let txn = NdbTxn(ndb: self.damus_state.ndb) else { return }
|
|
||||||
load_profiles(relay_id: relay_id, txn: txn)
|
|
||||||
} else if sub_id == self.profiles_id {
|
|
||||||
damus_state.nostrNetwork.pool.unsubscribe(sub_id: profiles_id, to: [relay_id])
|
|
||||||
}
|
|
||||||
|
|
||||||
case .ok:
|
|
||||||
break
|
|
||||||
case .auth:
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class FollowingModel {
|
|||||||
let contacts: [Pubkey]
|
let contacts: [Pubkey]
|
||||||
let hashtags: [Hashtag]
|
let hashtags: [Hashtag]
|
||||||
|
|
||||||
let sub_id: String = UUID().description
|
private var listener: Task<Void, Never>? = nil
|
||||||
|
|
||||||
init(damus_state: DamusState, contacts: [Pubkey], hashtags: [Hashtag]) {
|
init(damus_state: DamusState, contacts: [Pubkey], hashtags: [Hashtag]) {
|
||||||
self.damus_state = damus_state
|
self.damus_state = damus_state
|
||||||
@@ -41,19 +41,17 @@ class FollowingModel {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
let filters = [filter]
|
let filters = [filter]
|
||||||
//print_filters(relay_id: "following", filters: [filters])
|
self.listener?.cancel()
|
||||||
self.damus_state.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
|
self.listener = Task {
|
||||||
|
for await item in self.damus_state.nostrNetwork.reader.advancedStream(filters: filters) {
|
||||||
|
// don't need to do anything here really
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func unsubscribe() {
|
func unsubscribe() {
|
||||||
if !needs_sub {
|
self.listener?.cancel()
|
||||||
return
|
self.listener = nil
|
||||||
}
|
|
||||||
print("unsubscribing from following \(sub_id)")
|
|
||||||
self.damus_state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
|
||||||
// don't need to do anything here really
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ func loadOldMutedThreads(pubkey: Pubkey) -> [NoteId] {
|
|||||||
// We need to still use it since existing users might have their muted threads stored in UserDefaults
|
// We need to still use it since existing users might have their muted threads stored in UserDefaults
|
||||||
// So now all it's doing is moving a users muted threads to the new kind:10000 system
|
// So now all it's doing is moving a users muted threads to the new kind:10000 system
|
||||||
// It should not be used for any purpose beyond that
|
// It should not be used for any purpose beyond that
|
||||||
|
@MainActor
|
||||||
func migrate_old_muted_threads_to_new_mutelist(keypair: Keypair, damus_state: DamusState) {
|
func migrate_old_muted_threads_to_new_mutelist(keypair: Keypair, damus_state: DamusState) {
|
||||||
// Ensure that keypair is fullkeypair
|
// Ensure that keypair is fullkeypair
|
||||||
guard let fullKeypair = keypair.to_full() else { return }
|
guard let fullKeypair = keypair.to_full() else { return }
|
||||||
@@ -33,7 +34,7 @@ func migrate_old_muted_threads_to_new_mutelist(keypair: Keypair, damus_state: Da
|
|||||||
let previous_mute_list_event = damus_state.mutelist_manager.event
|
let previous_mute_list_event = damus_state.mutelist_manager.event
|
||||||
guard let new_mutelist_event = create_or_update_mutelist(keypair: fullKeypair, mprev: previous_mute_list_event, to_add: Set(mutedThreads.map { MuteItem.thread($0, nil) })) else { return }
|
guard let new_mutelist_event = create_or_update_mutelist(keypair: fullKeypair, mprev: previous_mute_list_event, to_add: Set(mutedThreads.map { MuteItem.thread($0, nil) })) else { return }
|
||||||
damus_state.mutelist_manager.set_mutelist(new_mutelist_event)
|
damus_state.mutelist_manager.set_mutelist(new_mutelist_event)
|
||||||
damus_state.nostrNetwork.postbox.send(new_mutelist_event)
|
Task { await damus_state.nostrNetwork.postbox.send(new_mutelist_event) }
|
||||||
// Set existing muted threads to an empty array
|
// Set existing muted threads to an empty array
|
||||||
UserDefaults.standard.set([], forKey: getMutedThreadsKey(pubkey: keypair.pubkey))
|
UserDefaults.standard.set([], forKey: getMutedThreadsKey(pubkey: keypair.pubkey))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
@MainActor
|
||||||
class MutelistManager {
|
class MutelistManager {
|
||||||
let user_keypair: Keypair
|
let user_keypair: Keypair
|
||||||
private(set) var event: NostrEvent? = nil
|
private(set) var event: NostrEvent? = nil
|
||||||
@@ -26,7 +27,7 @@ class MutelistManager {
|
|||||||
|
|
||||||
var muted_notes_cache: [NoteId: EventMuteStatus] = [:]
|
var muted_notes_cache: [NoteId: EventMuteStatus] = [:]
|
||||||
|
|
||||||
init(user_keypair: Keypair) {
|
nonisolated init(user_keypair: Keypair) {
|
||||||
self.user_keypair = user_keypair
|
self.user_keypair = user_keypair
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ struct AddMuteItemView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
state.mutelist_manager.set_mutelist(mutelist)
|
state.mutelist_manager.set_mutelist(mutelist)
|
||||||
state.nostrNetwork.postbox.send(mutelist)
|
Task { await state.nostrNetwork.postbox.send(mutelist) }
|
||||||
}
|
}
|
||||||
|
|
||||||
new_text = ""
|
new_text = ""
|
||||||
|
|||||||
@@ -30,8 +30,10 @@ struct MutelistView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
damus_state.mutelist_manager.set_mutelist(new_ev)
|
damus_state.mutelist_manager.set_mutelist(new_ev)
|
||||||
damus_state.nostrNetwork.postbox.send(new_ev)
|
Task {
|
||||||
updateMuteItems()
|
await damus_state.nostrNetwork.postbox.send(new_ev)
|
||||||
|
updateMuteItems()
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label(NSLocalizedString("Delete", comment: "Button to remove a user from their mutelist."), image: "delete")
|
Label(NSLocalizedString("Delete", comment: "Button to remove a user from their mutelist."), image: "delete")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ class NIP05DomainEventsModel: ObservableObject {
|
|||||||
|
|
||||||
let domain: String
|
let domain: String
|
||||||
var filter: NostrFilter
|
var filter: NostrFilter
|
||||||
let sub_id = UUID().description
|
var loadingTask: Task<Void, Never>?
|
||||||
let profiles_subid = UUID().description
|
|
||||||
let limit: UInt32 = 500
|
let limit: UInt32 = 500
|
||||||
|
|
||||||
init(state: DamusState, domain: String) {
|
init(state: DamusState, domain: String) {
|
||||||
@@ -29,6 +28,20 @@ class NIP05DomainEventsModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainActor func subscribe() {
|
@MainActor func subscribe() {
|
||||||
|
print("subscribing to notes from friends of friends with '\(domain)' NIP-05 domain")
|
||||||
|
loadingTask = Task {
|
||||||
|
await streamItems()
|
||||||
|
}
|
||||||
|
loading = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func unsubscribe() {
|
||||||
|
loadingTask?.cancel()
|
||||||
|
loading = false
|
||||||
|
print("unsubscribing from notes from friends of friends with '\(domain)' NIP-05 domain")
|
||||||
|
}
|
||||||
|
|
||||||
|
func streamItems() async {
|
||||||
filter.limit = self.limit
|
filter.limit = self.limit
|
||||||
filter.kinds = [.text, .longform, .highlight]
|
filter.kinds = [.text, .longform, .highlight]
|
||||||
|
|
||||||
@@ -50,48 +63,35 @@ class NIP05DomainEventsModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
filter.authors = Array(authors)
|
filter.authors = Array(authors)
|
||||||
|
|
||||||
print("subscribing to notes from friends of friends with '\(domain)' NIP-05 domain with sub_id \(sub_id)")
|
|
||||||
state.nostrNetwork.pool.register_handler(sub_id: sub_id, handler: handle_event)
|
for await item in state.nostrNetwork.reader.advancedStream(filters: [filter]) {
|
||||||
loading = true
|
switch item {
|
||||||
state.nostrNetwork.pool.send(.subscribe(.init(filters: [filter], sub_id: sub_id)))
|
case .event(let lender):
|
||||||
|
await lender.justUseACopy({ await self.add_event($0) })
|
||||||
|
case .eose:
|
||||||
|
DispatchQueue.main.async { self.loading = false }
|
||||||
|
continue
|
||||||
|
case .ndbEose:
|
||||||
|
break
|
||||||
|
case .networkEose:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func unsubscribe() {
|
func add_event(_ ev: NostrEvent) async {
|
||||||
state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
|
|
||||||
loading = false
|
|
||||||
print("unsubscribing from notes from friends of friends with '\(domain)' NIP-05 domain with sub_id \(sub_id)")
|
|
||||||
}
|
|
||||||
|
|
||||||
func add_event(_ ev: NostrEvent) {
|
|
||||||
if !event_matches_filter(ev, filter: filter) {
|
if !event_matches_filter(ev, filter: filter) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard should_show_event(state: state, ev: ev) else {
|
guard await should_show_event(state: state, ev: ev) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.events.insert(ev) {
|
if await self.events.insert(ev) {
|
||||||
objectWillChange.send()
|
DispatchQueue.main.async {
|
||||||
}
|
self.objectWillChange.send()
|
||||||
}
|
|
||||||
|
|
||||||
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
|
||||||
let (sub_id, done) = handle_subid_event(pool: state.nostrNetwork.pool, relay_id: relay_id, ev: ev) { sub_id, ev in
|
|
||||||
if sub_id == self.sub_id && ev.is_textlike && ev.should_show_event {
|
|
||||||
self.add_event(ev)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
guard done else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.loading = false
|
|
||||||
|
|
||||||
if sub_id == self.sub_id {
|
|
||||||
guard let txn = NdbTxn(ndb: state.ndb) else { return }
|
|
||||||
load_profiles(context: "search", profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(self.events.all_events), damus_state: state, txn: txn)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import UIKit
|
|||||||
|
|
||||||
let EVENT_MAX_AGE_FOR_NOTIFICATION: TimeInterval = 12 * 60 * 60
|
let EVENT_MAX_AGE_FOR_NOTIFICATION: TimeInterval = 12 * 60 * 60
|
||||||
|
|
||||||
func process_local_notification(state: HeadlessDamusState, event ev: NostrEvent) {
|
func process_local_notification(state: HeadlessDamusState, event ev: NostrEvent) async {
|
||||||
guard should_display_notification(state: state, event: ev, mode: .local) else {
|
guard await should_display_notification(state: state, event: ev, mode: .local) else {
|
||||||
// We should not display notification. Exit.
|
// We should not display notification. Exit.
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ func process_local_notification(state: HeadlessDamusState, event ev: NostrEvent)
|
|||||||
create_local_notification(profiles: state.profiles, notify: local_notification)
|
create_local_notification(profiles: state.profiles, notify: local_notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent, mode: UserSettingsStore.NotificationsMode) -> Bool {
|
func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent, mode: UserSettingsStore.NotificationsMode) async -> Bool {
|
||||||
// Do not show notification if it's coming from a mode different from the one selected by our user
|
// Do not show notification if it's coming from a mode different from the one selected by our user
|
||||||
guard state.settings.notification_mode == mode else {
|
guard state.settings.notification_mode == mode else {
|
||||||
return false
|
return false
|
||||||
@@ -46,7 +46,7 @@ func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Don't show notifications that match mute list.
|
// Don't show notifications that match mute list.
|
||||||
if state.mutelist_manager.is_event_muted(ev) {
|
if await state.mutelist_manager.is_event_muted(ev) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ struct ProfilePicturesView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
ForEach(pubkeys.prefix(8), id: \.self) { pubkey in
|
ForEach(pubkeys.prefix(8), id: \.self) { pubkey in
|
||||||
ProfilePicView(pubkey: pubkey, size: 32.0, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation)
|
ProfilePicView(pubkey: pubkey, size: 32.0, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation, damusState: state)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
state.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
|
state.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,34 +189,30 @@ class SuggestedUsersViewModel: ObservableObject {
|
|||||||
authors: [Constants.ONBOARDING_FOLLOW_PACK_CURATOR_PUBKEY]
|
authors: [Constants.ONBOARDING_FOLLOW_PACK_CURATOR_PUBKEY]
|
||||||
)
|
)
|
||||||
|
|
||||||
for await item in self.damus_state.nostrNetwork.reader.subscribe(filters: [filter]) {
|
for await lender in self.damus_state.nostrNetwork.reader.streamExistingEvents(filters: [filter]) {
|
||||||
// Check for cancellation on each iteration
|
// Check for cancellation on each iteration
|
||||||
guard !Task.isCancelled else { break }
|
guard !Task.isCancelled else { break }
|
||||||
|
|
||||||
switch item {
|
lender.justUseACopy({ event in
|
||||||
case .event(let borrow):
|
let followPack = FollowPackEvent.parse(from: event)
|
||||||
try? borrow { event in
|
|
||||||
let followPack = FollowPackEvent.parse(from: event.toOwned())
|
guard let id = followPack.uuid else { return }
|
||||||
|
|
||||||
guard let id = followPack.uuid else { return }
|
let latestPackForThisId: FollowPackEvent
|
||||||
|
|
||||||
let latestPackForThisId: FollowPackEvent
|
if let existingPack = packsById[id], existingPack.event.created_at > followPack.event.created_at {
|
||||||
|
latestPackForThisId = existingPack
|
||||||
if let existingPack = packsById[id], existingPack.event.created_at > followPack.event.created_at {
|
} else {
|
||||||
latestPackForThisId = existingPack
|
latestPackForThisId = followPack
|
||||||
} else {
|
|
||||||
latestPackForThisId = followPack
|
|
||||||
}
|
|
||||||
|
|
||||||
packsById[id] = latestPackForThisId
|
|
||||||
}
|
}
|
||||||
case .eose:
|
|
||||||
break
|
packsById[id] = latestPackForThisId
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds all profiles mentioned in the follow packs, and loads the profile data from the network
|
/// Finds all profiles mentioned in the follow packs, and loads the profile data from the network
|
||||||
|
// TODO LOCAL_RELAY_PROFILE: Remove this
|
||||||
private func loadProfiles(for packs: [FollowPackEvent]) async {
|
private func loadProfiles(for packs: [FollowPackEvent]) async {
|
||||||
var allPubkeys: [Pubkey] = []
|
var allPubkeys: [Pubkey] = []
|
||||||
|
|
||||||
@@ -228,13 +224,8 @@ class SuggestedUsersViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let profileFilter = NostrFilter(kinds: [.metadata], authors: allPubkeys)
|
let profileFilter = NostrFilter(kinds: [.metadata], authors: allPubkeys)
|
||||||
for await item in damus_state.nostrNetwork.reader.subscribe(filters: [profileFilter]) {
|
for await _ in damus_state.nostrNetwork.reader.streamExistingEvents(filters: [profileFilter]) {
|
||||||
switch item {
|
// NO-OP. We just need NostrDB to ingest these for them to be available elsewhere, no need to analyze the data
|
||||||
case .event(_):
|
|
||||||
continue // We just need NostrDB to ingest these for them to be available elsewhere, no need to analyze the data
|
|
||||||
case .eose:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,10 +55,12 @@ struct CreateAccountView: View, KeyboardReadable {
|
|||||||
.foregroundColor(DamusColors.neutral6)
|
.foregroundColor(DamusColors.neutral6)
|
||||||
FormTextInput(NSLocalizedString("Satoshi Nakamoto", comment: "Name of Bitcoin creator(s)."), text: $account.name)
|
FormTextInput(NSLocalizedString("Satoshi Nakamoto", comment: "Name of Bitcoin creator(s)."), text: $account.name)
|
||||||
.textInputAutocapitalization(.words)
|
.textInputAutocapitalization(.words)
|
||||||
|
.accessibilityIdentifier(AppAccessibilityIdentifiers.sign_up_name_field.rawValue)
|
||||||
|
|
||||||
FormLabel(NSLocalizedString("Bio", comment: "Label to prompt bio entry for user to describe themself."), optional: true)
|
FormLabel(NSLocalizedString("Bio", comment: "Label to prompt bio entry for user to describe themself."), optional: true)
|
||||||
.foregroundColor(DamusColors.neutral6)
|
.foregroundColor(DamusColors.neutral6)
|
||||||
FormTextInput(NSLocalizedString("Absolute legend.", comment: "Example Bio"), text: $account.about)
|
FormTextInput(NSLocalizedString("Absolute legend.", comment: "Example Bio"), text: $account.about)
|
||||||
|
.accessibilityIdentifier(AppAccessibilityIdentifiers.sign_up_bio_field.rawValue)
|
||||||
}
|
}
|
||||||
.padding(.top, 25)
|
.padding(.top, 25)
|
||||||
|
|
||||||
@@ -75,6 +77,7 @@ struct CreateAccountView: View, KeyboardReadable {
|
|||||||
.disabled(profileUploadObserver.isLoading || account.name.isEmpty)
|
.disabled(profileUploadObserver.isLoading || account.name.isEmpty)
|
||||||
.opacity(profileUploadObserver.isLoading || account.name.isEmpty ? 0.5 : 1)
|
.opacity(profileUploadObserver.isLoading || account.name.isEmpty ? 0.5 : 1)
|
||||||
.padding(.top, 20)
|
.padding(.top, 20)
|
||||||
|
.accessibilityIdentifier(AppAccessibilityIdentifiers.sign_up_next_button.rawValue)
|
||||||
|
|
||||||
LoginPrompt()
|
LoginPrompt()
|
||||||
.padding(.top)
|
.padding(.top)
|
||||||
|
|||||||
@@ -452,6 +452,7 @@ struct CreateAccountPrompt: View {
|
|||||||
Button(NSLocalizedString("Create account", comment: "Button to navigate to create account view.")) {
|
Button(NSLocalizedString("Create account", comment: "Button to navigate to create account view.")) {
|
||||||
nav.push(route: Route.CreateAccount)
|
nav.push(route: Route.CreateAccount)
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier(AppAccessibilityIdentifiers.sign_up_option_button.rawValue)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ struct OnboardingSuggestionsView: View {
|
|||||||
// - We don't have other mechanisms to allow the user to edit this yet
|
// - We don't have other mechanisms to allow the user to edit this yet
|
||||||
//
|
//
|
||||||
// Therefore, it is better to just save it locally, and retrieve this once we build out https://github.com/damus-io/damus/issues/3042
|
// Therefore, it is better to just save it locally, and retrieve this once we build out https://github.com/damus-io/damus/issues/3042
|
||||||
model.damus_state.nostrNetwork.pool.send_raw_to_local_ndb(.typical(.event(event)))
|
Task { await model.damus_state.nostrNetwork.sendToNostrDB(event: event) }
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ struct SaveKeysView: View {
|
|||||||
.foregroundColor(.red)
|
.foregroundColor(.red)
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
complete_account_creation(account)
|
Task { await complete_account_creation(account) }
|
||||||
}) {
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Retry", comment: "Button to retry completing account creation after an error occurred.")
|
Text("Retry", comment: "Button to retry completing account creation after an error occurred.")
|
||||||
@@ -89,7 +89,7 @@ struct SaveKeysView: View {
|
|||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
save_key(account)
|
save_key(account)
|
||||||
complete_account_creation(account)
|
Task { await complete_account_creation(account) }
|
||||||
}) {
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Save", comment: "Button to save key, complete account creation, and start using the app.")
|
Text("Save", comment: "Button to save key, complete account creation, and start using the app.")
|
||||||
@@ -99,9 +99,10 @@ struct SaveKeysView: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(GradientButtonStyle())
|
.buttonStyle(GradientButtonStyle())
|
||||||
.padding(.top, 20)
|
.padding(.top, 20)
|
||||||
|
.accessibilityIdentifier(AppAccessibilityIdentifiers.sign_up_save_keys_button.rawValue)
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
complete_account_creation(account)
|
Task { await complete_account_creation(account) }
|
||||||
}) {
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Not now", comment: "Button to not save key, complete account creation, and start using the app.")
|
Text("Not now", comment: "Button to not save key, complete account creation, and start using the app.")
|
||||||
@@ -111,6 +112,7 @@ struct SaveKeysView: View {
|
|||||||
}
|
}
|
||||||
.buttonStyle(NeutralButtonStyle(padding: EdgeInsets(top: 15, leading: 15, bottom: 15, trailing: 15), cornerRadius: 12))
|
.buttonStyle(NeutralButtonStyle(padding: EdgeInsets(top: 15, leading: 15, bottom: 15, trailing: 15), cornerRadius: 12))
|
||||||
.padding(.top, 20)
|
.padding(.top, 20)
|
||||||
|
.accessibilityIdentifier(AppAccessibilityIdentifiers.sign_up_skip_save_keys_button.rawValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(20)
|
.padding(20)
|
||||||
@@ -125,7 +127,7 @@ struct SaveKeysView: View {
|
|||||||
credential_handler.save_credential(pubkey: account.pubkey, privkey: account.privkey)
|
credential_handler.save_credential(pubkey: account.pubkey, privkey: account.privkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func complete_account_creation(_ account: CreateAccountModel) {
|
func complete_account_creation(_ account: CreateAccountModel) async {
|
||||||
guard let first_contact_event else {
|
guard let first_contact_event else {
|
||||||
error = NSLocalizedString("Could not create your initial contact list event. This is a software bug, please contact Damus support via support@damus.io or through our Nostr account for help.", comment: "Error message to the user indicating that the initial contact list failed to be created.")
|
error = NSLocalizedString("Could not create your initial contact list event. This is a software bug, please contact Damus support via support@damus.io or through our Nostr account for help.", comment: "Error message to the user indicating that the initial contact list failed to be created.")
|
||||||
return
|
return
|
||||||
@@ -139,14 +141,21 @@ struct SaveKeysView: View {
|
|||||||
|
|
||||||
let bootstrap_relays = load_bootstrap_relays(pubkey: account.pubkey)
|
let bootstrap_relays = load_bootstrap_relays(pubkey: account.pubkey)
|
||||||
for relay in bootstrap_relays {
|
for relay in bootstrap_relays {
|
||||||
add_rw_relay(self.pool, relay)
|
await add_rw_relay(self.pool, relay)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.pool.register_handler(sub_id: "signup", handler: handle_event)
|
|
||||||
|
|
||||||
self.loading = true
|
|
||||||
|
|
||||||
self.pool.connect()
|
self.loading = true
|
||||||
|
|
||||||
|
Task {
|
||||||
|
await self.pool.connect()
|
||||||
|
|
||||||
|
let stream = AsyncStream<(RelayURL, NostrConnectionEvent)> { streamContinuation in
|
||||||
|
Task { await self.pool.register_handler(sub_id: "signup", filters: nil, handler: streamContinuation) }
|
||||||
|
}
|
||||||
|
for await (relayUrl, connectionEvent) in stream {
|
||||||
|
await handle_event(relay: relayUrl, ev: connectionEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func save_to_storage(first_contact_event: NdbNote, first_relay_list_event: NdbNote, for account: CreateAccountModel) {
|
func save_to_storage(first_contact_event: NdbNote, first_relay_list_event: NdbNote, for account: CreateAccountModel) {
|
||||||
@@ -160,7 +169,7 @@ struct SaveKeysView: View {
|
|||||||
settings.latestRelayListEventIdHex = first_relay_list_event.id.hex()
|
settings.latestRelayListEventIdHex = first_relay_list_event.id.hex()
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_event(relay: RelayURL, ev: NostrConnectionEvent) {
|
func handle_event(relay: RelayURL, ev: NostrConnectionEvent) async {
|
||||||
switch ev {
|
switch ev {
|
||||||
case .ws_connection_event(let wsev):
|
case .ws_connection_event(let wsev):
|
||||||
switch wsev {
|
switch wsev {
|
||||||
@@ -169,15 +178,15 @@ struct SaveKeysView: View {
|
|||||||
|
|
||||||
if let keypair = account.keypair.to_full(),
|
if let keypair = account.keypair.to_full(),
|
||||||
let metadata_ev = make_metadata_event(keypair: keypair, metadata: metadata) {
|
let metadata_ev = make_metadata_event(keypair: keypair, metadata: metadata) {
|
||||||
self.pool.send(.event(metadata_ev))
|
await self.pool.send(.event(metadata_ev))
|
||||||
}
|
}
|
||||||
|
|
||||||
if let first_contact_event {
|
if let first_contact_event {
|
||||||
self.pool.send(.event(first_contact_event))
|
await self.pool.send(.event(first_contact_event))
|
||||||
}
|
}
|
||||||
|
|
||||||
if let first_relay_list_event {
|
if let first_relay_list_event {
|
||||||
self.pool.send(.event(first_relay_list_event))
|
await self.pool.send(.event(first_relay_list_event))
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ struct SetupView: View {
|
|||||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
||||||
}
|
}
|
||||||
.buttonStyle(GradientButtonStyle())
|
.buttonStyle(GradientButtonStyle())
|
||||||
|
.accessibilityIdentifier(AppAccessibilityIdentifiers.sign_up_option_button.rawValue)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
|
|||||||
@@ -64,9 +64,9 @@ class DraftArtifacts: Equatable {
|
|||||||
/// - damus_state: The damus state, needed for encrypting, fetching Nostr data depedencies, and forming the NIP-37 draft
|
/// - damus_state: The damus state, needed for encrypting, fetching Nostr data depedencies, and forming the NIP-37 draft
|
||||||
/// - references: references in the post?
|
/// - references: references in the post?
|
||||||
/// - Returns: The NIP-37 draft packaged in a way that can be easily wrapped/unwrapped.
|
/// - Returns: The NIP-37 draft packaged in a way that can be easily wrapped/unwrapped.
|
||||||
func to_nip37_draft(action: PostAction, damus_state: DamusState) throws -> NIP37Draft? {
|
func to_nip37_draft(action: PostAction, damus_state: DamusState) async throws -> NIP37Draft? {
|
||||||
guard let keypair = damus_state.keypair.to_full() else { return nil }
|
guard let keypair = damus_state.keypair.to_full() else { return nil }
|
||||||
let post = build_post(state: damus_state, action: action, draft: self)
|
let post = await build_post(state: damus_state, action: action, draft: self)
|
||||||
guard let note = post.to_event(keypair: keypair) else { return nil }
|
guard let note = post.to_event(keypair: keypair) else { return nil }
|
||||||
return try NIP37Draft(unwrapped_note: note, draft_id: self.id, keypair: keypair)
|
return try NIP37Draft(unwrapped_note: note, draft_id: self.id, keypair: keypair)
|
||||||
}
|
}
|
||||||
@@ -224,27 +224,27 @@ class Drafts: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Saves the drafts tracked by this class persistently using NostrDB + UserDefaults
|
/// Saves the drafts tracked by this class persistently using NostrDB + UserDefaults
|
||||||
func save(damus_state: DamusState) {
|
func save(damus_state: DamusState) async {
|
||||||
var draft_events: [NdbNote] = []
|
var draft_events: [NdbNote] = []
|
||||||
post_artifact_block: if let post_artifacts = self.post {
|
post_artifact_block: if let post_artifacts = self.post {
|
||||||
let nip37_draft = try? post_artifacts.to_nip37_draft(action: .posting(.user(damus_state.pubkey)), damus_state: damus_state)
|
let nip37_draft = try? await post_artifacts.to_nip37_draft(action: .posting(.user(damus_state.pubkey)), damus_state: damus_state)
|
||||||
guard let wrapped_note = nip37_draft?.wrapped_note else { break post_artifact_block }
|
guard let wrapped_note = nip37_draft?.wrapped_note else { break post_artifact_block }
|
||||||
draft_events.append(wrapped_note)
|
draft_events.append(wrapped_note)
|
||||||
}
|
}
|
||||||
for (replied_to_note_id, reply_artifacts) in self.replies {
|
for (replied_to_note_id, reply_artifacts) in self.replies {
|
||||||
guard let replied_to_note = damus_state.ndb.lookup_note(replied_to_note_id)?.unsafeUnownedValue?.to_owned() else { continue }
|
guard let replied_to_note = damus_state.ndb.lookup_note(replied_to_note_id)?.unsafeUnownedValue?.to_owned() else { continue }
|
||||||
let nip37_draft = try? reply_artifacts.to_nip37_draft(action: .replying_to(replied_to_note), damus_state: damus_state)
|
let nip37_draft = try? await reply_artifacts.to_nip37_draft(action: .replying_to(replied_to_note), damus_state: damus_state)
|
||||||
guard let wrapped_note = nip37_draft?.wrapped_note else { continue }
|
guard let wrapped_note = nip37_draft?.wrapped_note else { continue }
|
||||||
draft_events.append(wrapped_note)
|
draft_events.append(wrapped_note)
|
||||||
}
|
}
|
||||||
for (quoted_note_id, quote_note_artifacts) in self.quotes {
|
for (quoted_note_id, quote_note_artifacts) in self.quotes {
|
||||||
guard let quoted_note = damus_state.ndb.lookup_note(quoted_note_id)?.unsafeUnownedValue?.to_owned() else { continue }
|
guard let quoted_note = damus_state.ndb.lookup_note(quoted_note_id)?.unsafeUnownedValue?.to_owned() else { continue }
|
||||||
let nip37_draft = try? quote_note_artifacts.to_nip37_draft(action: .quoting(quoted_note), damus_state: damus_state)
|
let nip37_draft = try? await quote_note_artifacts.to_nip37_draft(action: .quoting(quoted_note), damus_state: damus_state)
|
||||||
guard let wrapped_note = nip37_draft?.wrapped_note else { continue }
|
guard let wrapped_note = nip37_draft?.wrapped_note else { continue }
|
||||||
draft_events.append(wrapped_note)
|
draft_events.append(wrapped_note)
|
||||||
}
|
}
|
||||||
for (highlight, highlight_note_artifacts) in self.highlights {
|
for (highlight, highlight_note_artifacts) in self.highlights {
|
||||||
let nip37_draft = try? highlight_note_artifacts.to_nip37_draft(action: .highlighting(highlight), damus_state: damus_state)
|
let nip37_draft = try? await highlight_note_artifacts.to_nip37_draft(action: .highlighting(highlight), damus_state: damus_state)
|
||||||
guard let wrapped_note = nip37_draft?.wrapped_note else { continue }
|
guard let wrapped_note = nip37_draft?.wrapped_note else { continue }
|
||||||
draft_events.append(wrapped_note)
|
draft_events.append(wrapped_note)
|
||||||
}
|
}
|
||||||
@@ -254,10 +254,12 @@ class Drafts: ObservableObject {
|
|||||||
// TODO: Once it is time to implement draft syncing with relays, please consider the following:
|
// TODO: Once it is time to implement draft syncing with relays, please consider the following:
|
||||||
// - Privacy: Sending drafts to the network leaks metadata about app activity, and may break user expectations
|
// - Privacy: Sending drafts to the network leaks metadata about app activity, and may break user expectations
|
||||||
// - Down-sync conflict resolution: Consider how to solve conflicts for different draft versions holding the same ID (e.g. edited in Damus, then another client, then Damus again)
|
// - Down-sync conflict resolution: Consider how to solve conflicts for different draft versions holding the same ID (e.g. edited in Damus, then another client, then Damus again)
|
||||||
damus_state.nostrNetwork.pool.send_raw_to_local_ndb(.typical(.event(draft_event)))
|
await damus_state.nostrNetwork.sendToNostrDB(event: draft_event)
|
||||||
}
|
}
|
||||||
|
|
||||||
damus_state.settings.draft_event_ids = draft_events.map({ $0.id.hex() })
|
DispatchQueue.main.async {
|
||||||
|
damus_state.settings.draft_event_ids = draft_events.map({ $0.id.hex() })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,14 @@ class PostBox {
|
|||||||
init(pool: RelayPool) {
|
init(pool: RelayPool) {
|
||||||
self.pool = pool
|
self.pool = pool
|
||||||
self.events = [:]
|
self.events = [:]
|
||||||
pool.register_handler(sub_id: "postbox", handler: handle_event)
|
Task {
|
||||||
|
let stream = AsyncStream<(RelayURL, NostrConnectionEvent)> { streamContinuation in
|
||||||
|
Task { await self.pool.register_handler(sub_id: "postbox", filters: nil, to: nil, handler: streamContinuation) }
|
||||||
|
}
|
||||||
|
for await (relayUrl, connectionEvent) in stream {
|
||||||
|
handle_event(relay_id: relayUrl, connectionEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// only works reliably on delay-sent events
|
// only works reliably on delay-sent events
|
||||||
@@ -81,7 +88,7 @@ class PostBox {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func try_flushing_events() {
|
func try_flushing_events() async {
|
||||||
let now = Int64(Date().timeIntervalSince1970)
|
let now = Int64(Date().timeIntervalSince1970)
|
||||||
for kv in events {
|
for kv in events {
|
||||||
let event = kv.value
|
let event = kv.value
|
||||||
@@ -95,7 +102,7 @@ class PostBox {
|
|||||||
if relayer.last_attempt == nil ||
|
if relayer.last_attempt == nil ||
|
||||||
(now >= (relayer.last_attempt! + Int64(relayer.retry_after))) {
|
(now >= (relayer.last_attempt! + Int64(relayer.retry_after))) {
|
||||||
print("attempt #\(relayer.attempts) to flush event '\(event.event.content)' to \(relayer.relay) after \(relayer.retry_after) seconds")
|
print("attempt #\(relayer.attempts) to flush event '\(event.event.content)' to \(relayer.relay) after \(relayer.retry_after) seconds")
|
||||||
flush_event(event, to_relay: relayer)
|
await flush_event(event, to_relay: relayer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -140,7 +147,7 @@ class PostBox {
|
|||||||
return prev_count != after_count
|
return prev_count != after_count
|
||||||
}
|
}
|
||||||
|
|
||||||
private func flush_event(_ event: PostedEvent, to_relay: Relayer? = nil) {
|
private func flush_event(_ event: PostedEvent, to_relay: Relayer? = nil) async {
|
||||||
var relayers = event.remaining
|
var relayers = event.remaining
|
||||||
if let to_relay {
|
if let to_relay {
|
||||||
relayers = [to_relay]
|
relayers = [to_relay]
|
||||||
@@ -150,29 +157,35 @@ class PostBox {
|
|||||||
relayer.attempts += 1
|
relayer.attempts += 1
|
||||||
relayer.last_attempt = Int64(Date().timeIntervalSince1970)
|
relayer.last_attempt = Int64(Date().timeIntervalSince1970)
|
||||||
relayer.retry_after *= 1.5
|
relayer.retry_after *= 1.5
|
||||||
if pool.get_relay(relayer.relay) != nil {
|
if await pool.get_relay(relayer.relay) != nil {
|
||||||
print("flushing event \(event.event.id) to \(relayer.relay)")
|
print("flushing event \(event.event.id) to \(relayer.relay)")
|
||||||
} else {
|
} else {
|
||||||
print("could not find relay when flushing: \(relayer.relay)")
|
print("could not find relay when flushing: \(relayer.relay)")
|
||||||
}
|
}
|
||||||
pool.send(.event(event.event), to: [relayer.relay], skip_ephemeral: event.skip_ephemeral)
|
await pool.send(.event(event.event), to: [relayer.relay], skip_ephemeral: event.skip_ephemeral)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func send(_ event: NostrEvent, to: [RelayURL]? = nil, skip_ephemeral: Bool = true, delay: TimeInterval? = nil, on_flush: OnFlush? = nil) {
|
func send(_ event: NostrEvent, to: [RelayURL]? = nil, skip_ephemeral: Bool = true, delay: TimeInterval? = nil, on_flush: OnFlush? = nil) async {
|
||||||
// Don't add event if we already have it
|
// Don't add event if we already have it
|
||||||
if events[event.id] != nil {
|
if events[event.id] != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let remaining = to ?? pool.our_descriptors.map { $0.url }
|
let remaining: [RelayURL]
|
||||||
|
if let to {
|
||||||
|
remaining = to
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
remaining = await pool.our_descriptors.map { $0.url }
|
||||||
|
}
|
||||||
let after = delay.map { d in Date.now.addingTimeInterval(d) }
|
let after = delay.map { d in Date.now.addingTimeInterval(d) }
|
||||||
let posted_ev = PostedEvent(event: event, remaining: remaining, skip_ephemeral: skip_ephemeral, flush_after: after, on_flush: on_flush)
|
let posted_ev = PostedEvent(event: event, remaining: remaining, skip_ephemeral: skip_ephemeral, flush_after: after, on_flush: on_flush)
|
||||||
|
|
||||||
events[event.id] = posted_ev
|
events[event.id] = posted_ev
|
||||||
|
|
||||||
if after == nil {
|
if after == nil {
|
||||||
flush_event(posted_ev)
|
await flush_event(posted_ev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ struct PostView: View {
|
|||||||
self.prompt_view = prompt_view
|
self.prompt_view = prompt_view
|
||||||
self.placeholder_messages = placeholder_messages ?? [POST_PLACEHOLDER]
|
self.placeholder_messages = placeholder_messages ?? [POST_PLACEHOLDER]
|
||||||
self.initial_text_suffix = initial_text_suffix
|
self.initial_text_suffix = initial_text_suffix
|
||||||
self.autoSaveModel = AutoSaveIndicatorView.AutoSaveViewModel(save: { damus_state.drafts.save(damus_state: damus_state) })
|
self.autoSaveModel = AutoSaveIndicatorView.AutoSaveViewModel(save: { await damus_state.drafts.save(damus_state: damus_state) })
|
||||||
}
|
}
|
||||||
|
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
@@ -121,8 +121,8 @@ struct PostView: View {
|
|||||||
uploadTasks.removeAll()
|
uploadTasks.removeAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
func send_post() {
|
func send_post() async {
|
||||||
let new_post = build_post(state: self.damus_state, post: self.post, action: action, uploadedMedias: uploadedMedias, references: self.references, filtered_pubkeys: filtered_pubkeys)
|
let new_post = await build_post(state: self.damus_state, post: self.post, action: action, uploadedMedias: uploadedMedias, references: self.references, filtered_pubkeys: filtered_pubkeys)
|
||||||
|
|
||||||
notify(.post(.post(new_post)))
|
notify(.post(.post(new_post)))
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ struct PostView: View {
|
|||||||
|
|
||||||
var PostButton: some View {
|
var PostButton: some View {
|
||||||
Button(NSLocalizedString("Post", comment: "Button to post a note.")) {
|
Button(NSLocalizedString("Post", comment: "Button to post a note.")) {
|
||||||
self.send_post()
|
Task { await self.send_post() }
|
||||||
}
|
}
|
||||||
.disabled(posting_disabled)
|
.disabled(posting_disabled)
|
||||||
.opacity(posting_disabled ? 0.5 : 1.0)
|
.opacity(posting_disabled ? 0.5 : 1.0)
|
||||||
@@ -231,7 +231,7 @@ struct PostView: View {
|
|||||||
damus_state.drafts.post = nil
|
damus_state.drafts.post = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
damus_state.drafts.save(damus_state: damus_state)
|
Task{ await damus_state.drafts.save(damus_state: damus_state) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func load_draft() -> Bool {
|
func load_draft() -> Bool {
|
||||||
@@ -388,7 +388,7 @@ struct PostView: View {
|
|||||||
HStack(alignment: .top, spacing: 0) {
|
HStack(alignment: .top, spacing: 0) {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
ProfilePicView(pubkey: damus_state.pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
ProfilePicView(pubkey: damus_state.pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation, damusState: damus_state)
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
if let prompt_view {
|
if let prompt_view {
|
||||||
@@ -829,8 +829,8 @@ func nip10_reply_tags(replying_to: NostrEvent, keypair: Keypair, relayURL: Relay
|
|||||||
return tags
|
return tags
|
||||||
}
|
}
|
||||||
|
|
||||||
func build_post(state: DamusState, action: PostAction, draft: DraftArtifacts) -> NostrPost {
|
func build_post(state: DamusState, action: PostAction, draft: DraftArtifacts) async -> NostrPost {
|
||||||
return build_post(
|
return await build_post(
|
||||||
state: state,
|
state: state,
|
||||||
post: draft.content,
|
post: draft.content,
|
||||||
action: action,
|
action: action,
|
||||||
@@ -840,7 +840,7 @@ func build_post(state: DamusState, action: PostAction, draft: DraftArtifacts) ->
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func build_post(state: DamusState, post: NSAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], references: [RefId], filtered_pubkeys: Set<Pubkey>) -> NostrPost {
|
func build_post(state: DamusState, post: NSAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], references: [RefId], filtered_pubkeys: Set<Pubkey>) async -> NostrPost {
|
||||||
// don't add duplicate pubkeys but retain order
|
// don't add duplicate pubkeys but retain order
|
||||||
var pkset = Set<Pubkey>()
|
var pkset = Set<Pubkey>()
|
||||||
|
|
||||||
@@ -858,7 +858,7 @@ func build_post(state: DamusState, post: NSAttributedString, action: PostAction,
|
|||||||
acc.append(pk)
|
acc.append(pk)
|
||||||
}
|
}
|
||||||
|
|
||||||
return build_post(state: state, post: post, action: action, uploadedMedias: uploadedMedias, pubkeys: pks)
|
return await build_post(state: state, post: post, action: action, uploadedMedias: uploadedMedias, pubkeys: pks)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This builds a Nostr post from draft data from `PostView` or other draft-related classes
|
/// This builds a Nostr post from draft data from `PostView` or other draft-related classes
|
||||||
@@ -874,7 +874,7 @@ func build_post(state: DamusState, post: NSAttributedString, action: PostAction,
|
|||||||
/// - uploadedMedias: The medias attached to this post
|
/// - uploadedMedias: The medias attached to this post
|
||||||
/// - pubkeys: The referenced pubkeys
|
/// - pubkeys: The referenced pubkeys
|
||||||
/// - Returns: A NostrPost, which can then be signed into an event.
|
/// - Returns: A NostrPost, which can then be signed into an event.
|
||||||
func build_post(state: DamusState, post: NSAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], pubkeys: [Pubkey]) -> NostrPost {
|
func build_post(state: DamusState, post: NSAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], pubkeys: [Pubkey]) async -> NostrPost {
|
||||||
let post = NSMutableAttributedString(attributedString: post)
|
let post = NSMutableAttributedString(attributedString: post)
|
||||||
post.enumerateAttributes(in: NSRange(location: 0, length: post.length), options: []) { attributes, range, stop in
|
post.enumerateAttributes(in: NSRange(location: 0, length: post.length), options: []) { attributes, range, stop in
|
||||||
let linkValue = attributes[.link]
|
let linkValue = attributes[.link]
|
||||||
@@ -916,10 +916,10 @@ func build_post(state: DamusState, post: NSAttributedString, action: PostAction,
|
|||||||
switch action {
|
switch action {
|
||||||
case .replying_to(let replying_to):
|
case .replying_to(let replying_to):
|
||||||
// start off with the reply tags
|
// start off with the reply tags
|
||||||
tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair, relayURL: state.nostrNetwork.relaysForEvent(event: replying_to).first)
|
tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair, relayURL: await state.nostrNetwork.relaysForEvent(event: replying_to).first)
|
||||||
|
|
||||||
case .quoting(let ev):
|
case .quoting(let ev):
|
||||||
let relay_urls = state.nostrNetwork.relaysForEvent(event: ev)
|
let relay_urls = await state.nostrNetwork.relaysForEvent(event: ev)
|
||||||
let nevent = Bech32Object.encode(.nevent(NEvent(event: ev, relays: relay_urls.prefix(4).map { $0 })))
|
let nevent = Bech32Object.encode(.nevent(NEvent(event: ev, relays: relay_urls.prefix(4).map { $0 })))
|
||||||
content.append("\n\nnostr:\(nevent)")
|
content.append("\n\nnostr:\(nevent)")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// CondensedProfilePicturesViewModel.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by Daniel D’Aquino on 2025-09-15.
|
||||||
|
//
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class CondensedProfilePicturesViewModel: ObservableObject {
|
||||||
|
let state: DamusState
|
||||||
|
let pubkeys: [Pubkey]
|
||||||
|
let maxPictures: Int
|
||||||
|
var shownPubkeys: [Pubkey] {
|
||||||
|
return Array(pubkeys.prefix(maxPictures))
|
||||||
|
}
|
||||||
|
var loadingTask: Task<Void, Never>? = nil
|
||||||
|
|
||||||
|
init(state: DamusState, pubkeys: [Pubkey], maxPictures: Int) {
|
||||||
|
self.state = state
|
||||||
|
self.pubkeys = pubkeys
|
||||||
|
self.maxPictures = min(maxPictures, pubkeys.count)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,17 +23,21 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let MAX_SHARE_RELAYS = 4
|
||||||
|
|
||||||
var events: EventHolder
|
var events: EventHolder
|
||||||
let pubkey: Pubkey
|
let pubkey: Pubkey
|
||||||
let damus: DamusState
|
let damus: DamusState
|
||||||
|
|
||||||
var seen_event: Set<NoteId> = Set()
|
var seen_event: Set<NoteId> = Set()
|
||||||
var sub_id = UUID().description
|
|
||||||
var prof_subid = UUID().description
|
var findRelaysListener: Task<Void, Never>? = nil
|
||||||
var conversations_subid = UUID().description
|
var listener: Task<Void, Never>? = nil
|
||||||
var findRelay_subid = UUID().description
|
var profileListener: Task<Void, Never>? = nil
|
||||||
|
var conversationListener: Task<Void, Never>? = nil
|
||||||
|
|
||||||
var conversation_events: Set<NoteId> = Set()
|
var conversation_events: Set<NoteId> = Set()
|
||||||
|
|
||||||
init(pubkey: Pubkey, damus: DamusState) {
|
init(pubkey: Pubkey, damus: DamusState) {
|
||||||
self.pubkey = pubkey
|
self.pubkey = pubkey
|
||||||
self.damus = damus
|
self.damus = damus
|
||||||
@@ -46,7 +50,7 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
guard let contacts = self.contacts else {
|
guard let contacts = self.contacts else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return contacts.referenced_pubkeys.contains(pubkey)
|
return contacts.referenced_pubkeys.contains(pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,39 +64,46 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
static func == (lhs: ProfileModel, rhs: ProfileModel) -> Bool {
|
static func == (lhs: ProfileModel, rhs: ProfileModel) -> Bool {
|
||||||
return lhs.pubkey == rhs.pubkey
|
return lhs.pubkey == rhs.pubkey
|
||||||
}
|
}
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(pubkey)
|
hasher.combine(pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func unsubscribe() {
|
func subscribe() {
|
||||||
print("unsubscribing from profile \(pubkey) with sub_id \(sub_id)")
|
print("subscribing to profile \(pubkey)")
|
||||||
damus.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
|
listener?.cancel()
|
||||||
damus.nostrNetwork.pool.unsubscribe(sub_id: prof_subid)
|
listener = Task {
|
||||||
if pubkey != damus.pubkey {
|
var text_filter = NostrFilter(kinds: [.text, .longform, .highlight])
|
||||||
damus.nostrNetwork.pool.unsubscribe(sub_id: conversations_subid)
|
text_filter.authors = [pubkey]
|
||||||
|
text_filter.limit = 500
|
||||||
|
await bumpUpProgress()
|
||||||
|
for await event in damus.nostrNetwork.reader.streamIndefinitely(filters: [text_filter]) {
|
||||||
|
event.justUseACopy({ handleNostrEvent($0) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
profileListener?.cancel()
|
||||||
|
profileListener = Task {
|
||||||
|
var profile_filter = NostrFilter(kinds: [.contacts, .metadata, .boost])
|
||||||
|
var relay_list_filter = NostrFilter(kinds: [.relay_list], authors: [pubkey])
|
||||||
|
profile_filter.authors = [pubkey]
|
||||||
|
await bumpUpProgress()
|
||||||
|
for await event in damus.nostrNetwork.reader.streamIndefinitely(filters: [profile_filter, relay_list_filter]) {
|
||||||
|
event.justUseACopy({ handleNostrEvent($0) })
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
conversationListener?.cancel()
|
||||||
|
conversationListener = Task {
|
||||||
|
await listenToConversations()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func subscribe() {
|
@MainActor
|
||||||
var text_filter = NostrFilter(kinds: [.text, .longform, .highlight])
|
func bumpUpProgress() {
|
||||||
var profile_filter = NostrFilter(kinds: [.contacts, .metadata, .boost])
|
progress += 1
|
||||||
var relay_list_filter = NostrFilter(kinds: [.relay_list], authors: [pubkey])
|
|
||||||
|
|
||||||
profile_filter.authors = [pubkey]
|
|
||||||
|
|
||||||
text_filter.authors = [pubkey]
|
|
||||||
text_filter.limit = 500
|
|
||||||
|
|
||||||
print("subscribing to textlike events from profile \(pubkey) with sub_id \(sub_id)")
|
|
||||||
//print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]])
|
|
||||||
damus.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: [text_filter], handler: handle_event)
|
|
||||||
damus.nostrNetwork.pool.subscribe(sub_id: prof_subid, filters: [profile_filter, relay_list_filter], handler: handle_event)
|
|
||||||
|
|
||||||
subscribe_to_conversations()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func subscribe_to_conversations() {
|
func listenToConversations() async {
|
||||||
// Only subscribe to conversation events if the profile is not us.
|
// Only subscribe to conversation events if the profile is not us.
|
||||||
guard pubkey != damus.pubkey else {
|
guard pubkey != damus.pubkey else {
|
||||||
return
|
return
|
||||||
@@ -102,10 +113,30 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
let limit: UInt32 = 500
|
let limit: UInt32 = 500
|
||||||
let conversations_filter_them = NostrFilter(kinds: conversation_kinds, pubkeys: [damus.pubkey], limit: limit, authors: [pubkey])
|
let conversations_filter_them = NostrFilter(kinds: conversation_kinds, pubkeys: [damus.pubkey], limit: limit, authors: [pubkey])
|
||||||
let conversations_filter_us = NostrFilter(kinds: conversation_kinds, pubkeys: [pubkey], limit: limit, authors: [damus.pubkey])
|
let conversations_filter_us = NostrFilter(kinds: conversation_kinds, pubkeys: [pubkey], limit: limit, authors: [damus.pubkey])
|
||||||
print("subscribing to conversation events from and to profile \(pubkey) with sub_id \(conversations_subid)")
|
print("subscribing to conversation events from and to profile \(pubkey)")
|
||||||
damus.nostrNetwork.pool.subscribe(sub_id: conversations_subid, filters: [conversations_filter_them, conversations_filter_us], handler: handle_event)
|
for await noteLender in self.damus.nostrNetwork.reader.streamIndefinitely(filters: [conversations_filter_them, conversations_filter_us]) {
|
||||||
|
try? noteLender.borrow { ev in
|
||||||
|
if !seen_event.contains(ev.id) {
|
||||||
|
let event = ev.toOwned()
|
||||||
|
Task { await self.add_event(event) }
|
||||||
|
conversation_events.insert(ev.id)
|
||||||
|
}
|
||||||
|
else if !conversation_events.contains(ev.id) {
|
||||||
|
conversation_events.insert(ev.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func unsubscribe() {
|
||||||
|
listener?.cancel()
|
||||||
|
listener = nil
|
||||||
|
profileListener?.cancel()
|
||||||
|
profileListener = nil
|
||||||
|
conversationListener?.cancel()
|
||||||
|
conversationListener = nil
|
||||||
|
}
|
||||||
|
|
||||||
func handle_profile_contact_event(_ ev: NostrEvent) {
|
func handle_profile_contact_event(_ ev: NostrEvent) {
|
||||||
process_contact_event(state: damus, ev: ev)
|
process_contact_event(state: damus, ev: ev)
|
||||||
|
|
||||||
@@ -120,8 +151,13 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
self.following = count_pubkeys(ev.tags)
|
self.following = count_pubkeys(ev.tags)
|
||||||
self.legacy_relay_list = decode_json_relays(ev.content)
|
self.legacy_relay_list = decode_json_relays(ev.content)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func add_event(_ ev: NostrEvent) {
|
||||||
|
guard ev.should_show_event else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
private func add_event(_ ev: NostrEvent) {
|
|
||||||
if ev.is_textlike || ev.known_kind == .boost {
|
if ev.is_textlike || ev.known_kind == .boost {
|
||||||
if self.events.insert(ev) {
|
if self.events.insert(ev) {
|
||||||
self.objectWillChange.send()
|
self.objectWillChange.send()
|
||||||
@@ -134,72 +170,13 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
}
|
}
|
||||||
seen_event.insert(ev.id)
|
seen_event.insert(ev.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the event public key matches the public key(s) we are querying.
|
private func handleNostrEvent(_ ev: NostrEvent) {
|
||||||
// This is done to protect against a relay not properly filtering events by the pubkey
|
// Ensure the event public key matches this profiles public key
|
||||||
// See https://github.com/damus-io/damus/issues/1846 for more information
|
// This is done to protect against a relay not properly filtering events by the pubkey
|
||||||
private func relay_filtered_correctly(_ ev: NostrEvent, subid: String?) -> Bool {
|
// See https://github.com/damus-io/damus/issues/1846 for more information
|
||||||
if subid == self.conversations_subid {
|
guard self.pubkey == ev.pubkey else { return }
|
||||||
switch ev.pubkey {
|
Task { await add_event(ev) }
|
||||||
case self.pubkey:
|
|
||||||
return ev.referenced_pubkeys.contains(damus.pubkey)
|
|
||||||
case damus.pubkey:
|
|
||||||
return ev.referenced_pubkeys.contains(self.pubkey)
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.pubkey == ev.pubkey
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
|
||||||
switch ev {
|
|
||||||
case .ws_connection_event:
|
|
||||||
return
|
|
||||||
case .nostr_event(let resp):
|
|
||||||
guard resp.subid == self.sub_id || resp.subid == self.prof_subid || resp.subid == self.conversations_subid else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch resp {
|
|
||||||
case .ok:
|
|
||||||
break
|
|
||||||
case .event(_, let ev):
|
|
||||||
guard ev.should_show_event else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if !seen_event.contains(ev.id) {
|
|
||||||
guard relay_filtered_correctly(ev, subid: resp.subid) else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
add_event(ev)
|
|
||||||
|
|
||||||
if resp.subid == self.conversations_subid {
|
|
||||||
conversation_events.insert(ev.id)
|
|
||||||
}
|
|
||||||
} else if resp.subid == self.conversations_subid && !conversation_events.contains(ev.id) {
|
|
||||||
guard relay_filtered_correctly(ev, subid: resp.subid) else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
conversation_events.insert(ev.id)
|
|
||||||
}
|
|
||||||
case .notice:
|
|
||||||
break
|
|
||||||
//notify(.notice, notice)
|
|
||||||
case .eose:
|
|
||||||
guard let txn = NdbTxn(ndb: damus.ndb) else { return }
|
|
||||||
if resp.subid == sub_id {
|
|
||||||
load_profiles(context: "profile", profiles_subid: prof_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus, txn: txn)
|
|
||||||
}
|
|
||||||
progress += 1
|
|
||||||
break
|
|
||||||
case .auth:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func findRelaysHandler(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
private func findRelaysHandler(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
||||||
@@ -211,12 +188,22 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
func subscribeToFindRelays() {
|
func subscribeToFindRelays() {
|
||||||
var profile_filter = NostrFilter(kinds: [.contacts])
|
var profile_filter = NostrFilter(kinds: [.contacts])
|
||||||
profile_filter.authors = [pubkey]
|
profile_filter.authors = [pubkey]
|
||||||
|
self.findRelaysListener?.cancel()
|
||||||
damus.nostrNetwork.pool.subscribe(sub_id: findRelay_subid, filters: [profile_filter], handler: findRelaysHandler)
|
self.findRelaysListener = Task {
|
||||||
|
for await noteLender in damus.nostrNetwork.reader.streamIndefinitely(filters: [profile_filter]) {
|
||||||
|
try? noteLender.borrow { event in
|
||||||
|
if case .contacts = event.known_kind {
|
||||||
|
// TODO: Is this correct?
|
||||||
|
self.legacy_relay_list = decode_json_relays(event.content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func unsubscribeFindRelays() {
|
func unsubscribeFindRelays() {
|
||||||
damus.nostrNetwork.pool.unsubscribe(sub_id: findRelay_subid)
|
self.findRelaysListener?.cancel()
|
||||||
|
self.findRelaysListener = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCappedRelays() -> [RelayURL] {
|
func getCappedRelays() -> [RelayURL] {
|
||||||
|
|||||||
@@ -8,26 +8,22 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct CondensedProfilePicturesView: View {
|
struct CondensedProfilePicturesView: View {
|
||||||
let state: DamusState
|
let model: CondensedProfilePicturesViewModel
|
||||||
let pubkeys: [Pubkey]
|
|
||||||
let maxPictures: Int
|
|
||||||
|
|
||||||
init(state: DamusState, pubkeys: [Pubkey], maxPictures: Int) {
|
init(state: DamusState, pubkeys: [Pubkey], maxPictures: Int) {
|
||||||
self.state = state
|
self.model = CondensedProfilePicturesViewModel(state: state, pubkeys: pubkeys, maxPictures: maxPictures)
|
||||||
self.pubkeys = pubkeys
|
|
||||||
self.maxPictures = min(maxPictures, pubkeys.count)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
// Using ZStack to make profile pictures floating and stacked on top of each other.
|
// Using ZStack to make profile pictures floating and stacked on top of each other.
|
||||||
ZStack {
|
ZStack {
|
||||||
ForEach((0..<maxPictures).reversed(), id: \.self) { index in
|
ForEach((0..<model.maxPictures).reversed(), id: \.self) { index in
|
||||||
ProfilePicView(pubkey: pubkeys[index], size: 32.0, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation)
|
ProfilePicView(pubkey: model.pubkeys[index], size: 32.0, highlight: .none, profiles: model.state.profiles, disable_animation: model.state.settings.disable_animation, damusState: model.state)
|
||||||
.offset(x: CGFloat(index) * 20)
|
.offset(x: CGFloat(index) * 20)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Padding is needed so that other components drawn adjacent to this view don't get drawn on top.
|
// Padding is needed so that other components drawn adjacent to this view don't get drawn on top.
|
||||||
.padding(.trailing, CGFloat((maxPictures - 1) * 20))
|
.padding(.trailing, CGFloat((model.maxPictures - 1) * 20))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ struct EditMetadataView: View {
|
|||||||
return profile
|
return profile
|
||||||
}
|
}
|
||||||
|
|
||||||
func save() {
|
func save() async {
|
||||||
let profile = to_profile()
|
let profile = to_profile()
|
||||||
guard let keypair = damus_state.keypair.to_full(),
|
guard let keypair = damus_state.keypair.to_full(),
|
||||||
let metadata_ev = make_metadata_event(keypair: keypair, metadata: profile)
|
let metadata_ev = make_metadata_event(keypair: keypair, metadata: profile)
|
||||||
@@ -66,7 +66,7 @@ struct EditMetadataView: View {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
damus_state.nostrNetwork.postbox.send(metadata_ev)
|
await damus_state.nostrNetwork.postbox.send(metadata_ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
func is_ln_valid(ln: String) -> Bool {
|
func is_ln_valid(ln: String) -> Bool {
|
||||||
@@ -211,8 +211,10 @@ struct EditMetadataView: View {
|
|||||||
if !ln.isEmpty && !is_ln_valid(ln: ln) {
|
if !ln.isEmpty && !is_ln_valid(ln: ln) {
|
||||||
confirm_ln_address = true
|
confirm_ln_address = true
|
||||||
} else {
|
} else {
|
||||||
save()
|
Task {
|
||||||
dismiss()
|
await save()
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, label: {
|
}, label: {
|
||||||
Text(NSLocalizedString("Save", comment: "Button for saving profile."))
|
Text(NSLocalizedString("Save", comment: "Button for saving profile."))
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ struct EventProfileName: View {
|
|||||||
@State var nip05: NIP05?
|
@State var nip05: NIP05?
|
||||||
@State var donation: Int?
|
@State var donation: Int?
|
||||||
@State var purple_account: DamusPurple.Account?
|
@State var purple_account: DamusPurple.Account?
|
||||||
|
@StateObject private var profileObserver: ProfileObserver
|
||||||
|
|
||||||
let size: EventViewKind
|
let size: EventViewKind
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ struct EventProfileName: View {
|
|||||||
let donation = damus.ndb.lookup_profile(pubkey)?.map({ p in p?.profile?.damus_donation }).value
|
let donation = damus.ndb.lookup_profile(pubkey)?.map({ p in p?.profile?.damus_donation }).value
|
||||||
self._donation = State(wrappedValue: donation)
|
self._donation = State(wrappedValue: donation)
|
||||||
self.purple_account = nil
|
self.purple_account = nil
|
||||||
|
self._profileObserver = StateObject.init(wrappedValue: ProfileObserver(pubkey: pubkey, damusState: damus))
|
||||||
}
|
}
|
||||||
|
|
||||||
var friend_type: FriendType? {
|
var friend_type: FriendType? {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ struct MaybeAnonPfpView: View {
|
|||||||
.font(.largeTitle)
|
.font(.largeTitle)
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
} else {
|
} else {
|
||||||
ProfilePicView(pubkey: pubkey, size: size, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation, show_zappability: true)
|
ProfilePicView(pubkey: pubkey, size: size, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation, show_zappability: true, damusState: state)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
show_profile_action_sheet_if_enabled(damus_state: state, pubkey: pubkey)
|
show_profile_action_sheet_if_enabled(damus_state: state, pubkey: pubkey)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ struct ProfileActionSheetView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .center) {
|
VStack(alignment: .center) {
|
||||||
ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation, damusState: damus_state)
|
||||||
if let url = self.profile_data()?.profile?.website_url {
|
if let url = self.profile_data()?.profile?.website_url {
|
||||||
WebsiteLink(url: url, style: .accent)
|
WebsiteLink(url: url, style: .accent)
|
||||||
.padding(.top, -15)
|
.padding(.top, -15)
|
||||||
@@ -310,7 +310,7 @@ fileprivate struct ProfileActionSheetZapButton: View {
|
|||||||
VStack(alignment: .center, spacing: 10) {
|
VStack(alignment: .center, spacing: 10) {
|
||||||
Button(
|
Button(
|
||||||
action: {
|
action: {
|
||||||
send_zap(damus_state: damus_state, target: .profile(self.profile.pubkey), lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type)
|
Task { await send_zap(damus_state: damus_state, target: .profile(self.profile.pubkey), lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type) }
|
||||||
zap_state = .zapping
|
zap_state = .zapping
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ struct ProfileName: View {
|
|||||||
@State var donation: Int?
|
@State var donation: Int?
|
||||||
@State var purple_account: DamusPurple.Account?
|
@State var purple_account: DamusPurple.Account?
|
||||||
@State var nip05_domain_favicon: FaviconURL?
|
@State var nip05_domain_favicon: FaviconURL?
|
||||||
|
@StateObject var profileObserver: ProfileObserver
|
||||||
|
|
||||||
init(pubkey: Pubkey, prefix: String = "", damus: DamusState, show_nip5_domain: Bool = true, supporterBadgeStyle: SupporterBadge.Style = .compact) {
|
init(pubkey: Pubkey, prefix: String = "", damus: DamusState, show_nip5_domain: Bool = true, supporterBadgeStyle: SupporterBadge.Style = .compact) {
|
||||||
self.pubkey = pubkey
|
self.pubkey = pubkey
|
||||||
@@ -53,6 +54,7 @@ struct ProfileName: View {
|
|||||||
self.show_nip5_domain = show_nip5_domain
|
self.show_nip5_domain = show_nip5_domain
|
||||||
self.supporterBadgeStyle = supporterBadgeStyle
|
self.supporterBadgeStyle = supporterBadgeStyle
|
||||||
self.purple_account = nil
|
self.purple_account = nil
|
||||||
|
self._profileObserver = StateObject.init(wrappedValue: ProfileObserver(pubkey: pubkey, damusState: damus))
|
||||||
}
|
}
|
||||||
|
|
||||||
var friend_type: FriendType? {
|
var friend_type: FriendType? {
|
||||||
|
|||||||
@@ -75,8 +75,10 @@ struct ProfilePicView: View {
|
|||||||
let privacy_sensitive: Bool
|
let privacy_sensitive: Bool
|
||||||
|
|
||||||
@State var picture: String?
|
@State var picture: String?
|
||||||
|
@StateObject private var profileObserver: ProfileObserver
|
||||||
|
@EnvironmentObject var damusState: DamusState
|
||||||
|
|
||||||
init(pubkey: Pubkey, size: CGFloat, highlight: Highlight, profiles: Profiles, disable_animation: Bool, picture: String? = nil, show_zappability: Bool? = nil, privacy_sensitive: Bool = false) {
|
init(pubkey: Pubkey, size: CGFloat, highlight: Highlight, profiles: Profiles, disable_animation: Bool, picture: String? = nil, show_zappability: Bool? = nil, privacy_sensitive: Bool = false, damusState: DamusState) {
|
||||||
self.pubkey = pubkey
|
self.pubkey = pubkey
|
||||||
self.profiles = profiles
|
self.profiles = profiles
|
||||||
self.size = size
|
self.size = size
|
||||||
@@ -85,6 +87,7 @@ struct ProfilePicView: View {
|
|||||||
self.disable_animation = disable_animation
|
self.disable_animation = disable_animation
|
||||||
self.zappability_indicator = show_zappability ?? false
|
self.zappability_indicator = show_zappability ?? false
|
||||||
self.privacy_sensitive = privacy_sensitive
|
self.privacy_sensitive = privacy_sensitive
|
||||||
|
self._profileObserver = StateObject.init(wrappedValue: ProfileObserver(pubkey: pubkey, damusState: damusState))
|
||||||
}
|
}
|
||||||
|
|
||||||
var privacy_sensitive_pubkey: Pubkey {
|
var privacy_sensitive_pubkey: Pubkey {
|
||||||
@@ -163,7 +166,8 @@ struct ProfilePicView_Previews: PreviewProvider {
|
|||||||
size: 100,
|
size: 100,
|
||||||
highlight: .none,
|
highlight: .none,
|
||||||
profiles: make_preview_profiles(pubkey),
|
profiles: make_preview_profiles(pubkey),
|
||||||
disable_animation: false
|
disable_animation: false,
|
||||||
|
damusState: test_damus_state
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ struct ProfileView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
damus_state.mutelist_manager.set_mutelist(new_ev)
|
damus_state.mutelist_manager.set_mutelist(new_ev)
|
||||||
damus_state.nostrNetwork.postbox.send(new_ev)
|
Task { await damus_state.nostrNetwork.postbox.send(new_ev) }
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Button(NSLocalizedString("Mute", comment: "Button to mute a profile"), role: .destructive) {
|
Button(NSLocalizedString("Mute", comment: "Button to mute a profile"), role: .destructive) {
|
||||||
@@ -313,7 +313,7 @@ struct ProfileView: View {
|
|||||||
let follows_you = profile.pubkey != damus_state.pubkey && profile.follows(pubkey: damus_state.pubkey)
|
let follows_you = profile.pubkey != damus_state.pubkey && profile.follows(pubkey: damus_state.pubkey)
|
||||||
|
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation, damusState: damus_state)
|
||||||
.padding(.top, -(pfp_size / 2.0))
|
.padding(.top, -(pfp_size / 2.0))
|
||||||
.offset(y: pfpOffset())
|
.offset(y: pfpOffset())
|
||||||
.scaleEffect(pfpScale())
|
.scaleEffect(pfpScale())
|
||||||
@@ -589,3 +589,4 @@ func check_nip05_validity(pubkey: Pubkey, profiles: Profiles) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ struct DamusPurpleAccountView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack {
|
||||||
ProfilePicView(pubkey: account.pubkey, size: pfp_size, highlight: .custom(Color.black.opacity(0.4), 1.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
ProfilePicView(pubkey: account.pubkey, size: pfp_size, highlight: .custom(Color.black.opacity(0.4), 1.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation, damusState: damus_state)
|
||||||
.background(Color.black.opacity(0.4).clipShape(Circle()))
|
.background(Color.black.opacity(0.4).clipShape(Circle()))
|
||||||
.shadow(color: .black, radius: 10, x: 0.0, y: 5)
|
.shadow(color: .black, radius: 10, x: 0.0, y: 5)
|
||||||
|
|
||||||
|
|||||||
@@ -84,9 +84,3 @@ func load_relay_filters(_ pubkey: Pubkey) -> Set<RelayFilter>? {
|
|||||||
s.insert(filter)
|
s.insert(filter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func determine_to_relays(pool: RelayPool, filters: RelayFilters) -> [RelayURL] {
|
|
||||||
return pool.our_descriptors
|
|
||||||
.map { $0.url }
|
|
||||||
.filter { !filters.is_filtered(timeline: .search, relay_id: $0) }
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -80,30 +80,32 @@ struct AddRelayView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
if new_relay.starts(with: "wss://") == false && new_relay.starts(with: "ws://") == false {
|
Task {
|
||||||
new_relay = "wss://" + new_relay
|
if new_relay.starts(with: "wss://") == false && new_relay.starts(with: "ws://") == false {
|
||||||
|
new_relay = "wss://" + new_relay
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let url = RelayURL(new_relay) else {
|
||||||
|
relayAddErrorTitle = NSLocalizedString("Invalid relay address", comment: "Heading for an error when adding a relay")
|
||||||
|
relayAddErrorMessage = NSLocalizedString("Please check the address and try again", comment: "Tip for an error where the relay address being added is invalid")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try await state.nostrNetwork.userRelayList.insert(relay: NIP65.RelayList.RelayItem(url: url, rwConfiguration: .readWrite))
|
||||||
|
relayAddErrorTitle = nil // Clear error title
|
||||||
|
relayAddErrorMessage = nil // Clear error message
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
present_sheet(.error(self.humanReadableError(for: error)))
|
||||||
|
}
|
||||||
|
|
||||||
|
new_relay = ""
|
||||||
|
|
||||||
|
this_app.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||||
|
|
||||||
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let url = RelayURL(new_relay) else {
|
|
||||||
relayAddErrorTitle = NSLocalizedString("Invalid relay address", comment: "Heading for an error when adding a relay")
|
|
||||||
relayAddErrorMessage = NSLocalizedString("Please check the address and try again", comment: "Tip for an error where the relay address being added is invalid")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
try state.nostrNetwork.userRelayList.insert(relay: NIP65.RelayList.RelayItem(url: url, rwConfiguration: .readWrite))
|
|
||||||
relayAddErrorTitle = nil // Clear error title
|
|
||||||
relayAddErrorMessage = nil // Clear error message
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
present_sheet(.error(self.humanReadableError(for: error)))
|
|
||||||
}
|
|
||||||
|
|
||||||
new_relay = ""
|
|
||||||
|
|
||||||
this_app.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
|
||||||
|
|
||||||
dismiss()
|
|
||||||
}) {
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Add relay", comment: "Button to add a relay.")
|
Text("Add relay", comment: "Button to add a relay.")
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ struct RelayAdminDetail: View {
|
|||||||
.fontWeight(.heavy)
|
.fontWeight(.heavy)
|
||||||
.foregroundColor(DamusColors.mediumGrey)
|
.foregroundColor(DamusColors.mediumGrey)
|
||||||
if let pubkey = nip11?.pubkey {
|
if let pubkey = nip11?.pubkey {
|
||||||
ProfilePicView(pubkey: pubkey, size: 40, highlight: .custom(.gray.opacity(0.5), 1), profiles: state.profiles, disable_animation: state.settings.disable_animation)
|
ProfilePicView(pubkey: pubkey, size: 40, highlight: .custom(.gray.opacity(0.5), 1), profiles: state.profiles, disable_animation: state.settings.disable_animation, damusState: state)
|
||||||
.padding(.bottom, 5)
|
.padding(.bottom, 5)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
state.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
|
state.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ struct RelayConfigView: View {
|
|||||||
|
|
||||||
init(state: DamusState) {
|
init(state: DamusState) {
|
||||||
self.state = state
|
self.state = state
|
||||||
_relays = State(initialValue: state.nostrNetwork.pool.our_descriptors)
|
_relays = State(initialValue: state.nostrNetwork.ourRelayDescriptors)
|
||||||
UITabBar.appearance().isHidden = true
|
UITabBar.appearance().isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ struct RelayConfigView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.relays_changed)) { _ in
|
.onReceive(handle_notify(.relays_changed)) { _ in
|
||||||
self.relays = state.nostrNetwork.pool.our_descriptors
|
self.relays = state.nostrNetwork.ourRelayDescriptors
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
notify(.display_tabbar(false))
|
notify(.display_tabbar(false))
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ struct RelayDetailView: View {
|
|||||||
|
|
||||||
func RemoveRelayButton(_ keypair: FullKeypair) -> some View {
|
func RemoveRelayButton(_ keypair: FullKeypair) -> some View {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
self.removeRelay()
|
Task { await self.removeRelay() }
|
||||||
}) {
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Disconnect", comment: "Button to disconnect from the relay.")
|
Text("Disconnect", comment: "Button to disconnect from the relay.")
|
||||||
@@ -43,7 +43,7 @@ struct RelayDetailView: View {
|
|||||||
|
|
||||||
func ConnectRelayButton(_ keypair: FullKeypair) -> some View {
|
func ConnectRelayButton(_ keypair: FullKeypair) -> some View {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
self.connectRelay()
|
Task { await self.connectRelay() }
|
||||||
}) {
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Connect", comment: "Button to connect to the relay.")
|
Text("Connect", comment: "Button to connect to the relay.")
|
||||||
@@ -177,16 +177,18 @@ struct RelayDetailView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var relay_object: RelayPool.Relay? {
|
private var relay_object: RelayPool.Relay? {
|
||||||
state.nostrNetwork.pool.get_relay(relay)
|
// TODO: Concurrency problems?
|
||||||
|
state.nostrNetwork.connectedRelays.first(where: { $0.descriptor.url == relay })
|
||||||
}
|
}
|
||||||
|
|
||||||
private var relay_connection: RelayConnection? {
|
private var relay_connection: RelayConnection? {
|
||||||
relay_object?.connection
|
relay_object?.connection
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeRelay() {
|
func removeRelay() async {
|
||||||
do {
|
do {
|
||||||
try state.nostrNetwork.userRelayList.remove(relayURL: self.relay)
|
// TODO: Concurrency problems?
|
||||||
|
try await state.nostrNetwork.userRelayList.remove(relayURL: self.relay)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
@@ -194,9 +196,10 @@ struct RelayDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func connectRelay() {
|
func connectRelay() async {
|
||||||
do {
|
do {
|
||||||
try state.nostrNetwork.userRelayList.insert(relay: NIP65.RelayList.RelayItem(url: relay, rwConfiguration: .readWrite))
|
// TODO: Concurrency problems?
|
||||||
|
try await state.nostrNetwork.userRelayList.insert(relay: NIP65.RelayList.RelayItem(url: relay, rwConfiguration: .readWrite))
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ struct RelayFilterView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var relays: [RelayPool.RelayDescriptor] {
|
var relays: [RelayPool.RelayDescriptor] {
|
||||||
return state.nostrNetwork.pool.our_descriptors
|
return state.nostrNetwork.ourRelayDescriptors
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ struct RelayStatusView: View {
|
|||||||
|
|
||||||
struct RelayStatusView_Previews: PreviewProvider {
|
struct RelayStatusView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let connection = test_damus_state.nostrNetwork.pool.get_relay(RelayURL("wss://relay.damus.io")!)!.connection
|
let connection = test_damus_state.nostrNetwork.getRelay(RelayURL("wss://relay.damus.io")!)!.connection
|
||||||
RelayStatusView(connection: connection)
|
RelayStatusView(connection: connection)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ struct RelayToggle: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var relay_connection: RelayConnection? {
|
private var relay_connection: RelayConnection? {
|
||||||
state.nostrNetwork.pool.get_relay(relay_id)?.connection
|
state.nostrNetwork.getRelay(relay_id)?.connection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,13 +24,13 @@ struct RelayView: View {
|
|||||||
self.recommended = recommended
|
self.recommended = recommended
|
||||||
self.model_cache = state.relay_model_cache
|
self.model_cache = state.relay_model_cache
|
||||||
_showActionButtons = showActionButtons
|
_showActionButtons = showActionButtons
|
||||||
let relay_state = RelayView.get_relay_state(pool: state.nostrNetwork.pool, relay: relay)
|
let relay_state = RelayView.get_relay_state(state: state, relay: relay)
|
||||||
self._relay_state = State(initialValue: relay_state)
|
self._relay_state = State(initialValue: relay_state)
|
||||||
self.disableNavLink = disableNavLink
|
self.disableNavLink = disableNavLink
|
||||||
}
|
}
|
||||||
|
|
||||||
static func get_relay_state(pool: RelayPool, relay: RelayURL) -> Bool {
|
static func get_relay_state(state: DamusState, relay: RelayURL) -> Bool {
|
||||||
return pool.get_relay(relay) == nil
|
return state.nostrNetwork.getRelay(relay) == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -110,7 +110,7 @@ struct RelayView: View {
|
|||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.relays_changed)) { _ in
|
.onReceive(handle_notify(.relays_changed)) { _ in
|
||||||
self.relay_state = RelayView.get_relay_state(pool: state.nostrNetwork.pool, relay: self.relay)
|
self.relay_state = RelayView.get_relay_state(state: state, relay: self.relay)
|
||||||
}
|
}
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if !disableNavLink {
|
if !disableNavLink {
|
||||||
@@ -120,7 +120,7 @@ struct RelayView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var relay_connection: RelayConnection? {
|
private var relay_connection: RelayConnection? {
|
||||||
state.nostrNetwork.pool.get_relay(relay)?.connection
|
state.nostrNetwork.getRelay(relay)?.connection
|
||||||
}
|
}
|
||||||
|
|
||||||
func add_action(keypair: FullKeypair) async {
|
func add_action(keypair: FullKeypair) async {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ struct UserRelaysView: View {
|
|||||||
|
|
||||||
static func make_relay_state(state: DamusState, relays: [RelayURL]) -> [(RelayURL, Bool)] {
|
static func make_relay_state(state: DamusState, relays: [RelayURL]) -> [(RelayURL, Bool)] {
|
||||||
return relays.map({ r in
|
return relays.map({ r in
|
||||||
return (r, state.nostrNetwork.pool.get_relay(r) == nil)
|
return (r, state.nostrNetwork.getRelay(r) == nil)
|
||||||
}).sorted { (a, b) in a.0 < b.0 }
|
}).sorted { (a, b) in a.0 < b.0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,14 +10,16 @@ import Foundation
|
|||||||
/// The data model for the SearchHome view, typically something global-like
|
/// The data model for the SearchHome view, typically something global-like
|
||||||
class SearchHomeModel: ObservableObject {
|
class SearchHomeModel: ObservableObject {
|
||||||
var events: EventHolder
|
var events: EventHolder
|
||||||
|
var followPackEvents: EventHolder
|
||||||
@Published var loading: Bool = false
|
@Published var loading: Bool = false
|
||||||
|
|
||||||
var seen_pubkey: Set<Pubkey> = Set()
|
var seen_pubkey: Set<Pubkey> = Set()
|
||||||
|
var follow_pack_seen_pubkey: Set<Pubkey> = Set()
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
let base_subid = UUID().description
|
let base_subid = UUID().description
|
||||||
let follow_pack_subid = UUID().description
|
let follow_pack_subid = UUID().description
|
||||||
let profiles_subid = UUID().description
|
let profiles_subid = UUID().description
|
||||||
let limit: UInt32 = 500
|
let limit: UInt32 = 200
|
||||||
//let multiple_events_per_pubkey: Bool = false
|
//let multiple_events_per_pubkey: Bool = false
|
||||||
|
|
||||||
init(damus_state: DamusState) {
|
init(damus_state: DamusState) {
|
||||||
@@ -25,6 +27,9 @@ class SearchHomeModel: ObservableObject {
|
|||||||
self.events = EventHolder(on_queue: { ev in
|
self.events = EventHolder(on_queue: { ev in
|
||||||
preload_events(state: damus_state, events: [ev])
|
preload_events(state: damus_state, events: [ev])
|
||||||
})
|
})
|
||||||
|
self.followPackEvents = EventHolder(on_queue: { ev in
|
||||||
|
preload_events(state: damus_state, events: [ev])
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func get_base_filter() -> NostrFilter {
|
func get_base_filter() -> NostrFilter {
|
||||||
@@ -34,68 +39,73 @@ class SearchHomeModel: ObservableObject {
|
|||||||
return filter
|
return filter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func filter_muted() {
|
func filter_muted() {
|
||||||
events.filter { should_show_event(state: damus_state, ev: $0) }
|
events.filter { should_show_event(state: damus_state, ev: $0) }
|
||||||
self.objectWillChange.send()
|
self.objectWillChange.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
func subscribe() {
|
@MainActor
|
||||||
loading = true
|
func reload() async {
|
||||||
let to_relays = determine_to_relays(pool: damus_state.nostrNetwork.pool, filters: damus_state.relay_filters)
|
self.events.reset()
|
||||||
|
await self.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
func load() async {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.loading = true
|
||||||
|
}
|
||||||
|
let to_relays = await damus_state.nostrNetwork.ourRelayDescriptors
|
||||||
|
.map { $0.url }
|
||||||
|
.filter { !damus_state.relay_filters.is_filtered(timeline: .search, relay_id: $0) }
|
||||||
|
|
||||||
var follow_list_filter = NostrFilter(kinds: [.follow_list])
|
var follow_list_filter = NostrFilter(kinds: [.follow_list])
|
||||||
follow_list_filter.until = UInt32(Date.now.timeIntervalSince1970)
|
follow_list_filter.until = UInt32(Date.now.timeIntervalSince1970)
|
||||||
|
|
||||||
damus_state.nostrNetwork.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event, to: to_relays)
|
for await item in damus_state.nostrNetwork.reader.advancedStream(filters: [get_base_filter(), follow_list_filter], to: to_relays) {
|
||||||
damus_state.nostrNetwork.pool.subscribe(sub_id: follow_pack_subid, filters: [follow_list_filter], handler: handle_event, to: to_relays)
|
switch item {
|
||||||
}
|
case .event(lender: let lender):
|
||||||
|
await lender.justUseACopy({ event in
|
||||||
func unsubscribe(to: RelayURL? = nil) {
|
await self.handleFollowPackEvent(event)
|
||||||
loading = false
|
await self.handleEvent(event)
|
||||||
damus_state.nostrNetwork.pool.unsubscribe(sub_id: base_subid, to: to.map { [$0] })
|
})
|
||||||
damus_state.nostrNetwork.pool.unsubscribe(sub_id: follow_pack_subid, to: to.map { [$0] })
|
case .eose:
|
||||||
}
|
break
|
||||||
|
case .ndbEose:
|
||||||
func handle_event(relay_id: RelayURL, conn_ev: NostrConnectionEvent) {
|
DispatchQueue.main.async {
|
||||||
guard case .nostr_event(let event) = conn_ev else {
|
self.loading = false
|
||||||
return
|
}
|
||||||
|
case .networkEose:
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
switch event {
|
|
||||||
case .event(let sub_id, let ev):
|
@MainActor
|
||||||
guard sub_id == self.base_subid || sub_id == self.profiles_subid || sub_id == self.follow_pack_subid else {
|
func handleEvent(_ ev: NostrEvent) {
|
||||||
|
if ev.is_textlike && should_show_event(state: damus_state, ev: ev) && !ev.is_reply() {
|
||||||
|
if !damus_state.settings.multiple_events_per_pubkey && seen_pubkey.contains(ev.pubkey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ev.is_textlike && should_show_event(state: damus_state, ev: ev) && !ev.is_reply()
|
seen_pubkey.insert(ev.pubkey)
|
||||||
{
|
|
||||||
if !damus_state.settings.multiple_events_per_pubkey && seen_pubkey.contains(ev.pubkey) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
seen_pubkey.insert(ev.pubkey)
|
|
||||||
|
|
||||||
if self.events.insert(ev) {
|
|
||||||
self.objectWillChange.send()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case .notice(let msg):
|
|
||||||
print("search home notice: \(msg)")
|
|
||||||
case .ok:
|
|
||||||
break
|
|
||||||
case .eose(let sub_id):
|
|
||||||
loading = false
|
|
||||||
|
|
||||||
if sub_id == self.base_subid {
|
if self.events.insert(ev) {
|
||||||
// Make sure we unsubscribe after we've fetched the global events
|
self.objectWillChange.send()
|
||||||
// global events are not realtime
|
}
|
||||||
unsubscribe(to: relay_id)
|
}
|
||||||
|
}
|
||||||
guard let txn = NdbTxn(ndb: damus_state.ndb) else { return }
|
|
||||||
load_profiles(context: "universe", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.all_events), damus_state: damus_state, txn: txn)
|
@MainActor
|
||||||
|
func handleFollowPackEvent(_ ev: NostrEvent) {
|
||||||
|
if ev.known_kind == .follow_list && should_show_event(state: damus_state, ev: ev) && !ev.is_reply() {
|
||||||
|
if !damus_state.settings.multiple_events_per_pubkey && follow_pack_seen_pubkey.contains(ev.pubkey) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
follow_pack_seen_pubkey.insert(ev.pubkey)
|
||||||
|
|
||||||
|
if self.followPackEvents.insert(ev) {
|
||||||
|
self.objectWillChange.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
|
||||||
case .auth:
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,45 +144,3 @@ enum PubkeysToLoad {
|
|||||||
case from_events([NostrEvent])
|
case from_events([NostrEvent])
|
||||||
case from_keys([Pubkey])
|
case from_keys([Pubkey])
|
||||||
}
|
}
|
||||||
|
|
||||||
func load_profiles<Y>(context: String, profiles_subid: String, relay_id: RelayURL, load: PubkeysToLoad, damus_state: DamusState, txn: NdbTxn<Y>) {
|
|
||||||
let authors = find_profiles_to_fetch(profiles: damus_state.profiles, load: load, cache: damus_state.events, txn: txn)
|
|
||||||
|
|
||||||
guard !authors.isEmpty else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
print("load_profiles[\(context)]: requesting \(authors.count) profiles from \(relay_id)")
|
|
||||||
|
|
||||||
let filter = NostrFilter(kinds: [.metadata], authors: authors)
|
|
||||||
|
|
||||||
damus_state.nostrNetwork.pool.subscribe_to(sub_id: profiles_subid, filters: [filter], to: [relay_id]) { rid, conn_ev in
|
|
||||||
|
|
||||||
let now = UInt64(Date.now.timeIntervalSince1970)
|
|
||||||
switch conn_ev {
|
|
||||||
case .ws_connection_event:
|
|
||||||
break
|
|
||||||
case .nostr_event(let ev):
|
|
||||||
guard ev.subid == profiles_subid, rid == relay_id else { return }
|
|
||||||
|
|
||||||
switch ev {
|
|
||||||
case .event(_, let ev):
|
|
||||||
if ev.known_kind == .metadata {
|
|
||||||
damus_state.ndb.write_profile_last_fetched(pubkey: ev.pubkey, fetched_at: now)
|
|
||||||
}
|
|
||||||
case .eose:
|
|
||||||
print("load_profiles[\(context)]: done loading \(authors.count) profiles from \(relay_id)")
|
|
||||||
damus_state.nostrNetwork.pool.unsubscribe(sub_id: profiles_subid, to: [relay_id])
|
|
||||||
case .ok:
|
|
||||||
break
|
|
||||||
case .notice:
|
|
||||||
break
|
|
||||||
case .auth:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ class SearchModel: ObservableObject {
|
|||||||
@Published var loading: Bool = false
|
@Published var loading: Bool = false
|
||||||
|
|
||||||
var search: NostrFilter
|
var search: NostrFilter
|
||||||
let sub_id = UUID().description
|
|
||||||
let profiles_subid = UUID().description
|
let profiles_subid = UUID().description
|
||||||
|
var listener: Task<Void, any Error>? = nil
|
||||||
let limit: UInt32 = 500
|
let limit: UInt32 = 500
|
||||||
|
|
||||||
init(state: DamusState, search: NostrFilter) {
|
init(state: DamusState, search: NostrFilter) {
|
||||||
@@ -26,6 +26,7 @@ class SearchModel: ObservableObject {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func filter_muted() {
|
func filter_muted() {
|
||||||
self.events.filter {
|
self.events.filter {
|
||||||
should_show_event(state: state, ev: $0)
|
should_show_event(state: state, ev: $0)
|
||||||
@@ -39,19 +40,33 @@ class SearchModel: ObservableObject {
|
|||||||
search.kinds = [.text, .like, .longform, .highlight, .follow_list]
|
search.kinds = [.text, .like, .longform, .highlight, .follow_list]
|
||||||
|
|
||||||
//likes_filter.ids = ref_events.referenced_ids!
|
//likes_filter.ids = ref_events.referenced_ids!
|
||||||
|
listener?.cancel()
|
||||||
print("subscribing to search '\(search)' with sub_id \(sub_id)")
|
listener = Task {
|
||||||
state.nostrNetwork.pool.register_handler(sub_id: sub_id, handler: handle_event)
|
DispatchQueue.main.async {
|
||||||
loading = true
|
self.loading = true
|
||||||
state.nostrNetwork.pool.send(.subscribe(.init(filters: [search], sub_id: sub_id)))
|
}
|
||||||
|
print("subscribing to search")
|
||||||
|
try Task.checkCancellation()
|
||||||
|
let events = await state.nostrNetwork.reader.query(filters: [search])
|
||||||
|
for event in events {
|
||||||
|
if event.is_textlike && event.should_show_event {
|
||||||
|
await self.add_event(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try Task.checkCancellation()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func unsubscribe() {
|
func unsubscribe() {
|
||||||
state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
|
listener?.cancel()
|
||||||
loading = false
|
listener = nil
|
||||||
print("unsubscribing from search '\(search)' with sub_id \(sub_id)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func add_event(_ ev: NostrEvent) {
|
func add_event(_ ev: NostrEvent) {
|
||||||
if !event_matches_filter(ev, filter: search) {
|
if !event_matches_filter(ev, filter: search) {
|
||||||
return
|
return
|
||||||
@@ -65,25 +80,6 @@ class SearchModel: ObservableObject {
|
|||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
|
||||||
let (sub_id, done) = handle_subid_event(pool: state.nostrNetwork.pool, relay_id: relay_id, ev: ev) { sub_id, ev in
|
|
||||||
if ev.is_textlike && ev.should_show_event {
|
|
||||||
self.add_event(ev)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
guard done else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.loading = false
|
|
||||||
|
|
||||||
if sub_id == self.sub_id {
|
|
||||||
guard let txn = NdbTxn(ndb: state.ndb) else { return }
|
|
||||||
load_profiles(context: "search", profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(self.events.all_events), damus_state: state, txn: txn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func event_matches_hashtag(_ ev: NostrEvent, hashtags: [String]) -> Bool {
|
func event_matches_hashtag(_ ev: NostrEvent, hashtags: [String]) -> Bool {
|
||||||
@@ -106,33 +102,3 @@ func event_matches_filter(_ ev: NostrEvent, filter: NostrFilter) -> Bool {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_subid_event(pool: RelayPool, relay_id: RelayURL, ev: NostrConnectionEvent, handle: (String, NostrEvent) -> ()) -> (String?, Bool) {
|
|
||||||
switch ev {
|
|
||||||
case .ws_connection_event:
|
|
||||||
return (nil, false)
|
|
||||||
|
|
||||||
case .nostr_event(let res):
|
|
||||||
switch res {
|
|
||||||
case .event(let ev_subid, let ev):
|
|
||||||
handle(ev_subid, ev)
|
|
||||||
return (ev_subid, false)
|
|
||||||
|
|
||||||
case .ok:
|
|
||||||
return (nil, false)
|
|
||||||
|
|
||||||
case .notice(let note):
|
|
||||||
if note.contains("Too many subscription filters") {
|
|
||||||
// TODO: resend filters?
|
|
||||||
pool.reconnect(to: [relay_id])
|
|
||||||
}
|
|
||||||
return (nil, false)
|
|
||||||
|
|
||||||
case .eose(let subid):
|
|
||||||
return (subid, true)
|
|
||||||
|
|
||||||
case .auth:
|
|
||||||
return (nil, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ struct HashtagUnfollowButton: View {
|
|||||||
|
|
||||||
func unfollow(_ hashtag: String) {
|
func unfollow(_ hashtag: String) {
|
||||||
is_following = false
|
is_following = false
|
||||||
handle_unfollow(state: damus_state, unfollow: FollowRef.hashtag(hashtag))
|
Task { await handle_unfollow(state: damus_state, unfollow: FollowRef.hashtag(hashtag)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ struct HashtagFollowButton: View {
|
|||||||
|
|
||||||
func follow(_ hashtag: String) {
|
func follow(_ hashtag: String) {
|
||||||
is_following = true
|
is_following = true
|
||||||
handle_follow(state: damus_state, follow: .hashtag(hashtag))
|
Task { await handle_follow(state: damus_state, follow: .hashtag(hashtag)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ struct SearchHomeView: View {
|
|||||||
@StateObject var model: SearchHomeModel
|
@StateObject var model: SearchHomeModel
|
||||||
@State var search: String = ""
|
@State var search: String = ""
|
||||||
@FocusState private var isFocused: Bool
|
@FocusState private var isFocused: Bool
|
||||||
|
@State var loadingTask: Task<Void, Never>?
|
||||||
|
|
||||||
func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
|
func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
|
||||||
var filters = ContentFilters.defaults(damus_state: damus_state)
|
var filters = ContentFilters.defaults(damus_state: damus_state)
|
||||||
@@ -53,7 +54,7 @@ struct SearchHomeView: View {
|
|||||||
loading: $model.loading,
|
loading: $model.loading,
|
||||||
damus: damus_state,
|
damus: damus_state,
|
||||||
show_friend_icon: true,
|
show_friend_icon: true,
|
||||||
filter:content_filter(FilterState.posts),
|
filter: content_filter(FilterState.posts),
|
||||||
content: {
|
content: {
|
||||||
AnyView(VStack(alignment: .leading) {
|
AnyView(VStack(alignment: .leading) {
|
||||||
HStack {
|
HStack {
|
||||||
@@ -65,7 +66,7 @@ struct SearchHomeView: View {
|
|||||||
.padding(.top)
|
.padding(.top)
|
||||||
.padding(.horizontal)
|
.padding(.horizontal)
|
||||||
|
|
||||||
FollowPackTimelineView<AnyView>(events: model.events, loading: $model.loading, damus: damus_state, show_friend_icon: true,filter:content_filter(FilterState.follow_list)
|
FollowPackTimelineView<AnyView>(events: model.followPackEvents, loading: $model.loading, damus: damus_state, show_friend_icon: true, filter: content_filter(FilterState.follow_list)
|
||||||
).padding(.bottom)
|
).padding(.bottom)
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
@@ -82,20 +83,10 @@ struct SearchHomeView: View {
|
|||||||
}.padding(.bottom, 50))
|
}.padding(.bottom, 50))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.refreshable {
|
|
||||||
// Fetch new information by unsubscribing and resubscribing to the relay
|
|
||||||
model.unsubscribe()
|
|
||||||
model.subscribe()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var SearchContent: some View {
|
var SearchContent: some View {
|
||||||
SearchResultsView(damus_state: damus_state, search: $search)
|
SearchResultsView(damus_state: damus_state, search: $search)
|
||||||
.refreshable {
|
|
||||||
// Fetch new information by unsubscribing and resubscribing to the relay
|
|
||||||
model.unsubscribe()
|
|
||||||
model.subscribe()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var MainContent: some View {
|
var MainContent: some View {
|
||||||
@@ -129,11 +120,11 @@ struct SearchHomeView: View {
|
|||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if model.events.events.isEmpty {
|
if model.events.events.isEmpty {
|
||||||
model.subscribe()
|
loadingTask = Task { await model.load() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
model.unsubscribe()
|
loadingTask?.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ struct SearchView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
appstate.mutelist_manager.set_mutelist(mutelist)
|
appstate.mutelist_manager.set_mutelist(mutelist)
|
||||||
appstate.nostrNetwork.postbox.send(mutelist)
|
Task { await appstate.nostrNetwork.postbox.send(mutelist) }
|
||||||
} label: {
|
} label: {
|
||||||
Text("Unmute Hashtag", comment: "Label represnting a button that the user can tap to unmute a given hashtag so they start seeing it in their feed again.")
|
Text("Unmute Hashtag", comment: "Label represnting a button that the user can tap to unmute a given hashtag so they start seeing it in their feed again.")
|
||||||
}
|
}
|
||||||
@@ -104,7 +104,7 @@ struct SearchView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
appstate.mutelist_manager.set_mutelist(mutelist)
|
appstate.mutelist_manager.set_mutelist(mutelist)
|
||||||
appstate.nostrNetwork.postbox.send(mutelist)
|
Task { await appstate.nostrNetwork.postbox.send(mutelist) }
|
||||||
}
|
}
|
||||||
|
|
||||||
var described_search: DescribedSearch {
|
var described_search: DescribedSearch {
|
||||||
|
|||||||
@@ -77,7 +77,8 @@ struct SearchingEventView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case .event(let note_id):
|
case .event(let note_id):
|
||||||
find_event(state: state, query: .event(evid: note_id)) { res in
|
Task {
|
||||||
|
let res = await state.nostrNetwork.reader.findEvent(query: .event(evid: note_id))
|
||||||
guard case .event(let ev) = res else {
|
guard case .event(let ev) = res else {
|
||||||
self.search_state = .not_found
|
self.search_state = .not_found
|
||||||
return
|
return
|
||||||
@@ -85,7 +86,8 @@ struct SearchingEventView: View {
|
|||||||
self.search_state = .found(ev)
|
self.search_state = .found(ev)
|
||||||
}
|
}
|
||||||
case .profile(let pubkey):
|
case .profile(let pubkey):
|
||||||
find_event(state: state, query: .profile(pubkey: pubkey)) { res in
|
Task {
|
||||||
|
let res = await state.nostrNetwork.reader.findEvent(query: .profile(pubkey: pubkey))
|
||||||
guard case .profile(let pubkey) = res else {
|
guard case .profile(let pubkey) = res else {
|
||||||
self.search_state = .not_found
|
self.search_state = .not_found
|
||||||
return
|
return
|
||||||
@@ -93,7 +95,8 @@ struct SearchingEventView: View {
|
|||||||
self.search_state = .found_profile(pubkey)
|
self.search_state = .found_profile(pubkey)
|
||||||
}
|
}
|
||||||
case .naddr(let naddr):
|
case .naddr(let naddr):
|
||||||
naddrLookup(damus_state: state, naddr: naddr) { res in
|
Task {
|
||||||
|
let res = await state.nostrNetwork.reader.lookup(naddr: naddr)
|
||||||
guard let res = res else {
|
guard let res = res else {
|
||||||
self.search_state = .not_found
|
self.search_state = .not_found
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ func setting_get_property_value<T>(key: String, scoped_key: String, default_valu
|
|||||||
func setting_set_property_value<T: Equatable>(scoped_key: String, old_value: T, new_value: T) -> T? {
|
func setting_set_property_value<T: Equatable>(scoped_key: String, old_value: T, new_value: T) -> T? {
|
||||||
guard old_value != new_value else { return nil }
|
guard old_value != new_value else { return nil }
|
||||||
DamusUserDefaults.standard.set(new_value, forKey: scoped_key)
|
DamusUserDefaults.standard.set(new_value, forKey: scoped_key)
|
||||||
UserSettingsStore.shared?.objectWillChange.send()
|
DispatchQueue.main.async {
|
||||||
|
UserSettingsStore.shared?.objectWillChange.send()
|
||||||
|
}
|
||||||
return new_value
|
return new_value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,6 +245,14 @@ class UserSettingsStore: ObservableObject {
|
|||||||
@Setting(key: "enable_experimental_purple_api", default_value: false)
|
@Setting(key: "enable_experimental_purple_api", default_value: false)
|
||||||
var enable_experimental_purple_api: Bool
|
var enable_experimental_purple_api: Bool
|
||||||
|
|
||||||
|
/// Whether the app has the experimental local relay model flag that streams data only from the local relay (ndb)
|
||||||
|
@Setting(key: "enable_experimental_local_relay_model", default_value: false)
|
||||||
|
var enable_experimental_local_relay_model: Bool
|
||||||
|
|
||||||
|
/// Whether the app should present the experimental floating "Load new content" button
|
||||||
|
@Setting(key: "enable_experimental_load_new_content_button", default_value: false)
|
||||||
|
var enable_experimental_load_new_content_button: Bool
|
||||||
|
|
||||||
@StringSetting(key: "purple_environment", default_value: .production)
|
@StringSetting(key: "purple_environment", default_value: .production)
|
||||||
var purple_enviroment: DamusPurpleEnvironment
|
var purple_enviroment: DamusPurpleEnvironment
|
||||||
|
|
||||||
|
|||||||
@@ -182,8 +182,10 @@ struct ConfigView: View {
|
|||||||
let ev = created_deleted_account_profile(keypair: keypair) else {
|
let ev = created_deleted_account_profile(keypair: keypair) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
state.nostrNetwork.postbox.send(ev)
|
Task {
|
||||||
logout(state)
|
await state.nostrNetwork.postbox.send(ev)
|
||||||
|
logout(state)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.alert(NSLocalizedString("Logout", comment: "Alert for logging out the user."), isPresented: $confirm_logout) {
|
.alert(NSLocalizedString("Logout", comment: "Alert for logging out the user."), isPresented: $confirm_logout) {
|
||||||
|
|||||||
@@ -68,13 +68,13 @@ struct FirstAidSettingsView: View {
|
|||||||
guard let new_contact_list_event = make_first_contact_event(keypair: damus_state.keypair) else {
|
guard let new_contact_list_event = make_first_contact_event(keypair: damus_state.keypair) else {
|
||||||
throw FirstAidError.cannotMakeFirstContactEvent
|
throw FirstAidError.cannotMakeFirstContactEvent
|
||||||
}
|
}
|
||||||
damus_state.nostrNetwork.pool.send(.event(new_contact_list_event))
|
await damus_state.nostrNetwork.send(event: new_contact_list_event)
|
||||||
damus_state.settings.latest_contact_event_id_hex = new_contact_list_event.id.hex()
|
damus_state.settings.latest_contact_event_id_hex = new_contact_list_event.id.hex()
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetRelayList() async throws {
|
func resetRelayList() async throws {
|
||||||
let bestEffortRelayList = damus_state.nostrNetwork.userRelayList.getBestEffortRelayList()
|
let bestEffortRelayList = damus_state.nostrNetwork.userRelayList.getBestEffortRelayList()
|
||||||
try damus_state.nostrNetwork.userRelayList.set(userRelayList: bestEffortRelayList)
|
try await damus_state.nostrNetwork.userRelayList.set(userRelayList: bestEffortRelayList)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum FirstAidError: Error {
|
enum FirstAidError: Error {
|
||||||
|
|||||||
@@ -109,16 +109,18 @@ struct UserStatusSheet: View {
|
|||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
guard let status = self.status.general,
|
Task {
|
||||||
let kp = keypair.to_full(),
|
guard let status = self.status.general,
|
||||||
let ev = make_user_status_note(status: status, keypair: kp, expiry: duration.expiration)
|
let kp = keypair.to_full(),
|
||||||
else {
|
let ev = make_user_status_note(status: status, keypair: kp, expiry: duration.expiration)
|
||||||
return
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await postbox.send(ev)
|
||||||
|
|
||||||
|
dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
postbox.send(ev)
|
|
||||||
|
|
||||||
dismiss()
|
|
||||||
}, label: {
|
}, label: {
|
||||||
Text("Share", comment: "Save button text for saving profile status settings.")
|
Text("Share", comment: "Save button text for saving profile status settings.")
|
||||||
})
|
})
|
||||||
@@ -129,7 +131,7 @@ struct UserStatusSheet: View {
|
|||||||
Divider()
|
Divider()
|
||||||
|
|
||||||
ZStack(alignment: .top) {
|
ZStack(alignment: .top) {
|
||||||
ProfilePicView(pubkey: keypair.pubkey, size: 120.0, highlight: .custom(DamusColors.white, 3.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
ProfilePicView(pubkey: keypair.pubkey, size: 120.0, highlight: .custom(DamusColors.white, 3.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation, damusState: damus_state)
|
||||||
.padding(.top, 30)
|
.padding(.top, 30)
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ func nsfw_tag_filter(ev: NostrEvent) -> Bool {
|
|||||||
return ev.referenced_hashtags.first(where: { t in t.hashtag.caseInsensitiveCompare("nsfw") == .orderedSame }) == nil
|
return ev.referenced_hashtags.first(where: { t in t.hashtag.caseInsensitiveCompare("nsfw") == .orderedSame }) == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func get_repost_of_muted_user_filter(damus_state: DamusState) -> ((_ ev: NostrEvent) -> Bool) {
|
func get_repost_of_muted_user_filter(damus_state: DamusState) -> ((_ ev: NostrEvent) -> Bool) {
|
||||||
return { ev in
|
return { ev in
|
||||||
guard ev.known_kind == .boost else { return true }
|
guard ev.known_kind == .boost else { return true }
|
||||||
@@ -79,10 +80,12 @@ struct ContentFilters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension ContentFilters {
|
extension ContentFilters {
|
||||||
|
@MainActor
|
||||||
static func default_filters(damus_state: DamusState) -> ContentFilters {
|
static func default_filters(damus_state: DamusState) -> ContentFilters {
|
||||||
return ContentFilters(filters: ContentFilters.defaults(damus_state: damus_state))
|
return ContentFilters(filters: ContentFilters.defaults(damus_state: damus_state))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
static func defaults(damus_state: DamusState) -> [(NostrEvent) -> Bool] {
|
static func defaults(damus_state: DamusState) -> [(NostrEvent) -> Bool] {
|
||||||
var filters = Array<(NostrEvent) -> Bool>()
|
var filters = Array<(NostrEvent) -> Bool>()
|
||||||
if damus_state.settings.hide_nsfw_tagged_content {
|
if damus_state.settings.hide_nsfw_tagged_content {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ enum HomeResubFilter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HomeModel: ContactsDelegate {
|
class HomeModel: ContactsDelegate, ObservableObject {
|
||||||
// The maximum amount of contacts placed on a home feed subscription filter.
|
// The maximum amount of contacts placed on a home feed subscription filter.
|
||||||
// If the user has more contacts, chunking or other techniques will be used to avoid sending huge filters
|
// If the user has more contacts, chunking or other techniques will be used to avoid sending huge filters
|
||||||
let MAX_CONTACTS_ON_FILTER = 500
|
let MAX_CONTACTS_ON_FILTER = 500
|
||||||
@@ -65,14 +65,12 @@ class HomeModel: ContactsDelegate {
|
|||||||
let resub_debouncer = Debouncer(interval: 3.0)
|
let resub_debouncer = Debouncer(interval: 3.0)
|
||||||
var should_debounce_dms = true
|
var should_debounce_dms = true
|
||||||
|
|
||||||
let home_subid = UUID().description
|
var homeHandlerTask: Task<Void, Never>?
|
||||||
let contacts_subid = UUID().description
|
var notificationsHandlerTask: Task<Void, Never>?
|
||||||
let notifications_subid = UUID().description
|
var generalHandlerTask: Task<Void, Never>?
|
||||||
let dms_subid = UUID().description
|
var nwcHandlerTask: Task<Void, Never>?
|
||||||
let init_subid = UUID().description
|
|
||||||
let profiles_subid = UUID().description
|
|
||||||
|
|
||||||
var loading: Bool = false
|
@Published var loading: Bool = true
|
||||||
|
|
||||||
var signal = SignalModel()
|
var signal = SignalModel()
|
||||||
|
|
||||||
@@ -85,7 +83,9 @@ class HomeModel: ContactsDelegate {
|
|||||||
init() {
|
init() {
|
||||||
self.damus_state = DamusState.empty
|
self.damus_state = DamusState.empty
|
||||||
self.setup_debouncer()
|
self.setup_debouncer()
|
||||||
filter_events()
|
DispatchQueue.main.async {
|
||||||
|
self.filter_events()
|
||||||
|
}
|
||||||
events.on_queue = preloader
|
events.on_queue = preloader
|
||||||
//self.events = EventHolder(on_queue: preloader)
|
//self.events = EventHolder(on_queue: preloader)
|
||||||
}
|
}
|
||||||
@@ -94,23 +94,10 @@ class HomeModel: ContactsDelegate {
|
|||||||
preload_events(state: self.damus_state, events: [ev])
|
preload_events(state: self.damus_state, events: [ev])
|
||||||
}
|
}
|
||||||
|
|
||||||
var pool: RelayPool {
|
|
||||||
self.damus_state.nostrNetwork.pool
|
|
||||||
}
|
|
||||||
|
|
||||||
var dms: DirectMessagesModel {
|
var dms: DirectMessagesModel {
|
||||||
return damus_state.dms
|
return damus_state.dms
|
||||||
}
|
}
|
||||||
|
|
||||||
func has_sub_id_event(sub_id: String, ev_id: NoteId) -> Bool {
|
|
||||||
if !has_event.keys.contains(sub_id) {
|
|
||||||
has_event[sub_id] = Set()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return has_event[sub_id]!.contains(ev_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setup_debouncer() {
|
func setup_debouncer() {
|
||||||
// turn off debouncer after initial load
|
// turn off debouncer after initial load
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
|
||||||
@@ -140,6 +127,28 @@ class HomeModel: ContactsDelegate {
|
|||||||
damus_state.drafts.load(from: damus_state)
|
damus_state.drafts.load(from: damus_state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum RelayListLoadingError: Error {
|
||||||
|
case noRelayList
|
||||||
|
case relayListParseError
|
||||||
|
|
||||||
|
var humanReadableError: ErrorView.UserPresentableError {
|
||||||
|
switch self {
|
||||||
|
case .noRelayList:
|
||||||
|
return ErrorView.UserPresentableError(
|
||||||
|
user_visible_description: NSLocalizedString("Your relay list could not be found, so we cannot connect you to your Nostr network.", comment: "Human readable error description for a failure to find the relay list"),
|
||||||
|
tip: NSLocalizedString("Please check your internet connection and restart the app. If the error persists, please go to Settings > First Aid.", comment: "Human readable tips for what to do for a failure to find the relay list"),
|
||||||
|
technical_info: "No NIP-65 relay list or legacy kind:3 contact event could be found."
|
||||||
|
)
|
||||||
|
case .relayListParseError:
|
||||||
|
return ErrorView.UserPresentableError(
|
||||||
|
user_visible_description: NSLocalizedString("Your relay list appears to be broken, so we cannot connect you to your Nostr network.", comment: "Human readable error description for a failure to parse the relay list due to a bad relay list"),
|
||||||
|
tip: NSLocalizedString("Please contact support for further help.", comment: "Human readable tips for what to do for a failure to find the relay list"),
|
||||||
|
technical_info: "Relay list could not be parsed."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - ContactsDelegate functions
|
// MARK: - ContactsDelegate functions
|
||||||
|
|
||||||
func latest_contact_event_changed(new_event: NostrEvent) {
|
func latest_contact_event_changed(new_event: NostrEvent) {
|
||||||
@@ -158,9 +167,6 @@ class HomeModel: ContactsDelegate {
|
|||||||
print("hit resub debouncer")
|
print("hit resub debouncer")
|
||||||
|
|
||||||
resub_debouncer.debounce {
|
resub_debouncer.debounce {
|
||||||
print("resub")
|
|
||||||
self.unsubscribe_to_home_filters()
|
|
||||||
|
|
||||||
switch resubbing {
|
switch resubbing {
|
||||||
case .following:
|
case .following:
|
||||||
break
|
break
|
||||||
@@ -174,35 +180,17 @@ class HomeModel: ContactsDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Force refresh of home timeline filters, bypassing startup debounce
|
|
||||||
/// Used when favorites are fetched from network during startup to ensure unfollowed favorited users are included
|
|
||||||
/// This is needed because the normal resubscribe path is blocked during initial load.
|
|
||||||
/// TODO: Will this be a performance problem?
|
|
||||||
func refresh_home_filters() {
|
|
||||||
unsubscribe_to_home_filters()
|
|
||||||
subscribe_to_home_filters()
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func process_event(sub_id: String, relay_id: RelayURL, ev: NostrEvent) {
|
func process_event(ev: NostrEvent, context: SubscriptionContext) {
|
||||||
if has_sub_id_event(sub_id: sub_id, ev_id: ev.id) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let last_k = get_last_event_of_kind(relay_id: relay_id, kind: ev.kind)
|
|
||||||
if last_k == nil || ev.created_at > last_k!.created_at {
|
|
||||||
last_event_of_kind[relay_id]?[ev.kind] = ev
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let kind = ev.known_kind else {
|
guard let kind = ev.known_kind else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch kind {
|
switch kind {
|
||||||
case .chat, .longform, .text, .highlight:
|
case .chat, .longform, .text, .highlight:
|
||||||
handle_text_event(sub_id: sub_id, ev)
|
handle_text_event(ev, context: context)
|
||||||
case .contacts:
|
case .contacts:
|
||||||
handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
|
handle_contact_event(ev: ev)
|
||||||
case .metadata:
|
case .metadata:
|
||||||
// profile metadata processing is handled by nostrdb
|
// profile metadata processing is handled by nostrdb
|
||||||
break
|
break
|
||||||
@@ -213,7 +201,7 @@ class HomeModel: ContactsDelegate {
|
|||||||
case .contact_card:
|
case .contact_card:
|
||||||
damus_state.contactCards.loadEvent(ev, pubkey: damus_state.pubkey)
|
damus_state.contactCards.loadEvent(ev, pubkey: damus_state.pubkey)
|
||||||
case .boost:
|
case .boost:
|
||||||
handle_boost_event(sub_id: sub_id, ev)
|
handle_boost_event(ev, context: context)
|
||||||
case .like:
|
case .like:
|
||||||
handle_like_event(ev)
|
handle_like_event(ev)
|
||||||
case .dm:
|
case .dm:
|
||||||
@@ -227,7 +215,7 @@ class HomeModel: ContactsDelegate {
|
|||||||
case .nwc_request:
|
case .nwc_request:
|
||||||
break
|
break
|
||||||
case .nwc_response:
|
case .nwc_response:
|
||||||
handle_nwc_response(ev, relay: relay_id)
|
handle_nwc_response(ev)
|
||||||
case .http_auth:
|
case .http_auth:
|
||||||
break
|
break
|
||||||
case .status:
|
case .status:
|
||||||
@@ -272,7 +260,7 @@ class HomeModel: ContactsDelegate {
|
|||||||
pdata.status.update_status(st)
|
pdata.status.update_status(st)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_nwc_response(_ ev: NostrEvent, relay: RelayURL) {
|
func handle_nwc_response(_ ev: NostrEvent) {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
// TODO: Adapt KeychainStorage to StringCodable and instead of parsing to WalletConnectURL every time
|
// TODO: Adapt KeychainStorage to StringCodable and instead of parsing to WalletConnectURL every time
|
||||||
guard let nwc_str = damus_state.settings.nostr_wallet_connect,
|
guard let nwc_str = damus_state.settings.nostr_wallet_connect,
|
||||||
@@ -280,7 +268,6 @@ class HomeModel: ContactsDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard nwc.relay == relay else { return } // Don't process NWC responses coming from relays other than our designated one
|
|
||||||
guard ev.referenced_pubkeys.first == nwc.keypair.pubkey else {
|
guard ev.referenced_pubkeys.first == nwc.keypair.pubkey else {
|
||||||
return // This message is not for us. Ignore it.
|
return // This message is not for us. Ignore it.
|
||||||
}
|
}
|
||||||
@@ -300,9 +287,9 @@ class HomeModel: ContactsDelegate {
|
|||||||
// since command results are not returned for ephemeral events,
|
// since command results are not returned for ephemeral events,
|
||||||
// remove the request from the postbox which is likely failing over and over
|
// remove the request from the postbox which is likely failing over and over
|
||||||
if damus_state.nostrNetwork.postbox.remove_relayer(relay_id: nwc.relay, event_id: resp.req_id) {
|
if damus_state.nostrNetwork.postbox.remove_relayer(relay_id: nwc.relay, event_id: resp.req_id) {
|
||||||
Log.debug("HomeModel: got NWC response, removed %s from the postbox [%s]", for: .nwc, resp.req_id.hex(), relay.absoluteString)
|
Log.debug("HomeModel: got NWC response, removed %s from the postbox", for: .nwc, resp.req_id.hex())
|
||||||
} else {
|
} else {
|
||||||
Log.debug("HomeModel: got NWC response, %s not found in the postbox, nothing to remove [%s]", for: .nwc, resp.req_id.hex(), relay.absoluteString)
|
Log.debug("HomeModel: got NWC response, %s not found in the postbox, nothing to remove", for: .nwc, resp.req_id.hex())
|
||||||
}
|
}
|
||||||
|
|
||||||
damus_state.wallet.handle_nwc_response(response: resp) // This can handle success or error cases
|
damus_state.wallet.handle_nwc_response(response: resp) // This can handle success or error cases
|
||||||
@@ -314,7 +301,6 @@ class HomeModel: ContactsDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
print("nwc success: \(resp.response.result.debugDescription) [\(relay)]")
|
|
||||||
WalletConnect.handle_zap_success(state: self.damus_state, resp: resp)
|
WalletConnect.handle_zap_success(state: self.damus_state, resp: resp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -370,6 +356,7 @@ class HomeModel: ContactsDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func filter_events() {
|
func filter_events() {
|
||||||
events.filter { ev in
|
events.filter { ev in
|
||||||
!damus_state.mutelist_manager.is_muted(.user(ev.pubkey, nil))
|
!damus_state.mutelist_manager.is_muted(.user(ev.pubkey, nil))
|
||||||
@@ -393,19 +380,11 @@ class HomeModel: ContactsDelegate {
|
|||||||
self.deleted_events.insert(ev.id)
|
self.deleted_events.insert(ev.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_contact_event(sub_id: String, relay_id: RelayURL, ev: NostrEvent) {
|
func handle_contact_event(ev: NostrEvent) {
|
||||||
process_contact_event(state: self.damus_state, ev: ev)
|
process_contact_event(state: self.damus_state, ev: ev)
|
||||||
|
|
||||||
if sub_id == init_subid {
|
|
||||||
pool.send(.unsubscribe(init_subid), to: [relay_id])
|
|
||||||
if !done_init {
|
|
||||||
done_init = true
|
|
||||||
send_home_filters(relay_id: nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_boost_event(sub_id: String, _ ev: NostrEvent) {
|
func handle_boost_event(_ ev: NostrEvent, context: SubscriptionContext) {
|
||||||
var boost_ev_id = ev.last_refid()
|
var boost_ev_id = ev.last_refid()
|
||||||
|
|
||||||
if let inner_ev = ev.get_inner_event(cache: damus_state.events) {
|
if let inner_ev = ev.get_inner_event(cache: damus_state.events) {
|
||||||
@@ -420,7 +399,7 @@ class HomeModel: ContactsDelegate {
|
|||||||
|
|
||||||
if inner_ev.is_textlike {
|
if inner_ev.is_textlike {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.handle_text_event(sub_id: sub_id, ev)
|
self.handle_text_event(ev, context: context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -447,6 +426,7 @@ class HomeModel: ContactsDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func handle_like_event(_ ev: NostrEvent) {
|
func handle_like_event(_ ev: NostrEvent) {
|
||||||
guard let e = ev.last_refid() else {
|
guard let e = ev.last_refid() else {
|
||||||
// no id ref? invalid like event
|
// no id ref? invalid like event
|
||||||
@@ -468,94 +448,27 @@ class HomeModel: ContactsDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
/// Send the initial filters, just our contact list and relay list mostly
|
||||||
func handle_event(relay_id: RelayURL, conn_event: NostrConnectionEvent) {
|
func send_initial_filters() {
|
||||||
switch conn_event {
|
Task {
|
||||||
case .ws_connection_event(let ev):
|
let startTime = CFAbsoluteTimeGetCurrent()
|
||||||
switch ev {
|
let id = UUID()
|
||||||
case .connected:
|
Log.info("Initial filter task started with ID %s", for: .homeModel, id.uuidString)
|
||||||
|
let filter = NostrFilter(kinds: [.contacts], limit: 1, authors: [damus_state.pubkey])
|
||||||
|
for await event in damus_state.nostrNetwork.reader.streamExistingEvents(filters: [filter]) {
|
||||||
|
await event.justUseACopy({ await process_event(ev: $0, context: .other) })
|
||||||
if !done_init {
|
if !done_init {
|
||||||
self.loading = true
|
done_init = true
|
||||||
send_initial_filters(relay_id: relay_id)
|
Log.info("Initial filter task %s: Done initialization; Elapsed time: %.2f seconds", for: .homeModel, id.uuidString, CFAbsoluteTimeGetCurrent() - startTime)
|
||||||
} else {
|
send_home_filters()
|
||||||
//remove_bootstrap_nodes(damus_state)
|
|
||||||
send_home_filters(relay_id: relay_id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// connect to nwc relays when connected
|
|
||||||
if let nwc_str = damus_state.settings.nostr_wallet_connect,
|
|
||||||
let r = pool.get_relay(relay_id),
|
|
||||||
r.descriptor.variant == .nwc,
|
|
||||||
let nwc = WalletConnectURL(str: nwc_str),
|
|
||||||
nwc.relay == relay_id
|
|
||||||
{
|
|
||||||
WalletConnect.subscribe(url: nwc, pool: pool)
|
|
||||||
}
|
|
||||||
case .error(let merr):
|
|
||||||
let desc = String(describing: merr)
|
|
||||||
if desc.contains("Software caused connection abort") {
|
|
||||||
pool.reconnect(to: [relay_id])
|
|
||||||
}
|
|
||||||
case .disconnected:
|
|
||||||
pool.reconnect(to: [relay_id])
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
update_signal_from_pool(signal: self.signal, pool: damus_state.nostrNetwork.pool)
|
|
||||||
case .nostr_event(let ev):
|
|
||||||
switch ev {
|
|
||||||
case .event(let sub_id, let ev):
|
|
||||||
// globally handle likes
|
|
||||||
/*
|
|
||||||
let always_process = sub_id == notifications_subid || sub_id == contacts_subid || sub_id == home_subid || sub_id == dms_subid || sub_id == init_subid || ev.known_kind == .like || ev.known_kind == .boost || ev.known_kind == .zap || ev.known_kind == .contacts || ev.known_kind == .metadata
|
|
||||||
if !always_process {
|
|
||||||
// TODO: other views like threads might have their own sub ids, so ignore those events... or should we?
|
|
||||||
return
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
self.process_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
|
|
||||||
case .notice(let msg):
|
|
||||||
print(msg)
|
|
||||||
|
|
||||||
case .eose(let sub_id):
|
|
||||||
guard let txn = NdbTxn(ndb: damus_state.ndb) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if sub_id == dms_subid {
|
|
||||||
var dms = dms.dms.flatMap { $0.events }
|
|
||||||
dms.append(contentsOf: incoming_dms)
|
|
||||||
load_profiles(context: "dms", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(dms), damus_state: damus_state, txn: txn)
|
|
||||||
} else if sub_id == notifications_subid {
|
|
||||||
load_profiles(context: "notifications", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_keys(notifications.uniq_pubkeys()), damus_state: damus_state, txn: txn)
|
|
||||||
} else if sub_id == home_subid {
|
|
||||||
load_profiles(context: "home", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus_state, txn: txn)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.loading = false
|
|
||||||
break
|
|
||||||
|
|
||||||
case .ok:
|
|
||||||
break
|
|
||||||
case .auth:
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Send the initial filters, just our contact list mostly
|
|
||||||
func send_initial_filters(relay_id: RelayURL) {
|
|
||||||
let filter = NostrFilter(kinds: [.contacts], limit: 1, authors: [damus_state.pubkey])
|
|
||||||
let subscription = NostrSubscribe(filters: [filter], sub_id: init_subid)
|
|
||||||
pool.send(.subscribe(subscription), to: [relay_id])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// After initial connection or reconnect, send subscription filters for the home timeline, DMs, and notifications
|
/// After initial connection or reconnect, send subscription filters for the home timeline, DMs, and notifications
|
||||||
func send_home_filters(relay_id: RelayURL?) {
|
func send_home_filters() {
|
||||||
// TODO: since times should be based on events from a specific relay
|
// TODO: since times should be based on events from a specific relay
|
||||||
// perhaps we could mark this in the relay pool somehow
|
// perhaps we could mark this in the relay pool somehow
|
||||||
|
|
||||||
@@ -603,38 +516,60 @@ class HomeModel: ContactsDelegate {
|
|||||||
let contacts_filter_chunks = contacts_filter.chunked(on: .authors, into: MAX_CONTACTS_ON_FILTER)
|
let contacts_filter_chunks = contacts_filter.chunked(on: .authors, into: MAX_CONTACTS_ON_FILTER)
|
||||||
var contacts_filters = contacts_filter_chunks + [our_contacts_filter, our_blocklist_filter, our_old_blocklist_filter, contact_cards_filter]
|
var contacts_filters = contacts_filter_chunks + [our_contacts_filter, our_blocklist_filter, our_old_blocklist_filter, contact_cards_filter]
|
||||||
var dms_filters = [dms_filter, our_dms_filter]
|
var dms_filters = [dms_filter, our_dms_filter]
|
||||||
let last_of_kind = get_last_of_kind(relay_id: relay_id)
|
|
||||||
|
|
||||||
contacts_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: contacts_filters)
|
|
||||||
notifications_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: notifications_filters)
|
|
||||||
dms_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: dms_filters)
|
|
||||||
|
|
||||||
//print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters])
|
//print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters])
|
||||||
|
|
||||||
subscribe_to_home_filters(relay_id: relay_id)
|
subscribe_to_home_filters()
|
||||||
|
|
||||||
let relay_ids = relay_id.map { [$0] }
|
self.notificationsHandlerTask?.cancel()
|
||||||
|
self.notificationsHandlerTask = Task {
|
||||||
pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid)), to: relay_ids)
|
for await event in damus_state.nostrNetwork.reader.streamIndefinitely(filters: notifications_filters) {
|
||||||
pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid)), to: relay_ids)
|
await event.justUseACopy({ await process_event(ev: $0, context: .notifications) })
|
||||||
pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)), to: relay_ids)
|
}
|
||||||
|
}
|
||||||
|
self.generalHandlerTask?.cancel()
|
||||||
|
self.generalHandlerTask = Task {
|
||||||
|
for await item in damus_state.nostrNetwork.reader.advancedStream(filters: dms_filters + contacts_filters, streamMode: .ndbAndNetworkParallel(optimizeNetworkFilter: true)) {
|
||||||
|
switch item {
|
||||||
|
case .event(let lender):
|
||||||
|
await lender.justUseACopy({ await process_event(ev: $0, context: .other) })
|
||||||
|
case .eose:
|
||||||
|
var dms = dms.dms.flatMap { $0.events }
|
||||||
|
dms.append(contentsOf: incoming_dms)
|
||||||
|
case .ndbEose:
|
||||||
|
var dms = dms.dms.flatMap { $0.events }
|
||||||
|
dms.append(contentsOf: incoming_dms)
|
||||||
|
case .networkEose: break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.nwcHandlerTask?.cancel()
|
||||||
|
self.nwcHandlerTask = Task {
|
||||||
|
if let nwc_str = damus_state.settings.nostr_wallet_connect,
|
||||||
|
let nwc = WalletConnectURL(str: nwc_str)
|
||||||
|
{
|
||||||
|
var filter = NostrFilter(kinds: [.nwc_response])
|
||||||
|
filter.authors = [nwc.pubkey]
|
||||||
|
filter.limit = 0
|
||||||
|
for await event in damus_state.nostrNetwork.reader.streamIndefinitely(filters: [filter], to: [nwc.relay]) {
|
||||||
|
await event.justUseACopy({ await process_event(ev: $0, context: .other) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func get_last_of_kind(relay_id: RelayURL?) -> [UInt32: NostrEvent] {
|
func get_last_of_kind(relay_id: RelayURL?) -> [UInt32: NostrEvent] {
|
||||||
return relay_id.flatMap { last_event_of_kind[$0] } ?? [:]
|
return relay_id.flatMap { last_event_of_kind[$0] } ?? [:]
|
||||||
}
|
}
|
||||||
|
|
||||||
func unsubscribe_to_home_filters() {
|
|
||||||
pool.send(.unsubscribe(home_subid))
|
|
||||||
}
|
|
||||||
|
|
||||||
func get_friends() -> [Pubkey] {
|
func get_friends() -> [Pubkey] {
|
||||||
var friends = damus_state.contacts.get_friend_list()
|
var friends = damus_state.contacts.get_friend_list()
|
||||||
friends.insert(damus_state.pubkey)
|
friends.insert(damus_state.pubkey)
|
||||||
return Array(friends)
|
return Array(friends)
|
||||||
}
|
}
|
||||||
|
|
||||||
func subscribe_to_home_filters(friends fs: [Pubkey]? = nil, relay_id: RelayURL? = nil) {
|
func subscribe_to_home_filters(friends fs: [Pubkey]? = nil) {
|
||||||
// TODO: separate likes?
|
// TODO: separate likes?
|
||||||
var home_filter_kinds: [NostrKind] = [
|
var home_filter_kinds: [NostrKind] = [
|
||||||
.text, .longform, .boost, .highlight
|
.text, .longform, .boost, .highlight
|
||||||
@@ -673,13 +608,53 @@ class HomeModel: ContactsDelegate {
|
|||||||
home_filters.append(favorites_filter)
|
home_filters.append(favorites_filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
let relay_ids = relay_id.map { [$0] }
|
self.homeHandlerTask?.cancel()
|
||||||
home_filters = update_filters_with_since(last_of_kind: get_last_of_kind(relay_id: relay_id), filters: home_filters)
|
self.homeHandlerTask = Task {
|
||||||
let sub = NostrSubscribe(filters: home_filters, sub_id: home_subid)
|
let startTime = CFAbsoluteTimeGetCurrent()
|
||||||
|
let id = UUID()
|
||||||
pool.send(.subscribe(sub), to: relay_ids)
|
Log.info("Home handler task: Starting home handler task with ID %s", for: .homeModel, id.uuidString)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.loading = true
|
||||||
|
}
|
||||||
|
for await item in damus_state.nostrNetwork.reader.advancedStream(filters: home_filters, streamMode: .ndbAndNetworkParallel(optimizeNetworkFilter: true), id: id) {
|
||||||
|
switch item {
|
||||||
|
case .event(let lender):
|
||||||
|
let currentTime = CFAbsoluteTimeGetCurrent()
|
||||||
|
// Process events in parallel on a separate task, to avoid holding up upcoming signals
|
||||||
|
// Empirical evidence has shown that in at least one instance this technique saved up to 5 seconds of load time!
|
||||||
|
Task { await lender.justUseACopy({ await process_event(ev: $0, context: .home) }) }
|
||||||
|
case .eose:
|
||||||
|
let eoseTime = CFAbsoluteTimeGetCurrent()
|
||||||
|
Log.info("Home handler task %s: Received general EOSE after %.2f seconds", for: .homeModel, id.uuidString, eoseTime - startTime)
|
||||||
|
|
||||||
|
let finishTime = CFAbsoluteTimeGetCurrent()
|
||||||
|
Log.info("Home handler task %s: Completed initial loading task after %.2f seconds", for: .homeModel, id.uuidString, eoseTime - startTime)
|
||||||
|
case .ndbEose:
|
||||||
|
let eoseTime = CFAbsoluteTimeGetCurrent()
|
||||||
|
Log.info("Home handler task %s: Received NDB EOSE after %.2f seconds", for: .homeModel, id.uuidString, eoseTime - startTime)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.loading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let finishTime = CFAbsoluteTimeGetCurrent()
|
||||||
|
Log.info("Home handler task %s: Completed initial NDB loading task after %.2f seconds", for: .homeModel, id.uuidString, eoseTime - startTime)
|
||||||
|
case .networkEose:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adapter pattern to make migration easier
|
||||||
|
enum SubscriptionContext {
|
||||||
|
case home
|
||||||
|
case notifications
|
||||||
|
case other
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func handle_mute_list_event(_ ev: NostrEvent) {
|
func handle_mute_list_event(_ ev: NostrEvent) {
|
||||||
// we only care about our mutelist
|
// we only care about our mutelist
|
||||||
guard ev.pubkey == damus_state.pubkey else {
|
guard ev.pubkey == damus_state.pubkey else {
|
||||||
@@ -698,6 +673,7 @@ class HomeModel: ContactsDelegate {
|
|||||||
migrate_old_muted_threads_to_new_mutelist(keypair: damus_state.keypair, damus_state: damus_state)
|
migrate_old_muted_threads_to_new_mutelist(keypair: damus_state.keypair, damus_state: damus_state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func handle_old_list_event(_ ev: NostrEvent) {
|
func handle_old_list_event(_ ev: NostrEvent) {
|
||||||
// we only care about our lists
|
// we only care about our lists
|
||||||
guard ev.pubkey == damus_state.pubkey else {
|
guard ev.pubkey == damus_state.pubkey else {
|
||||||
@@ -729,6 +705,7 @@ class HomeModel: ContactsDelegate {
|
|||||||
return m[kind]
|
return m[kind]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func handle_notification(ev: NostrEvent) {
|
func handle_notification(ev: NostrEvent) {
|
||||||
// don't show notifications from ourselves
|
// don't show notifications from ourselves
|
||||||
guard ev.pubkey != damus_state.pubkey,
|
guard ev.pubkey != damus_state.pubkey,
|
||||||
@@ -748,7 +725,7 @@ class HomeModel: ContactsDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if handle_last_event(ev: ev, timeline: .notifications) {
|
if handle_last_event(ev: ev, timeline: .notifications) {
|
||||||
process_local_notification(state: damus_state, event: ev)
|
Task { await process_local_notification(state: damus_state, event: ev) }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -763,6 +740,7 @@ class HomeModel: ContactsDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func insert_home_event(_ ev: NostrEvent) {
|
func insert_home_event(_ ev: NostrEvent) {
|
||||||
if events.insert(ev) {
|
if events.insert(ev) {
|
||||||
handle_last_event(ev: ev, timeline: .home)
|
handle_last_event(ev: ev, timeline: .home)
|
||||||
@@ -770,7 +748,8 @@ class HomeModel: ContactsDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
|
@MainActor
|
||||||
|
func handle_text_event(_ ev: NostrEvent, context: SubscriptionContext) {
|
||||||
guard should_show_event(state: damus_state, ev: ev) else {
|
guard should_show_event(state: damus_state, ev: ev) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -794,25 +773,32 @@ class HomeModel: ContactsDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if sub_id == home_subid {
|
switch context {
|
||||||
insert_home_event(ev)
|
case .home:
|
||||||
} else if sub_id == notifications_subid {
|
Task { await insert_home_event(ev) }
|
||||||
|
case .notifications:
|
||||||
handle_notification(ev: ev)
|
handle_notification(ev: ev)
|
||||||
|
case .other:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func got_new_dm(notifs: NewEventsBits, ev: NostrEvent) {
|
func got_new_dm(notifs: NewEventsBits, ev: NostrEvent) {
|
||||||
notification_status.new_events = notifs
|
Task {
|
||||||
|
notification_status.new_events = notifs
|
||||||
guard should_display_notification(state: damus_state, event: ev, mode: .local),
|
|
||||||
let notification_object = generate_local_notification_object(ndb: self.damus_state.ndb, from: ev, state: damus_state)
|
|
||||||
else {
|
guard await should_display_notification(state: damus_state, event: ev, mode: .local),
|
||||||
return
|
let notification_object = generate_local_notification_object(ndb: self.damus_state.ndb, from: ev, state: damus_state)
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
create_local_notification(profiles: damus_state.profiles, notify: notification_object)
|
||||||
}
|
}
|
||||||
|
|
||||||
create_local_notification(profiles: damus_state.profiles, notify: notification_object)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func handle_dm(_ ev: NostrEvent) {
|
func handle_dm(_ ev: NostrEvent) {
|
||||||
guard should_show_event(state: damus_state, ev: ev) else {
|
guard should_show_event(state: damus_state, ev: ev) else {
|
||||||
return
|
return
|
||||||
@@ -841,13 +827,15 @@ class HomeModel: ContactsDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func update_signal_from_pool(signal: SignalModel, pool: RelayPool) {
|
func update_signal_from_pool(signal: SignalModel, pool: RelayPool) async {
|
||||||
if signal.max_signal != pool.relays.count {
|
let relayCount = await pool.relays.count
|
||||||
signal.max_signal = pool.relays.count
|
if signal.max_signal != relayCount {
|
||||||
|
signal.max_signal = relayCount
|
||||||
}
|
}
|
||||||
|
|
||||||
if signal.signal != pool.num_connected {
|
let numberOfConnectedRelays = await pool.num_connected
|
||||||
signal.signal = pool.num_connected
|
if signal.signal != numberOfConnectedRelays {
|
||||||
|
signal.signal = numberOfConnectedRelays
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1144,6 +1132,7 @@ func event_has_our_pubkey(_ ev: NostrEvent, our_pubkey: Pubkey) -> Bool {
|
|||||||
return ev.referenced_pubkeys.contains(our_pubkey)
|
return ev.referenced_pubkeys.contains(our_pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func should_show_event(event: NostrEvent, damus_state: DamusState) -> Bool {
|
func should_show_event(event: NostrEvent, damus_state: DamusState) -> Bool {
|
||||||
return should_show_event(
|
return should_show_event(
|
||||||
state: damus_state,
|
state: damus_state,
|
||||||
@@ -1151,6 +1140,7 @@ func should_show_event(event: NostrEvent, damus_state: DamusState) -> Bool {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func should_show_event(state: DamusState, ev: NostrEvent) -> Bool {
|
func should_show_event(state: DamusState, ev: NostrEvent) -> Bool {
|
||||||
let event_muted = state.mutelist_manager.is_event_muted(ev)
|
let event_muted = state.mutelist_manager.is_event_muted(ev)
|
||||||
if event_muted {
|
if event_muted {
|
||||||
@@ -1215,3 +1205,24 @@ func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Extension to bridge NIP-65 relay list structs with app-native objects
|
||||||
|
// TODO: Do we need this??
|
||||||
|
|
||||||
|
//extension NIP65.RelayList {
|
||||||
|
// static func fromLegacyContactList(_ contactList: NdbNote) throws(BridgeError) -> Self {
|
||||||
|
// guard let relayListInfo = decode_json_relays(contactList.content) else { throw .couldNotDecodeRelayListInfo }
|
||||||
|
// let relayItems = relayListInfo.map({ url, rwConfiguration in
|
||||||
|
// return RelayItem(url: url, rwConfiguration: rwConfiguration.toNIP65RWConfiguration() ?? .readWrite)
|
||||||
|
// })
|
||||||
|
// return NIP65.RelayList(relays: relayItems)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// static func fromLegacyContactList(_ contactList: NdbNote?) throws(BridgeError) -> Self? {
|
||||||
|
// guard let contactList = contactList else { return nil }
|
||||||
|
// return try fromLegacyContactList(contactList)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// enum BridgeError: Error {
|
||||||
|
// case couldNotDecodeRelayListInfo
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|||||||
@@ -9,14 +9,18 @@ import SwiftUI
|
|||||||
|
|
||||||
|
|
||||||
struct InnerTimelineView: View {
|
struct InnerTimelineView: View {
|
||||||
@ObservedObject var events: EventHolder
|
var events: EventHolder
|
||||||
|
@ObservedObject var filteredEvents: EventHolder.FilteredHolder
|
||||||
|
var filteredEventHolderId: UUID
|
||||||
let state: DamusState
|
let state: DamusState
|
||||||
let filter: (NostrEvent) -> Bool
|
|
||||||
|
|
||||||
init(events: EventHolder, damus: DamusState, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true) {
|
init(events: EventHolder, damus: DamusState, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true) {
|
||||||
self.events = events
|
self.events = events
|
||||||
self.state = damus
|
self.state = damus
|
||||||
self.filter = apply_mute_rules ? { filter($0) && !damus.mutelist_manager.is_event_muted($0) } : filter
|
let filter = apply_mute_rules ? { filter($0) && !damus.mutelist_manager.is_event_muted($0) } : filter
|
||||||
|
let filteredEvents = EventHolder.FilteredHolder(filter: filter)
|
||||||
|
self.filteredEvents = filteredEvents
|
||||||
|
self.filteredEventHolderId = events.add(filteredHolder: filteredEvents)
|
||||||
}
|
}
|
||||||
|
|
||||||
var event_options: EventViewOptions {
|
var event_options: EventViewOptions {
|
||||||
@@ -29,12 +33,11 @@ struct InnerTimelineView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
LazyVStack(spacing: 0) {
|
LazyVStack(spacing: 0) {
|
||||||
let events = self.events.events
|
let events = self.filteredEvents.events
|
||||||
let evs = events.filter(filter)
|
if events.isEmpty {
|
||||||
if evs.isEmpty {
|
|
||||||
EmptyTimelineView()
|
EmptyTimelineView()
|
||||||
} else {
|
} else {
|
||||||
let indexed = Array(zip(evs, 0...))
|
let indexed = Array(zip(events, 0...))
|
||||||
ForEach(indexed, id: \.0.id) { tup in
|
ForEach(indexed, id: \.0.id) { tup in
|
||||||
let ev = tup.0
|
let ev = tup.0
|
||||||
let ind = tup.1
|
let ind = tup.1
|
||||||
@@ -62,6 +65,9 @@ struct InnerTimelineView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onDisappear {
|
||||||
|
self.events.removeFilteredHolder(id: self.filteredEventHolderId)
|
||||||
|
}
|
||||||
//.padding(.horizontal)
|
//.padding(.horizontal)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ import SwiftUI
|
|||||||
struct PostingTimelineView: View {
|
struct PostingTimelineView: View {
|
||||||
|
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
var home: HomeModel
|
@ObservedObject var home: HomeModel
|
||||||
|
/// Set this to `home.events`. This is separate from `home` because we need the events object to be directly observed so that we get instant view updates
|
||||||
|
@ObservedObject var homeEvents: EventHolder
|
||||||
@State var search: String = ""
|
@State var search: String = ""
|
||||||
@State var results: [NostrEvent] = []
|
@State var results: [NostrEvent] = []
|
||||||
@State var initialOffset: CGFloat?
|
@State var initialOffset: CGFloat?
|
||||||
@@ -26,6 +28,14 @@ struct PostingTimelineView: View {
|
|||||||
@Binding var headerOffset: CGFloat
|
@Binding var headerOffset: CGFloat
|
||||||
@SceneStorage("PostingTimelineView.filter_state") var filter_state : FilterState = .posts_and_replies
|
@SceneStorage("PostingTimelineView.filter_state") var filter_state : FilterState = .posts_and_replies
|
||||||
@State var timeline_source: TimelineSource = .follows
|
@State var timeline_source: TimelineSource = .follows
|
||||||
|
|
||||||
|
var loading: Binding<Bool> {
|
||||||
|
Binding(get: {
|
||||||
|
return home.loading
|
||||||
|
}, set: {
|
||||||
|
home.loading = $0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
|
func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
|
||||||
var filters = ContentFilters.defaults(damus_state: damus_state)
|
var filters = ContentFilters.defaults(damus_state: damus_state)
|
||||||
@@ -40,21 +50,21 @@ struct PostingTimelineView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
|
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
|
||||||
TimelineView<AnyView>(events: home.events, loading: .constant(false), headerHeight: $headerHeight, headerOffset: $headerOffset, damus: damus_state, show_friend_icon: false, filter: filter)
|
TimelineView<AnyView>(events: home.events, loading: self.loading, headerHeight: $headerHeight, headerOffset: $headerOffset, damus: damus_state, show_friend_icon: false, filter: filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
func HeaderView()->some View {
|
func HeaderView() -> some View {
|
||||||
VStack {
|
VStack {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// This is needed for the Dynamic Island
|
// This is needed for the Dynamic Island
|
||||||
HStack {}
|
HStack {}
|
||||||
.frame(height: getSafeAreaTop())
|
.frame(height: getSafeAreaTop())
|
||||||
|
|
||||||
HStack(alignment: .top) {
|
HStack(alignment: .top) {
|
||||||
TopbarSideMenuButton(damus_state: damus_state, isSideBarOpened: $isSideBarOpened)
|
TopbarSideMenuButton(damus_state: damus_state, isSideBarOpened: $isSideBarOpened)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
SignalView(state: damus_state, signal: home.signal)
|
SignalView(state: damus_state, signal: home.signal)
|
||||||
let switchView = PostingTimelineSwitcherView(
|
let switchView = PostingTimelineSwitcherView(
|
||||||
@@ -147,6 +157,7 @@ struct PostingTimelineView_Previews: PreviewProvider {
|
|||||||
PostingTimelineView(
|
PostingTimelineView(
|
||||||
damus_state: test_damus_state,
|
damus_state: test_damus_state,
|
||||||
home: HomeModel(),
|
home: HomeModel(),
|
||||||
|
homeEvents: .init(),
|
||||||
isSideBarOpened: .constant(false),
|
isSideBarOpened: .constant(false),
|
||||||
active_sheet: .constant(nil),
|
active_sheet: .constant(nil),
|
||||||
headerOffset: .constant(0)
|
headerOffset: .constant(0)
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ struct SideMenuView: View {
|
|||||||
}, label: {
|
}, label: {
|
||||||
navLabel(title: NSLocalizedString("Logout", comment: "Sidebar menu label to sign out of the account."), img: "logout")
|
navLabel(title: NSLocalizedString("Logout", comment: "Sidebar menu label to sign out of the account."), img: "logout")
|
||||||
})
|
})
|
||||||
|
.accessibilityIdentifier(AppAccessibilityIdentifiers.side_menu_logout_button.rawValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +121,7 @@ struct SideMenuView: View {
|
|||||||
return VStack(alignment: .leading) {
|
return VStack(alignment: .leading) {
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
|
|
||||||
ProfilePicView(pubkey: damus_state.pubkey, size: 50, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
|
ProfilePicView(pubkey: damus_state.pubkey, size: 50, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation, damusState: damus_state)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
@@ -227,6 +228,7 @@ struct SideMenuView: View {
|
|||||||
Button(NSLocalizedString("Logout", comment: "Button for logging out the user."), role: .destructive) {
|
Button(NSLocalizedString("Logout", comment: "Button for logging out the user."), role: .destructive) {
|
||||||
logout(damus_state)
|
logout(damus_state)
|
||||||
}
|
}
|
||||||
|
.accessibilityIdentifier(AppAccessibilityIdentifiers.side_menu_logout_confirm_button.rawValue)
|
||||||
} message: {
|
} message: {
|
||||||
Text("Make sure your nsec account key is saved before you logout or you will lose access to this account", comment: "Reminder message in alert to get customer to verify that their private security account key is saved saved before logging out.")
|
Text("Make sure your nsec account key is saved before you logout or you will lose access to this account", comment: "Reminder message in alert to get customer to verify that their private security account key is saved saved before logging out.")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,9 +104,10 @@ struct TimelineView<Content: View>: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.coordinateSpace(name: "scroll")
|
.coordinateSpace(name: "scroll")
|
||||||
|
.disabled(self.loading)
|
||||||
.onReceive(handle_notify(.scroll_to_top)) { () in
|
.onReceive(handle_notify(.scroll_to_top)) { () in
|
||||||
events.flush()
|
events.flush()
|
||||||
self.events.should_queue = false
|
self.events.set_should_queue(false)
|
||||||
scroll_to_event(scroller: scroller, id: "startblock", delay: 0.0, animate: true, anchor: .top)
|
scroll_to_event(scroller: scroller, id: "startblock", delay: 0.0, animate: true, anchor: .top)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,11 +132,8 @@ protocol ScrollQueue {
|
|||||||
|
|
||||||
func handle_scroll_queue(_ proxy: GeometryProxy, queue: ScrollQueue) {
|
func handle_scroll_queue(_ proxy: GeometryProxy, queue: ScrollQueue) {
|
||||||
let offset = -proxy.frame(in: .named("scroll")).origin.y
|
let offset = -proxy.frame(in: .named("scroll")).origin.y
|
||||||
guard offset >= 0 else {
|
let new_should_queue = offset > 0
|
||||||
return
|
if queue.should_queue != new_should_queue {
|
||||||
}
|
queue.set_should_queue(new_should_queue)
|
||||||
let val = offset > 0
|
|
||||||
if queue.should_queue != val {
|
|
||||||
queue.set_should_queue(val)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ extension WalletConnect {
|
|||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - url: The Nostr Wallet Connect URL containing connection info to the NWC wallet
|
/// - url: The Nostr Wallet Connect URL containing connection info to the NWC wallet
|
||||||
/// - pool: The RelayPool to send the subscription request through
|
/// - pool: The RelayPool to send the subscription request through
|
||||||
static func subscribe(url: WalletConnectURL, pool: RelayPool) {
|
static func subscribe(url: WalletConnectURL, pool: RelayPool) async {
|
||||||
var filter = NostrFilter(kinds: [.nwc_response])
|
var filter = NostrFilter(kinds: [.nwc_response])
|
||||||
filter.authors = [url.pubkey]
|
filter.authors = [url.pubkey]
|
||||||
filter.pubkeys = [url.keypair.pubkey]
|
filter.pubkeys = [url.keypair.pubkey]
|
||||||
filter.limit = 0
|
filter.limit = 0
|
||||||
let sub = NostrSubscribe(filters: [filter], sub_id: "nwc")
|
let sub = NostrSubscribe(filters: [filter], sub_id: "nwc")
|
||||||
|
|
||||||
pool.send(.subscribe(sub), to: [url.relay], skip_ephemeral: false)
|
await pool.send(.subscribe(sub), to: [url.relay], skip_ephemeral: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends out a request to pay an invoice to the NWC relay, and ensures that:
|
/// Sends out a request to pay an invoice to the NWC relay, and ensures that:
|
||||||
@@ -41,93 +41,19 @@ extension WalletConnect {
|
|||||||
/// - on_flush: A callback to call after the event has been flushed to the network
|
/// - on_flush: A callback to call after the event has been flushed to the network
|
||||||
/// - Returns: The Nostr Event that was sent to the network, representing the request that was made
|
/// - Returns: The Nostr Event that was sent to the network, representing the request that was made
|
||||||
@discardableResult
|
@discardableResult
|
||||||
static func pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: String, zap_request: NostrEvent?, delay: TimeInterval? = 5.0, on_flush: OnFlush? = nil) -> NostrEvent? {
|
static func pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: String, zap_request: NostrEvent?, delay: TimeInterval? = 5.0, on_flush: OnFlush? = nil) async -> NostrEvent? {
|
||||||
|
|
||||||
let req = WalletConnect.Request.payZapRequest(invoice: invoice, zapRequest: zap_request)
|
let req = WalletConnect.Request.payZapRequest(invoice: invoice, zapRequest: zap_request)
|
||||||
guard let ev = req.to_nostr_event(to_pk: url.pubkey, keypair: url.keypair) else {
|
guard let ev = req.to_nostr_event(to_pk: url.pubkey, keypair: url.keypair) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
try? pool.add_relay(.nwc(url: url.relay)) // Ensure the NWC relay is connected
|
try? await pool.add_relay(.nwc(url: url.relay)) // Ensure the NWC relay is connected
|
||||||
WalletConnect.subscribe(url: url, pool: pool) // Ensure we are listening to NWC updates from the relay
|
await WalletConnect.subscribe(url: url, pool: pool) // Ensure we are listening to NWC updates from the relay
|
||||||
post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush)
|
await post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush)
|
||||||
return ev
|
return ev
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sends out a wallet balance request to the NWC relay, and ensures that:
|
|
||||||
/// 1. the NWC relay is connected and we are listening to NWC events
|
|
||||||
/// 2. the NWC relay is connected and we are listening to NWC
|
|
||||||
///
|
|
||||||
/// Note: This does not return the actual balance information. The actual balance is handled elsewhere around `HomeModel` and `WalletModel`
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - url: The NWC wallet connection URL
|
|
||||||
/// - pool: The relay pool to connect to
|
|
||||||
/// - post: The postbox to send events in
|
|
||||||
/// - delay: The delay before actually sending the request to the network
|
|
||||||
/// - on_flush: A callback to call after the event has been flushed to the network
|
|
||||||
/// - Returns: The Nostr Event that was sent to the network, representing the request that was made
|
|
||||||
@discardableResult
|
|
||||||
static func request_balance_information(url: WalletConnectURL, pool: RelayPool, post: PostBox, delay: TimeInterval? = 0.0, on_flush: OnFlush? = nil) -> NostrEvent? {
|
|
||||||
let req = WalletConnect.Request.getBalance
|
|
||||||
guard let ev = req.to_nostr_event(to_pk: url.pubkey, keypair: url.keypair) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
try? pool.add_relay(.nwc(url: url.relay)) // Ensure the NWC relay is connected
|
|
||||||
WalletConnect.subscribe(url: url, pool: pool) // Ensure we are listening to NWC updates from the relay
|
|
||||||
post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush)
|
|
||||||
return ev
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sends out a wallet transaction list request to the NWC relay, and ensures that:
|
|
||||||
/// 1. the NWC relay is connected and we are listening to NWC events
|
|
||||||
/// 2. the NWC relay is connected and we are listening to NWC
|
|
||||||
///
|
|
||||||
/// Note: This does not return the actual transaction list. The actual transaction list is handled elsewhere around `HomeModel` and `WalletModel`
|
|
||||||
///
|
|
||||||
/// - Parameters:
|
|
||||||
/// - url: The NWC wallet connection URL
|
|
||||||
/// - pool: The relay pool to connect to
|
|
||||||
/// - post: The postbox to send events in
|
|
||||||
/// - delay: The delay before actually sending the request to the network
|
|
||||||
/// - on_flush: A callback to call after the event has been flushed to the network
|
|
||||||
/// - Returns: The Nostr Event that was sent to the network, representing the request that was made
|
|
||||||
@discardableResult
|
|
||||||
static func request_transaction_list(url: WalletConnectURL, pool: RelayPool, post: PostBox, delay: TimeInterval? = 0.0, on_flush: OnFlush? = nil) -> NostrEvent? {
|
|
||||||
let req = WalletConnect.Request.getTransactionList(from: nil, until: nil, limit: 10, offset: 0, unpaid: false, type: "")
|
|
||||||
guard let ev = req.to_nostr_event(to_pk: url.pubkey, keypair: url.keypair) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
try? pool.add_relay(.nwc(url: url.relay)) // Ensure the NWC relay is connected
|
|
||||||
WalletConnect.subscribe(url: url, pool: pool) // Ensure we are listening to NWC updates from the relay
|
|
||||||
post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush)
|
|
||||||
return ev
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
static func refresh_wallet_information(damus_state: DamusState) async {
|
|
||||||
damus_state.wallet.resetWalletStateInformation()
|
|
||||||
await Self.update_wallet_information(damus_state: damus_state)
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
static func update_wallet_information(damus_state: DamusState) async {
|
|
||||||
guard let url = damus_state.settings.nostr_wallet_connect,
|
|
||||||
let nwc = WalletConnectURL(str: url) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let flusher: OnFlush? = nil
|
|
||||||
|
|
||||||
let delay = 0.0 // We don't need a delay when fetching a transaction list or balance
|
|
||||||
|
|
||||||
WalletConnect.request_transaction_list(url: nwc, pool: damus_state.nostrNetwork.pool, post: damus_state.nostrNetwork.postbox, delay: delay, on_flush: flusher)
|
|
||||||
WalletConnect.request_balance_information(url: nwc, pool: damus_state.nostrNetwork.pool, post: damus_state.nostrNetwork.postbox, delay: delay, on_flush: flusher)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
static func handle_zap_success(state: DamusState, resp: WalletConnect.FullWalletResponse) {
|
static func handle_zap_success(state: DamusState, resp: WalletConnect.FullWalletResponse) {
|
||||||
// find the pending zap and mark it as pending-confirmed
|
// find the pending zap and mark it as pending-confirmed
|
||||||
for kv in state.zaps.our_zaps {
|
for kv in state.zaps.our_zaps {
|
||||||
@@ -153,22 +79,6 @@ extension WalletConnect {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a donation zap to the Damus team
|
|
||||||
static func send_donation_zap(pool: RelayPool, postbox: PostBox, nwc: WalletConnectURL, percent: Int, base_msats: Int64) async {
|
|
||||||
let percent_f = Double(percent) / 100.0
|
|
||||||
let donations_msats = Int64(percent_f * Double(base_msats))
|
|
||||||
|
|
||||||
let payreq = LNUrlPayRequest(allowsNostr: true, commentAllowed: nil, nostrPubkey: "", callback: "https://sendsats.lol/@damus")
|
|
||||||
guard let invoice = await fetch_zap_invoice(payreq, zapreq: nil, msats: donations_msats, zap_type: .non_zap, comment: nil) else {
|
|
||||||
// we failed... oh well. no donation for us.
|
|
||||||
print("damus-donation failed to fetch invoice")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
print("damus-donation donating...")
|
|
||||||
WalletConnect.pay(url: nwc, pool: pool, post: postbox, invoice: invoice, zap_request: nil, delay: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handles a received Nostr Wallet Connect error
|
/// Handles a received Nostr Wallet Connect error
|
||||||
static func handle_error(zapcache: Zaps, evcache: EventCache, resp: WalletConnect.FullWalletResponse) {
|
static func handle_error(zapcache: Zaps, evcache: EventCache, resp: WalletConnect.FullWalletResponse) {
|
||||||
// find a pending zap with the nwc request id associated with this response and remove it
|
// find a pending zap with the nwc request id associated with this response and remove it
|
||||||
|
|||||||
@@ -11,11 +11,24 @@ enum WalletConnectState {
|
|||||||
case new(WalletConnectURL)
|
case new(WalletConnectURL)
|
||||||
case existing(WalletConnectURL)
|
case existing(WalletConnectURL)
|
||||||
case none
|
case none
|
||||||
|
|
||||||
|
/// Gets the currently connected NWC URL
|
||||||
|
func currentNwcUrl() -> WalletConnectURL? {
|
||||||
|
switch self {
|
||||||
|
case .new:
|
||||||
|
return nil // User has not confirmed they want to use this yet, so we cannot call it "current"
|
||||||
|
case .existing(let nwcUrl):
|
||||||
|
return nwcUrl
|
||||||
|
case .none:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Models and manages the user's NWC wallet based on the app's settings
|
/// Models and manages the user's NWC wallet based on the app's settings
|
||||||
class WalletModel: ObservableObject {
|
class WalletModel: ObservableObject {
|
||||||
var settings: UserSettingsStore
|
var settings: UserSettingsStore
|
||||||
|
var nostrNetwork: NostrNetworkManager? = nil
|
||||||
private(set) var previous_state: WalletConnectState
|
private(set) var previous_state: WalletConnectState
|
||||||
var initial_percent: Int
|
var initial_percent: Int
|
||||||
/// The wallet's balance, in sats.
|
/// The wallet's balance, in sats.
|
||||||
@@ -37,6 +50,7 @@ class WalletModel: ObservableObject {
|
|||||||
self.previous_state = .none
|
self.previous_state = .none
|
||||||
self.settings = settings
|
self.settings = settings
|
||||||
self.initial_percent = settings.donation_percent
|
self.initial_percent = settings.donation_percent
|
||||||
|
self.nostrNetwork = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
init(settings: UserSettingsStore) {
|
init(settings: UserSettingsStore) {
|
||||||
@@ -50,6 +64,7 @@ class WalletModel: ObservableObject {
|
|||||||
self.connect_state = .none
|
self.connect_state = .none
|
||||||
}
|
}
|
||||||
self.initial_percent = settings.donation_percent
|
self.initial_percent = settings.donation_percent
|
||||||
|
self.nostrNetwork = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func cancel() {
|
func cancel() {
|
||||||
@@ -96,12 +111,107 @@ class WalletModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// MARK: - Wallet internal state lifecycle functions
|
||||||
|
|
||||||
|
@MainActor
|
||||||
func resetWalletStateInformation() {
|
func resetWalletStateInformation() {
|
||||||
self.transactions = nil
|
self.transactions = nil
|
||||||
self.balance = nil
|
self.balance = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func refreshWalletInformation() async throws {
|
||||||
|
await self.resetWalletStateInformation()
|
||||||
|
try await loadWalletInformation()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadWalletInformation() async throws {
|
||||||
|
try await loadBalance()
|
||||||
|
try await loadTransactionList()
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadBalance() async throws {
|
||||||
|
let balance = try await fetchBalance()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.balance = balance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadTransactionList() async throws {
|
||||||
|
let transactions = try await fetchTransactions(from: nil, until: nil, limit: 50, offset: 0, unpaid: false, type: "")
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.transactions = transactions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Easy wallet info fetching interface
|
||||||
|
|
||||||
|
func fetchTransactions(from: UInt64?, until: UInt64?, limit: Int?, offset: Int?, unpaid: Bool?, type: String?) async throws -> [WalletConnect.Transaction] {
|
||||||
|
let response = try await self.request(.getTransactionList(from: from, until: until, limit: limit, offset: offset, unpaid: unpaid, type: type))
|
||||||
|
guard case .list_transactions(let transactionResponse) = response else { throw FetchError.responseMismatch }
|
||||||
|
return transactionResponse.transactions
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Fetches the balance amount from the network and returns the amount in sats
|
||||||
|
func fetchBalance() async throws -> Int64 {
|
||||||
|
let response = try await self.request(.getBalance)
|
||||||
|
guard case .get_balance(let balanceResponse) = response else { throw FetchError.responseMismatch }
|
||||||
|
return balanceResponse.balance / 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
enum FetchError: Error {
|
||||||
|
case responseMismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Easy request/response interface
|
||||||
|
|
||||||
|
func request(_ request: WalletConnect.Request, timeout: Duration = .seconds(10)) async throws(WalletRequestError) -> WalletConnect.Response.Result {
|
||||||
|
guard let nostrNetwork else { throw .notConnectedToTheNostrNetwork }
|
||||||
|
guard let currentNwcUrl = self.connect_state.currentNwcUrl() else { throw .noConnectedWallet }
|
||||||
|
guard let requestEvent = request.to_nostr_event(to_pk: currentNwcUrl.pubkey, keypair: currentNwcUrl.keypair) else { throw .errorFormattingRequest }
|
||||||
|
|
||||||
|
let responseFilters = [
|
||||||
|
NostrFilter(
|
||||||
|
kinds: [.nwc_response],
|
||||||
|
referenced_ids: [requestEvent.id],
|
||||||
|
pubkeys: [currentNwcUrl.keypair.pubkey],
|
||||||
|
authors: [currentNwcUrl.pubkey]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
await nostrNetwork.send(event: requestEvent, to: [currentNwcUrl.relay], skipEphemeralRelays: false)
|
||||||
|
for await event in nostrNetwork.reader.timedStream(filters: responseFilters, to: [currentNwcUrl.relay], timeout: timeout) {
|
||||||
|
guard let responseEvent = try? event.getCopy() else { throw .internalError }
|
||||||
|
|
||||||
|
let fullWalletResponse: WalletConnect.FullWalletResponse
|
||||||
|
do { fullWalletResponse = try WalletConnect.FullWalletResponse(from: responseEvent, nwc: currentNwcUrl) }
|
||||||
|
catch { throw WalletRequestError.walletResponseDecodingError(error) }
|
||||||
|
|
||||||
|
guard fullWalletResponse.req_id == requestEvent.id else { continue } // Our filters may match other responses
|
||||||
|
if let responseError = fullWalletResponse.response.error { throw .walletResponseError(responseError) }
|
||||||
|
|
||||||
|
guard let result = fullWalletResponse.response.result else { throw .walletEmptyResponse }
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
do { try Task.checkCancellation() } catch { throw .cancelled }
|
||||||
|
throw .responseTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
enum WalletRequestError: Error {
|
||||||
|
case notConnectedToTheNostrNetwork
|
||||||
|
case noConnectedWallet
|
||||||
|
case errorFormattingRequest
|
||||||
|
case internalError
|
||||||
|
case walletResponseDecodingError(WalletConnect.FullWalletResponse.InitializationError)
|
||||||
|
case walletResponseMismatch
|
||||||
|
case walletResponseError(WalletConnect.WalletResponseErr)
|
||||||
|
case walletEmptyResponse
|
||||||
|
case responseTimeout
|
||||||
|
case cancelled
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Async wallet response waiting mechanism
|
// MARK: - Async wallet response waiting mechanism
|
||||||
|
|
||||||
func waitForResponse(for requestId: NoteId, timeout: Duration = .seconds(10)) async throws -> WalletConnect.Response.Result {
|
func waitForResponse(for requestId: NoteId, timeout: Duration = .seconds(10)) async throws -> WalletConnect.Response.Result {
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ struct NWCSettings: View {
|
|||||||
guard let meta = make_metadata_event(keypair: keypair, metadata: prof) else {
|
guard let meta = make_metadata_event(keypair: keypair, metadata: prof) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
damus_state.nostrNetwork.postbox.send(meta)
|
Task { await damus_state.nostrNetwork.postbox.send(meta) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,11 +45,11 @@ struct SendPaymentView: View {
|
|||||||
break
|
break
|
||||||
case .completed:
|
case .completed:
|
||||||
// Refresh wallet to reflect new balance after payment
|
// Refresh wallet to reflect new balance after payment
|
||||||
Task { await WalletConnect.refresh_wallet_information(damus_state: damus_state) }
|
Task { try await model.refreshWalletInformation() }
|
||||||
case .failed:
|
case .failed:
|
||||||
// Even when a wallet says it has failed, update balance just in case it is a false negative,
|
// Even when a wallet says it has failed, update balance just in case it is a false negative,
|
||||||
// This might prevent the user from accidentally sending a payment twice in case of a bug.
|
// This might prevent the user from accidentally sending a payment twice in case of a bug.
|
||||||
Task { await WalletConnect.refresh_wallet_information(damus_state: damus_state) }
|
Task { try await model.refreshWalletInformation() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,18 +182,18 @@ struct SendPaymentView: View {
|
|||||||
.buttonStyle(NeutralButtonStyle())
|
.buttonStyle(NeutralButtonStyle())
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
sendState = .processing
|
|
||||||
|
|
||||||
// Process payment
|
|
||||||
guard let payRequestEv = WalletConnect.pay(url: nwc, pool: damus_state.nostrNetwork.pool, post: damus_state.nostrNetwork.postbox, invoice: invoice.string, zap_request: nil, delay: nil) else {
|
|
||||||
sendState = .failed(error: .init(
|
|
||||||
user_visible_description: NSLocalizedString("The payment request could not be made to your wallet provider.", comment: "A human-readable error message"),
|
|
||||||
tip: NSLocalizedString("Check if your wallet looks configured correctly and try again. If the error persists, please contact support.", comment: "A human-readable tip for an error when a payment request cannot be made to a wallet."),
|
|
||||||
technical_info: "Cannot form Nostr Event to send to the NWC provider when calling `pay` from the \"send payment\" feature. Wallet provider relay: \"\(nwc.relay)\""
|
|
||||||
))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Task {
|
Task {
|
||||||
|
sendState = .processing
|
||||||
|
|
||||||
|
// Process payment
|
||||||
|
guard let payRequestEv = await damus_state.nostrNetwork.nwcPay(url: nwc, post: damus_state.nostrNetwork.postbox, invoice: invoice.string, zap_request: nil) else {
|
||||||
|
sendState = .failed(error: .init(
|
||||||
|
user_visible_description: NSLocalizedString("The payment request could not be made to your wallet provider.", comment: "A human-readable error message"),
|
||||||
|
tip: NSLocalizedString("Check if your wallet looks configured correctly and try again. If the error persists, please contact support.", comment: "A human-readable tip for an error when a payment request cannot be made to a wallet."),
|
||||||
|
technical_info: "Cannot form Nostr Event to send to the NWC provider when calling `pay` from the \"send payment\" feature. Wallet provider relay: \"\(nwc.relay)\""
|
||||||
|
))
|
||||||
|
return
|
||||||
|
}
|
||||||
do {
|
do {
|
||||||
let result = try await model.waitForResponse(for: payRequestEv.id, timeout: SEND_PAYMENT_TIMEOUT)
|
let result = try await model.waitForResponse(for: payRequestEv.id, timeout: SEND_PAYMENT_TIMEOUT)
|
||||||
guard case .pay_invoice(_) = result else {
|
guard case .pay_invoice(_) = result else {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ struct TransactionView: View {
|
|||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
ZStack {
|
ZStack {
|
||||||
ProfilePicView(pubkey: pubkey ?? ANON_PUBKEY, size: 45, highlight: .custom(.damusAdaptableBlack, 0.1), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation, privacy_sensitive: true)
|
ProfilePicView(pubkey: pubkey ?? ANON_PUBKEY, size: 45, highlight: .custom(.damusAdaptableBlack, 0.1), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation, privacy_sensitive: true, damusState: damus_state)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if let pubkey {
|
if let pubkey {
|
||||||
damus_state.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
|
damus_state.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user