Compare commits

..

39 Commits

Author SHA1 Message Date
tyiu 7ee970ea9e Hide future notes from timeline
Changelog-Fixed: Hide future notes from timeline

Closes: https://github.com/damus-io/damus/issues/2949
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-04-18 16:21:50 -07:00
tyiu e7fe4ab9b4 Inverse hellthread_notifications_enabled to be hellthread_notifications_disabled and add hellthread_notifications_max_pubkeys setting
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-04-18 16:14:13 -07:00
tyiu c146bab08a Add notification setting to hide hellthreads
Changelog-Added: Add notification setting to hide hellthreads
Closes: https://github.com/damus-io/damus/issues/2943
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-04-18 16:14:13 -07:00
Daniel D’Aquino d1cced8d54 Fetch NIP-65 relay lists from profile view
Changelog-Fixed: Fixed issue where profiles with a NIP-65 relay list would not display on Damus
Closes: https://github.com/damus-io/damus/issues/2120
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-04-16 11:48:52 -07:00
Daniel D’Aquino 8849b6105c Add First Aid tool to repair relay list
This adds a First aid tool to repair the NIP-65 relay list

Changelog-Added: Added separated first aid option for relay lists that does not need a contact list reset
Closes: https://github.com/damus-io/damus/issues/2120
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-04-16 11:48:52 -07:00
Daniel D’Aquino 3a0acfaba1 Implement NostrNetworkManager and UserRelayListManager
This commit implements a new layer called NostrNetworkManager,
responsible for managing interactions with the Nostr network, and
providing a higher level API that is easier and more secure to use for
the layer above it.

It also integrates it with the rest of the app, by moving RelayPool and PostBox
into NostrNetworkManager, along with all their usages.

Changelog-Added: Added NIP-65 relay list support
Changelog-Changed: Improved robustness of relay list handling
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-04-16 11:48:52 -07:00
Daniel D’Aquino 0ec2b05070 Implement safe interface for unowned NdbNotes
This commit introduces a new interface that makes it easier and safer to
handle unowned NostrDB notes, by leveraging new non-copyable and borrow
features from modern Swift.

Changelog-None
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-04-16 11:48:52 -07:00
Daniel D’Aquino 130bbfafb4 New async streaming interface from RelayPool
This defines a higher level and easier to use streaming interface from
RelayPool.

Changelog-None
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-04-16 11:48:52 -07:00
Daniel D’Aquino ffc75772f9 NIP-65 relay list models and definitions
This commit adds the base models needed for the NIP-65 relay list support.

This introduces no user-facing changes.

Changelog-None
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-04-16 11:48:52 -07:00
Daniel D’Aquino 5b3fac70ed Organize RelayPool namespace
This is a non-functional refactor that organizes some classes and
structs used by RelayPool under the same namespace.

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-04-16 11:48:52 -07:00
Daniel D’Aquino 53e3f6d86b Define protocol NostrEventConvertible
This adds a new protocol for classes that can be converted to and from a
NostrEvent.

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-04-16 11:48:52 -07:00
Daniel D’Aquino c28ab7a57c Renamed RelayInfo to LegacyKind3RelayRWConfiguration
This is a non-functional refactor that makes a struct name more
detailed.

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-04-16 11:48:52 -07:00
Daniel D’Aquino 09ce3af11e Add some miscellaneous documentation
This commit adds some documentation to miscellaneous functions and
classes.

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-04-16 11:48:52 -07:00
tyiu e42c09883a Replace deprecated usage of UIMenuController with UITextViewDelegate
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-04-14 19:11:30 -07:00
tyiu 77e3924809 Fix some compiler warnings
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-04-14 19:11:30 -07:00
tyiu 3511b1ee91 Fix quote notes to include missing q tag
Changelog-Fixed: Fix quote notes to include missing q tag

Closes: https://github.com/damus-io/damus/issues/2615
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-04-14 19:10:46 -07:00
tyiu 78a62c8ef0 Clean up code in ProfileName.name_choice
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-04-14 18:38:51 -07:00
Daniel D’Aquino 8b96b9f4e6 Merge pull request #2973 from damus-io/translations
Translations
2025-04-14 18:35:34 -07:00
Daniel D’Aquino 649a857c3a Update Kingfisher to 8.3.1
Changelog-Changed: Updated image cache for better stability
Closes: https://github.com/damus-io/damus/issues/2899
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-04-14 17:56:23 -07:00
transifex-integration[bot] cdae2c7558 Translate Localizable.strings in pl_PL
100% translated source file: 'Localizable.strings'
on 'pl_PL'.
2025-04-08 11:52:23 +00:00
transifex-integration[bot] 3639110c51 Translate Localizable.strings in nl
100% translated source file: 'Localizable.strings'
on 'nl'.
2025-04-08 08:39:51 +00:00
tyiu 186668512e Export strings for translation
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-04-07 20:39:27 -04:00
tyiu f63666fae2 Add missing localized string comment
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-04-07 20:39:08 -04:00
transifex-integration[bot] 68d25059b1 Translate Localizable.strings in pl_PL
100% translated source file: 'Localizable.strings'
on 'pl_PL'.
2025-04-07 20:25:30 -04:00
transifex-integration[bot] 9aef6b7f5b Translate Localizable.strings in pl_PL
100% translated source file: 'Localizable.strings'
on 'pl_PL'.
2025-04-07 20:25:30 -04:00
transifex-integration[bot] d2e712575f Translate Localizable.strings in pl_PL
100% translated source file: 'Localizable.strings'
on 'pl_PL'.
2025-04-07 20:25:30 -04:00
transifex-integration[bot] bf9674e6e4 Translate Localizable.strings in pl_PL
100% translated source file: 'Localizable.strings'
on 'pl_PL'.
2025-04-07 20:25:30 -04:00
transifex-integration[bot] 4815390cbe Translate Localizable.strings in pl_PL
100% translated source file: 'Localizable.strings'
on 'pl_PL'.
2025-04-07 20:25:30 -04:00
transifex-integration[bot] 6ce903f1f6 Translate Localizable.strings in pl_PL
100% translated source file: 'Localizable.strings'
on 'pl_PL'.
2025-04-07 20:25:30 -04:00
transifex-integration[bot] b2c91ffce4 Translate Localizable.strings in pl_PL
100% translated source file: 'Localizable.strings'
on 'pl_PL'.
2025-04-07 20:25:30 -04:00
transifex-integration[bot] ae335b18bf Translate Localizable.strings in pl_PL
100% translated source file: 'Localizable.strings'
on 'pl_PL'.
2025-04-07 20:25:29 -04:00
transifex-integration[bot] 6391819fb2 Translate Localizable.strings in pl_PL
100% translated source file: 'Localizable.strings'
on 'pl_PL'.
2025-04-07 20:25:29 -04:00
transifex-integration[bot] 5d0e56b7c7 Translate Localizable.strings in pl_PL
100% translated source file: 'Localizable.strings'
on 'pl_PL'.
2025-04-07 20:25:29 -04:00
transifex-integration[bot] 50ccc7bd7f Translate Localizable.strings in pl_PL
100% translated source file: 'Localizable.strings'
on 'pl_PL'.
2025-04-07 20:25:29 -04:00
transifex-integration[bot] b3a6bcf3b2 Translate Localizable.strings in pl_PL
100% translated source file: 'Localizable.strings'
on 'pl_PL'.
2025-04-07 20:25:29 -04:00
transifex-integration[bot] 38b2988bbe Translate Localizable.strings in pl_PL
100% translated source file: 'Localizable.strings'
on 'pl_PL'.
2025-04-07 20:25:29 -04:00
transifex-integration[bot] 446c541dcb Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2025-04-07 20:25:29 -04:00
transifex-integration[bot] 31fd48ee52 Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2025-04-07 20:25:28 -04:00
tyiu b35cc33c32 Add Unicode 16 emoji reactions for iOS 18.4+ by upgrading EmojiPicker
Changelog-Added: Added Unicode 16 emoji reactions for iOS 18.4+ by upgrading EmojiPicker
Closes: https://github.com/damus-io/damus/issues/2915
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-03-29 11:40:47 -03:00
100 changed files with 1681 additions and 655 deletions
@@ -18,7 +18,7 @@ struct NotificationExtensionState: HeadlessDamusState {
let lnurls: LNUrls
init?() {
guard let ndb = try? Ndb(owns_db_file: false) else { return nil }
guard let ndb = Ndb(owns_db_file: false) else { return nil }
self.ndb = ndb
guard let keypair = get_saved_keypair() else { return nil }
+68 -10
View File
@@ -642,7 +642,6 @@
82D6FB6C2CD99F7900C925F4 /* DamusPurpleURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADD3DD2B53854300F104C4 /* DamusPurpleURL.swift */; };
82D6FB6D2CD99F7900C925F4 /* DamusPurpleEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */; };
82D6FB6E2CD99F7900C925F4 /* PurpleStoreKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C5D2B7709ED00C59298 /* PurpleStoreKitManager.swift */; };
82D6FB6F2CD99F7900C925F4 /* CameraService+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */; };
82D6FB702CD99F7900C925F4 /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; };
82D6FB712CD99F7900C925F4 /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; };
82D6FB722CD99F7900C925F4 /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; };
@@ -933,7 +932,6 @@
BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; };
BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; };
BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; };
BA3759922ABCCEBA0018D73B /* CameraService+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */; };
BA3759932ABCCEBA0018D73B /* CameraModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759902ABCCEBA0018D73B /* CameraModel.swift */; };
BA3759942ABCCEBA0018D73B /* CameraService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759912ABCCEBA0018D73B /* CameraService.swift */; };
BA3759972ABCCF360018D73B /* CameraPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759962ABCCF360018D73B /* CameraPreview.swift */; };
@@ -1092,6 +1090,13 @@
D72E127A2BEEEED000F4F781 /* NostrFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */; };
D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; };
D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; };
D733F9E12D92C1D900317B11 /* SubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E02D92C1AA00317B11 /* SubscriptionManager.swift */; };
D733F9E22D92C1D900317B11 /* SubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E02D92C1AA00317B11 /* SubscriptionManager.swift */; };
D733F9E32D92C1D900317B11 /* SubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E02D92C1AA00317B11 /* SubscriptionManager.swift */; };
D733F9E52D92C76100317B11 /* UnownedNdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */; };
D733F9E62D92C76100317B11 /* UnownedNdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */; };
D733F9E72D92C76100317B11 /* UnownedNdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */; };
D733F9E82D92C76100317B11 /* UnownedNdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */; };
D734B1452CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */ = {isa = PBXBuildFile; fileRef = D734B1442CCC19B1000B5C97 /* DamusFullScreenCover.swift */; };
D734B1462CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */ = {isa = PBXBuildFile; fileRef = D734B1442CCC19B1000B5C97 /* DamusFullScreenCover.swift */; };
D7373BA62B688EA300F7783D /* DamusPurpleTranslationSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7373BA52B688EA200F7783D /* DamusPurpleTranslationSetupView.swift */; };
@@ -1100,6 +1105,15 @@
D73B74E12D8365BA0067BDBC /* ExtraFonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73B74E02D8365B40067BDBC /* ExtraFonts.swift */; };
D73B74E22D8365BA0067BDBC /* ExtraFonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73B74E02D8365B40067BDBC /* ExtraFonts.swift */; };
D73B74E32D8365BA0067BDBC /* ExtraFonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73B74E02D8365B40067BDBC /* ExtraFonts.swift */; };
D73BDB0D2D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB0C2D6FF58600D69970 /* NostrNetworkManager.swift */; };
D73BDB0E2D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB0C2D6FF58600D69970 /* NostrNetworkManager.swift */; };
D73BDB102D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB0C2D6FF58600D69970 /* NostrNetworkManager.swift */; };
D73BDB142D71216500D69970 /* UserRelayListManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB132D71215F00D69970 /* UserRelayListManager.swift */; };
D73BDB152D71216500D69970 /* UserRelayListManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB132D71215F00D69970 /* UserRelayListManager.swift */; };
D73BDB162D71216500D69970 /* UserRelayListManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB132D71215F00D69970 /* UserRelayListManager.swift */; };
D73BDB182D71311900D69970 /* UserRelayListErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB172D71310C00D69970 /* UserRelayListErrors.swift */; };
D73BDB192D71311900D69970 /* UserRelayListErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB172D71310C00D69970 /* UserRelayListErrors.swift */; };
D73BDB1A2D71311900D69970 /* UserRelayListErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB172D71310C00D69970 /* UserRelayListErrors.swift */; };
D73E5E162C6A9619007EB227 /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; };
D73E5E172C6A962A007EB227 /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; };
D73E5E182C6A963D007EB227 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; };
@@ -1209,7 +1223,6 @@
D73E5E882C6A97F4007EB227 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; };
D73E5E892C6A97F4007EB227 /* DamusPurpleURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADD3DD2B53854300F104C4 /* DamusPurpleURL.swift */; };
D73E5E8A2C6A97F4007EB227 /* PurpleStoreKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C5D2B7709ED00C59298 /* PurpleStoreKitManager.swift */; };
D73E5E8D2C6A97F4007EB227 /* CameraService+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */; };
D73E5E8E2C6A97F4007EB227 /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; };
D73E5E8F2C6A97F4007EB227 /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; };
D73E5E902C6A97F4007EB227 /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; };
@@ -1652,6 +1665,9 @@
D7DB93052D66A44100DA1EE5 /* Undistractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */; };
D7DB93062D66A44100DA1EE5 /* Undistractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */; };
D7DB93072D66A44100DA1EE5 /* Undistractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */; };
D7DB930A2D69486700DA1EE5 /* NIP65.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93092D69485A00DA1EE5 /* NIP65.swift */; };
D7DB930B2D69486700DA1EE5 /* NIP65.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93092D69485A00DA1EE5 /* NIP65.swift */; };
D7DB930C2D69486700DA1EE5 /* NIP65.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93092D69485A00DA1EE5 /* NIP65.swift */; };
D7DBD41F2B02F15E002A6197 /* NostrKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */; };
D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */; };
D7EB00B02CD59C8D00660C07 /* PresentFullScreenItemNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */; };
@@ -2442,7 +2458,6 @@
BA3759892ABCCDE30018D73B /* ImageResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageResizer.swift; sourceTree = "<group>"; };
BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureProcessor.swift; sourceTree = "<group>"; };
BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCaptureProcessor.swift; sourceTree = "<group>"; };
BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CameraService+Extensions.swift"; sourceTree = "<group>"; };
BA3759902ABCCEBA0018D73B /* CameraModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraModel.swift; sourceTree = "<group>"; };
BA3759912ABCCEBA0018D73B /* CameraService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraService.swift; sourceTree = "<group>"; };
BA3759962ABCCF360018D73B /* CameraPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPreview.swift; sourceTree = "<group>"; };
@@ -2478,11 +2493,16 @@
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>"; };
D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = "<group>"; };
D733F9E02D92C1AA00317B11 /* SubscriptionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionManager.swift; sourceTree = "<group>"; };
D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnownedNdbNote.swift; sourceTree = "<group>"; };
D734B1442CCC19B1000B5C97 /* DamusFullScreenCover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusFullScreenCover.swift; sourceTree = "<group>"; };
D7373BA52B688EA200F7783D /* DamusPurpleTranslationSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleTranslationSetupView.swift; sourceTree = "<group>"; };
D7373BA72B68974500F7783D /* DamusPurpleNewUserOnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleNewUserOnboardingView.swift; sourceTree = "<group>"; };
D7373BA92B68A65A00F7783D /* PurpleAccountUpdateNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurpleAccountUpdateNotify.swift; sourceTree = "<group>"; };
D73B74E02D8365B40067BDBC /* ExtraFonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtraFonts.swift; sourceTree = "<group>"; };
D73BDB0C2D6FF58600D69970 /* NostrNetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrNetworkManager.swift; sourceTree = "<group>"; };
D73BDB132D71215F00D69970 /* UserRelayListManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRelayListManager.swift; sourceTree = "<group>"; };
D73BDB172D71310C00D69970 /* UserRelayListErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRelayListErrors.swift; sourceTree = "<group>"; };
D73E5F7E2C6AA066007EB227 /* DamusAliases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusAliases.swift; sourceTree = "<group>"; };
D73E5F802C6AA07A007EB227 /* HighlighterExtensionAliases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlighterExtensionAliases.swift; sourceTree = "<group>"; };
D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlessDamusState.swift; sourceTree = "<group>"; };
@@ -2541,6 +2561,7 @@
D7DB1FF02D5AC5D700CF06DA /* nip44.vectors.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = nip44.vectors.json; sourceTree = "<group>"; };
D7DB1FF22D5AC5E400CF06DA /* LICENSES */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSES; sourceTree = "<group>"; };
D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Undistractor.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>"; };
D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentFullScreenItemNotify.swift; sourceTree = "<group>"; };
D7EDED1B2B1178FE0018B19C /* NoteContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContent.swift; sourceTree = "<group>"; };
@@ -2745,6 +2766,7 @@
4C0A3F8D280F63FF000448DE /* Models */ = {
isa = PBXGroup;
children = (
D73BDB122D71212600D69970 /* NostrNetworkManager */,
D74F43082B23F09300425B75 /* Purple */,
BA3759882ABCCDE30018D73B /* Camera */,
4C190F1E2A535FC200027FD5 /* Zaps */,
@@ -3359,6 +3381,7 @@
4C9054862A6AEB4500811EEC /* nostrdb */ = {
isa = PBXGroup;
children = (
D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */,
4C47928D2A9939BD00489948 /* flatcc */,
4C478E2A2A9935D300489948 /* bindings */,
4CE9FBBB2A6B3D9C007E485C /* Test */,
@@ -3650,6 +3673,7 @@
4CE6DEE527F7A08100C66700 /* damus */ = {
isa = PBXGroup;
children = (
D7DB93082D69478400DA1EE5 /* NIP65 */,
D7DB1FDC2D5A77E500CF06DA /* NIP44 */,
D755B28B2D3E7D6500BBEEFA /* NIP37 */,
D78F08152D7F7F5F00FC6C75 /* NIP04 */,
@@ -3893,7 +3917,6 @@
children = (
BA3759902ABCCEBA0018D73B /* CameraModel.swift */,
BA3759912ABCCEBA0018D73B /* CameraService.swift */,
BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */,
BA3759892ABCCDE30018D73B /* ImageResizer.swift */,
BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */,
BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */,
@@ -3953,6 +3976,17 @@
path = Mocking;
sourceTree = "<group>";
};
D73BDB122D71212600D69970 /* NostrNetworkManager */ = {
isa = PBXGroup;
children = (
D733F9E02D92C1AA00317B11 /* SubscriptionManager.swift */,
D73BDB172D71310C00D69970 /* UserRelayListErrors.swift */,
D73BDB132D71215F00D69970 /* UserRelayListManager.swift */,
D73BDB0C2D6FF58600D69970 /* NostrNetworkManager.swift */,
);
path = NostrNetworkManager;
sourceTree = "<group>";
};
D74EA08C2D2E26E6002290DD /* ErrorHandling */ = {
isa = PBXGroup;
children = (
@@ -4049,6 +4083,14 @@
path = NIP44;
sourceTree = "<group>";
};
D7DB93082D69478400DA1EE5 /* NIP65 */ = {
isa = PBXGroup;
children = (
D7DB93092D69485A00DA1EE5 /* NIP65.swift */,
);
path = NIP65;
sourceTree = "<group>";
};
E06336A72B7582D600A88E6B /* Assets */ = {
isa = PBXGroup;
children = (
@@ -4438,6 +4480,7 @@
4C3DCC762A9FE9EC0091E592 /* NdbTxn.swift in Sources */,
4CEF958D2A9CE650000F901B /* verifier.c in Sources */,
4C32B9342A9AD01A00DC3548 /* NdbProfile.swift in Sources */,
D73BDB0E2D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */,
4C32B9332A99845B00DC3548 /* Ndb.swift in Sources */,
D7ADD3E22B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift in Sources */,
4C4793082A993E8900489948 /* refmap.c in Sources */,
@@ -4485,6 +4528,7 @@
F757933A29D7AECD007DEAC1 /* MediaPicker.swift in Sources */,
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */,
B57B4C662B312C3700A232C0 /* NostrAuth.swift in Sources */,
D7DB930B2D69486700DA1EE5 /* NIP65.swift in Sources */,
4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */,
4C3EA67728FF7A9800C48A62 /* talstr.c in Sources */,
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
@@ -4571,12 +4615,12 @@
4C32B9582A9AD44700DC3548 /* VeriferOptions.swift in Sources */,
D74AAFC22B153395006CF0F4 /* HeadlessDamusState.swift in Sources */,
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
D73BDB1A2D71311900D69970 /* UserRelayListErrors.swift in Sources */,
4C30AC7629A5770900E2BD5A /* NotificationItemView.swift in Sources */,
4C86F7C42A76C44C00EC0817 /* ZappingNotify.swift in Sources */,
4C363A8428233689006E126D /* Parser.swift in Sources */,
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */,
4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */,
BA3759922ABCCEBA0018D73B /* CameraService+Extensions.swift in Sources */,
D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */,
4C363A9A28283854006E126D /* Reply.swift in Sources */,
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */,
@@ -4608,6 +4652,7 @@
D78DB85B2C20FE5000F0AB12 /* VectorMath.swift in Sources */,
D7CB5D3E2B116DAD00AD4105 /* NotificationsManager.swift in Sources */,
50A16FFF2AA76A0900DFEC1F /* DamusVideoCoordinator.swift in Sources */,
D733F9E32D92C1D900317B11 /* SubscriptionManager.swift in Sources */,
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */,
4C285C8228385570008A31F1 /* CarouselView.swift in Sources */,
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */,
@@ -4716,6 +4761,7 @@
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */,
D73E5F7F2C6AA066007EB227 /* DamusAliases.swift in Sources */,
4C1A9A2A29DDF54400516EAC /* DamusVideoPlayerView.swift in Sources */,
D73BDB152D71216500D69970 /* UserRelayListManager.swift in Sources */,
4CA352A22A76AEC5003BB08B /* LikedNotify.swift in Sources */,
5CC8529F2BD744F60039FFC5 /* HighlightView.swift in Sources */,
BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */,
@@ -4919,6 +4965,7 @@
4C9AA14A2A4587A6003F49FD /* NotificationStatusModel.swift in Sources */,
D7100C5C2B77016700C59298 /* IAPProductStateView.swift in Sources */,
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
D733F9E82D92C76100317B11 /* UnownedNdbNote.swift in Sources */,
D74EA0902D2E271E002290DD /* ErrorView.swift in Sources */,
4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */,
BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */,
@@ -5127,6 +5174,7 @@
82D6FB212CD99F7900C925F4 /* SelectableText.swift in Sources */,
82D6FB222CD99F7900C925F4 /* DamusColors.swift in Sources */,
82D6FB232CD99F7900C925F4 /* ThiccDivider.swift in Sources */,
D733F9E22D92C1D900317B11 /* SubscriptionManager.swift in Sources */,
82D6FB242CD99F7900C925F4 /* IconLabel.swift in Sources */,
82D6FB252CD99F7900C925F4 /* TruncatedText.swift in Sources */,
82D6FB262CD99F7900C925F4 /* SupporterBadge.swift in Sources */,
@@ -5141,6 +5189,7 @@
82D6FB2F2CD99F7900C925F4 /* BlurHashDecode.swift in Sources */,
82D6FB302CD99F7900C925F4 /* PostBox.swift in Sources */,
82D6FB312CD99F7900C925F4 /* KFOptionSetter+.swift in Sources */,
D73BDB162D71216500D69970 /* UserRelayListManager.swift in Sources */,
82D6FB322CD99F7900C925F4 /* FillAndStroke.swift in Sources */,
82D6FB332CD99F7900C925F4 /* Array.swift in Sources */,
82D6FB342CD99F7900C925F4 /* VectorMath.swift in Sources */,
@@ -5205,7 +5254,6 @@
82D6FB6C2CD99F7900C925F4 /* DamusPurpleURL.swift in Sources */,
82D6FB6D2CD99F7900C925F4 /* DamusPurpleEnvironment.swift in Sources */,
82D6FB6E2CD99F7900C925F4 /* PurpleStoreKitManager.swift in Sources */,
82D6FB6F2CD99F7900C925F4 /* CameraService+Extensions.swift in Sources */,
82D6FB702CD99F7900C925F4 /* ImageResizer.swift in Sources */,
82D6FB712CD99F7900C925F4 /* PhotoCaptureProcessor.swift in Sources */,
82D6FB722CD99F7900C925F4 /* VideoCaptureProcessor.swift in Sources */,
@@ -5285,6 +5333,7 @@
82D6FBBA2CD99F7900C925F4 /* NostrRequest.swift in Sources */,
82D6FBBB2CD99F7900C925F4 /* Profiles.swift in Sources */,
82D6FBBC2CD99F7900C925F4 /* NostrKind.swift in Sources */,
D733F9E62D92C76100317B11 /* UnownedNdbNote.swift in Sources */,
82D6FBBD2CD99F7900C925F4 /* NostrLink.swift in Sources */,
82D6FBBE2CD99F7900C925F4 /* WebSocket.swift in Sources */,
82D6FBBF2CD99F7900C925F4 /* ReferencedId.swift in Sources */,
@@ -5318,6 +5367,7 @@
82D6FBE02CD99F7900C925F4 /* ReactionsSettingsView.swift in Sources */,
82D6FBE12CD99F7900C925F4 /* NotificationSettingsView.swift in Sources */,
82D6FBE22CD99F7900C925F4 /* AppearanceSettingsView.swift in Sources */,
D7DB930A2D69486700DA1EE5 /* NIP65.swift in Sources */,
82D6FBE32CD99F7900C925F4 /* KeySettingsView.swift in Sources */,
82D6FBE42CD99F7900C925F4 /* ZapSettingsView.swift in Sources */,
82D6FBE52CD99F7900C925F4 /* TranslationSettingsView.swift in Sources */,
@@ -5372,6 +5422,7 @@
82D6FC132CD99F7900C925F4 /* FriendIcon.swift in Sources */,
82D6FC142CD99F7900C925F4 /* CondensedProfilePicturesView.swift in Sources */,
82D6FC152CD99F7900C925F4 /* ProfileEditButton.swift in Sources */,
D73BDB102D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */,
82D6FC162CD99F7900C925F4 /* RelayPaidDetail.swift in Sources */,
82D6FC172CD99F7900C925F4 /* RelayAuthenticationDetail.swift in Sources */,
82D6FC182CD99F7900C925F4 /* RelaySoftwareDetail.swift in Sources */,
@@ -5425,6 +5476,7 @@
82D6FC472CD99F7900C925F4 /* RepostAction.swift in Sources */,
82D6FC482CD99F7900C925F4 /* ShareActionButton.swift in Sources */,
82D6FC492CD99F7900C925F4 /* BigButton.swift in Sources */,
D73BDB182D71311900D69970 /* UserRelayListErrors.swift in Sources */,
82D6FC4A2CD99F7900C925F4 /* AddRelayView.swift in Sources */,
82D6FC4B2CD99F7900C925F4 /* BlocksView.swift in Sources */,
D74EA0912D2E3464002290DD /* URLHandler.swift in Sources */,
@@ -5596,7 +5648,7 @@
D73E5E882C6A97F4007EB227 /* StoreObserver.swift in Sources */,
D73E5E892C6A97F4007EB227 /* DamusPurpleURL.swift in Sources */,
D73E5E8A2C6A97F4007EB227 /* PurpleStoreKitManager.swift in Sources */,
D73E5E8D2C6A97F4007EB227 /* CameraService+Extensions.swift in Sources */,
D733F9E72D92C76100317B11 /* UnownedNdbNote.swift in Sources */,
D73E5E8E2C6A97F4007EB227 /* ImageResizer.swift in Sources */,
D78F080E2D7F78EF00FC6C75 /* Request.swift in Sources */,
D73E5E8F2C6A97F4007EB227 /* PhotoCaptureProcessor.swift in Sources */,
@@ -5723,6 +5775,7 @@
D73E5F012C6A97F4007EB227 /* ZapTypePicker.swift in Sources */,
D73E5F022C6A97F4007EB227 /* ZapUserView.swift in Sources */,
D73E5F032C6A97F4007EB227 /* ProfileZapLinkView.swift in Sources */,
D7DB930C2D69486700DA1EE5 /* NIP65.swift in Sources */,
D73E5F042C6A97F4007EB227 /* AboutView.swift in Sources */,
D73E5F052C6A97F4007EB227 /* ProfileName.swift in Sources */,
D73E5F062C6A97F4007EB227 /* ProfilePictureSelector.swift in Sources */,
@@ -5763,6 +5816,7 @@
D73E5F272C6A97F4007EB227 /* TimeDot.swift in Sources */,
D73E5F282C6A97F4007EB227 /* EventTop.swift in Sources */,
D73E5F292C6A97F4007EB227 /* ReplyDescription.swift in Sources */,
D73BDB0D2D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */,
D73E5F2A2C6A97F4007EB227 /* RelativeTime.swift in Sources */,
D73E5F732C6A9885007EB227 /* TestData.swift in Sources */,
D78F08192D7F7F7500FC6C75 /* NIP04.swift in Sources */,
@@ -5801,6 +5855,7 @@
D73E5F472C6A97F5007EB227 /* BookmarksView.swift in Sources */,
D73E5F482C6A97F5007EB227 /* CarouselView.swift in Sources */,
D73E5F492C6A97F5007EB227 /* ConfigView.swift in Sources */,
D733F9E12D92C1D900317B11 /* SubscriptionManager.swift in Sources */,
D73E5F4A2C6A97F5007EB227 /* CreateAccountView.swift in Sources */,
D73E5F7A2C6A9C55007EB227 /* NotificationFormatter.swift in Sources */,
D73E5F4B2C6A97F5007EB227 /* DirectMessagesView.swift in Sources */,
@@ -5836,6 +5891,7 @@
D73E5F6A2C6A97F5007EB227 /* ReportView.swift in Sources */,
D73E5F6C2C6A97F5007EB227 /* RepostsView.swift in Sources */,
D734B1462CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */,
D73BDB142D71216500D69970 /* UserRelayListManager.swift in Sources */,
D73E5F6D2C6A97F5007EB227 /* Launch.storyboard in Sources */,
D73E5F6F2C6A97F5007EB227 /* RelayFilterView.swift in Sources */,
D703D78A2C670C8A00A400EA /* LibreTranslateServer.swift in Sources */,
@@ -5887,6 +5943,7 @@
D703D7A52C670E3E00A400EA /* mdb.c in Sources */,
D703D76B2C670B3100A400EA /* Referenced.swift in Sources */,
D703D7952C670DE600A400EA /* hash_u5.c in Sources */,
D73BDB192D71311900D69970 /* UserRelayListErrors.swift in Sources */,
D703D7582C670A6000A400EA /* Id.swift in Sources */,
5C05675A2C8FBDE70073F23A /* NDBSearchView.swift in Sources */,
D703D76E2C670B4900A400EA /* NdbTagsIterator.swift in Sources */,
@@ -5981,6 +6038,7 @@
buildActionMask = 2147483647;
files = (
4C8FA7242BED58A900798A6A /* ThreadReply.swift in Sources */,
D733F9E52D92C76100317B11 /* UnownedNdbNote.swift in Sources */,
D798D21F2B0858D600234419 /* MigratedTypes.swift in Sources */,
D7CE1B472B0BE719002EDAD4 /* NativeObject.swift in Sources */,
D71AD9002CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */,
@@ -6854,7 +6912,7 @@
repositoryURL = "https://github.com/tyiu/EmojiPicker.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.1.1;
minimumVersion = 0.2.0;
};
};
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
@@ -6862,7 +6920,7 @@
repositoryURL = "https://github.com/onevcat/Kingfisher";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 7.0.0;
minimumVersion = 8.3.1;
};
};
4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = {
@@ -1,5 +1,5 @@
{
"originHash" : "085cf0f645323bf77edb52886489bf77b309a0a2d2b78a54beaf8520b540d596",
"originHash" : "06318d35ee2e6bd681b95591e67da33a9461b48a3c652e58bd9d1a6f0d82bdac",
"pins" : [
{
"identity" : "codescanner",
@@ -22,8 +22,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/tyiu/EmojiKit",
"state" : {
"revision" : "05805f72d63a6d6a2d7dc7fe14abd37c1317b11a",
"version" : "0.1.2"
"revision" : "47a4b1402de26be0299dcb4d667c1faaf21a7874",
"version" : "0.2.0"
}
},
{
@@ -31,8 +31,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/tyiu/EmojiPicker.git",
"state" : {
"revision" : "0c28b4a1a6b8840cf2580bda59517f6d0a733dc8",
"version" : "0.1.1"
"revision" : "3f48903721eae223238ff0af17c22d6373d33813",
"version" : "0.2.0"
}
},
{
@@ -49,8 +49,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/onevcat/Kingfisher",
"state" : {
"revision" : "415b1d97fb38bda1e5a6b2dde63354720832110b",
"version" : "7.6.1"
"revision" : "4c6b067f96953ee19526e49e4189403a2be21fb3",
"version" : "8.3.1"
}
},
{

Before

Width:  |  Height:  |  Size: 216 B

After

Width:  |  Height:  |  Size: 216 B

+4 -4
View File
@@ -84,7 +84,7 @@ struct NoteZapButton: View {
print("cancel_zap: we already have a real zap, can't cancel")
break
case .pending(let pzap):
guard let res = cancel_zap(zap: pzap, box: damus_state.postbox, zapcache: damus_state.zaps, evcache: damus_state.events) else {
guard let res = cancel_zap(zap: pzap, box: damus_state.nostrNetwork.postbox, zapcache: damus_state.zaps, evcache: damus_state.events) else {
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
return
@@ -179,7 +179,7 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
}
// Only take the first 10 because reasons
let relays = Array(damus_state.pool.our_descriptors.prefix(10))
let relays = Array(damus_state.nostrNetwork.pool.our_descriptors.prefix(10))
let content = comment ?? ""
guard let mzapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type) else {
@@ -232,7 +232,7 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
flusher = .once({ pe in
// send donation zap when the pending zap is flushed, this allows user to cancel and not send a donation
Task { @MainActor in
await WalletConnect.send_donation_zap(pool: damus_state.pool, postbox: damus_state.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat)
await WalletConnect.send_donation_zap(pool: damus_state.nostrNetwork.pool, postbox: damus_state.nostrNetwork.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat)
}
})
}
@@ -240,7 +240,7 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
// we don't have a delay on one-tap nozaps (since this will be from customize zap view)
let delay = damus_state.settings.nozaps ? nil : 5.0
let nwc_req = WalletConnect.pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, delay: delay, on_flush: flusher)
let nwc_req = WalletConnect.pay(url: nwc_state.url, pool: damus_state.nostrNetwork.pool, post: damus_state.nostrNetwork.postbox, invoice: inv, delay: delay, on_flush: flusher)
guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else {
print("nwc: failed to send nwc request for zapreq \(reqid.reqid)")
+37 -14
View File
@@ -94,12 +94,12 @@ struct SelectableText: View {
case show_mute_word_view(highlighted_text: String)
func should_show_highlight_post_view() -> Bool {
guard case .show_highlight_post_view(let highlighted_text) = self else { return false }
guard case .show_highlight_post_view = self else { return false }
return true
}
func should_show_mute_word_view() -> Bool {
guard case .show_mute_word_view(let highlighted_text) = self else { return false }
guard case .show_mute_word_view = self else { return false }
return true
}
@@ -119,16 +119,23 @@ struct SelectableText: View {
fileprivate class TextView: UITextView {
var postHighlight: (String) -> Void
var muteWord: (String) -> Void
private let enableHighlighting: Bool
init(frame: CGRect, textContainer: NSTextContainer?, postHighlight: @escaping (String) -> Void, muteWord: @escaping (String) -> Void) {
init(frame: CGRect, textContainer: NSTextContainer?, postHighlight: @escaping (String) -> Void, muteWord: @escaping (String) -> Void, enableHighlighting: Bool) {
self.postHighlight = postHighlight
self.muteWord = muteWord
self.enableHighlighting = enableHighlighting
super.init(frame: frame, textContainer: textContainer)
if enableHighlighting {
self.delegate = self
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
fatalError("init(coder:) has not been implemented")
}
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(highlightText(_:)) {
@@ -142,23 +149,44 @@ fileprivate class TextView: UITextView {
return super.canPerformAction(action, withSender: sender)
}
func getSelectedText() -> String? {
private func getSelectedText() -> String? {
guard let selectedRange = self.selectedTextRange else { return nil }
return self.text(in: selectedRange)
}
@objc public func highlightText(_ sender: Any?) {
@objc private func highlightText(_ sender: Any?) {
guard let selectedText = self.getSelectedText() else { return }
self.postHighlight(selectedText)
}
@objc public func muteText(_ sender: Any?) {
@objc private func muteText(_ sender: Any?) {
guard let selectedText = self.getSelectedText() else { return }
self.muteWord(selectedText)
}
}
extension TextView: UITextViewDelegate {
func textView(_ textView: UITextView, editMenuForTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? {
guard enableHighlighting,
let selectedTextRange = self.selectedTextRange,
let selectedText = self.text(in: selectedTextRange),
!selectedText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
return nil
}
let highlightAction = UIAction(title: NSLocalizedString("Highlight", comment: "Context menu action to highlight the selected text as context to draft a new note."), image: UIImage(systemName: "highlighter")) { [weak self] _ in
self?.postHighlight(selectedText)
}
let muteAction = UIAction(title: NSLocalizedString("Mute", comment: "Context menu action to mute the selected word."), image: UIImage(systemName: "speaker.slash")) { [weak self] _ in
self?.muteWord(selectedText)
}
return UIMenu(children: suggestedActions + [highlightAction, muteAction])
}
}
fileprivate struct TextViewRepresentable: UIViewRepresentable {
let attributedString: AttributedString
@@ -172,7 +200,7 @@ fileprivate struct TextViewRepresentable: UIViewRepresentable {
@Binding var height: CGFloat
func makeUIView(context: UIViewRepresentableContext<Self>) -> TextView {
let view = TextView(frame: .zero, textContainer: nil, postHighlight: postHighlight, muteWord: muteWord)
let view = TextView(frame: .zero, textContainer: nil, postHighlight: postHighlight, muteWord: muteWord, enableHighlighting: enableHighlighting)
view.isEditable = false
view.dataDetectorTypes = .all
view.isSelectable = true
@@ -183,11 +211,6 @@ fileprivate struct TextViewRepresentable: UIViewRepresentable {
view.textContainerInset.right = 1.0
view.textAlignment = textAlignment
let menuController = UIMenuController.shared
let highlightItem = UIMenuItem(title: "Highlight", action: #selector(view.highlightText(_:)))
let muteItem = UIMenuItem(title: "Mute", action: #selector(view.muteText(_:)))
menuController.menuItems = self.enableHighlighting ? [highlightItem, muteItem] : []
return view
}
@@ -213,6 +213,6 @@ struct UserStatusSheet: View {
struct UserStatusSheet_Previews: PreviewProvider {
static var previews: some View {
UserStatusSheet(damus_state: test_damus_state, postbox: test_damus_state.postbox, keypair: test_keypair, status: .init())
UserStatusSheet(damus_state: test_damus_state, postbox: test_damus_state.nostrNetwork.postbox, keypair: test_keypair, status: .init())
}
}
+28 -47
View File
@@ -199,7 +199,7 @@ struct ContentView: View {
func MaybeReportView(target: ReportTarget) -> some View {
Group {
if let keypair = damus_state.keypair.to_full() {
ReportView(postbox: damus_state.postbox, target: target, keypair: keypair)
ReportView(postbox: damus_state.nostrNetwork.postbox, target: target, keypair: keypair)
} else {
EmptyView()
}
@@ -317,7 +317,7 @@ struct ContentView: View {
case .post(let action):
PostView(action: action, damus_state: damus_state!)
case .user_status:
UserStatusSheet(damus_state: damus_state!, postbox: damus_state!.postbox, keypair: damus_state!.keypair, status: damus_state!.profiles.profile_data(damus_state!.pubkey).status)
UserStatusSheet(damus_state: damus_state!, postbox: damus_state!.nostrNetwork.postbox, keypair: damus_state!.keypair, status: damus_state!.profiles.profile_data(damus_state!.pubkey).status)
.presentationDragIndicator(.visible)
case .event:
EventDetailView()
@@ -356,7 +356,7 @@ struct ContentView: View {
self.hide_bar = !show
}
.onReceive(timer) { n in
self.damus_state?.postbox.try_flushing_events()
self.damus_state?.nostrNetwork.postbox.try_flushing_events()
self.damus_state!.profiles.profile_data(self.damus_state!.pubkey).status.try_expire()
}
.onReceive(handle_notify(.report)) { target in
@@ -367,10 +367,6 @@ struct ContentView: View {
self.confirm_mute = true
}
.onReceive(handle_notify(.attached_wallet)) { nwc in
// Ensure to add NWC relay to the pool and connect it.
try? damus_state.pool.add_relay(.nwc(url: nwc.relay))
damus_state.pool.connect(to: [nwc.relay])
// update the lightning address on our profile when we attach a
// wallet with an associated
guard let ds = self.damus_state,
@@ -391,12 +387,12 @@ struct ContentView: View {
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.postbox.send(ev)
ds.nostrNetwork.postbox.send(ev)
}
.onReceive(handle_notify(.broadcast)) { ev in
guard let ds = self.damus_state else { return }
ds.postbox.send(ev)
ds.nostrNetwork.postbox.send(ev)
}
.onReceive(handle_notify(.unfollow)) { target in
guard let state = self.damus_state else { return }
@@ -418,7 +414,7 @@ struct ContentView: View {
return
}
if !handle_post_notification(keypair: keypair, postbox: state.postbox, events: state.events, post: post) {
if !handle_post_notification(keypair: keypair, postbox: state.nostrNetwork.postbox, events: state.events, post: post) {
self.active_sheet = nil
}
}
@@ -462,7 +458,7 @@ struct ContentView: View {
}
}
.onReceive(handle_notify(.disconnect_relays)) { () in
damus_state.pool.disconnect()
damus_state.nostrNetwork.pool.disconnect()
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { obj in
print("txn: 📙 DAMUS ACTIVE NOTIFY")
@@ -508,7 +504,7 @@ struct ContentView: View {
break
case .active:
print("txn: 📙 DAMUS ACTIVE")
damus_state.pool.ping()
damus_state.nostrNetwork.pool.ping()
@unknown default:
break
}
@@ -527,7 +523,7 @@ struct ContentView: View {
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.postbox.send(profile_ev)
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: {
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to muted a user was successful.")) {
@@ -559,7 +555,7 @@ struct ContentView: View {
}
ds.mutelist_manager.set_mutelist(mutelist)
ds.postbox.send(mutelist)
ds.nostrNetwork.postbox.send(mutelist)
confirm_overwrite_mutelist = false
confirm_mute = false
@@ -591,7 +587,7 @@ struct ContentView: View {
}
ds.mutelist_manager.set_mutelist(ev)
ds.postbox.send(ev)
ds.nostrNetwork.postbox.send(ev)
}
}
}, message: {
@@ -632,7 +628,7 @@ struct ContentView: View {
func handleNotification(notification: LossyLocalNotification) {
Log.info("ContentView is handling a notification", for: .push_notifications)
guard let damus_state else {
guard damus_state != nil else {
// This should never happen because `listenAndHandleLocalNotifications` is called after damus state is initialized in `onAppear`
assertionFailure("DamusState not loaded when ContentView (new handler) was handling a notification")
Log.error("DamusState not loaded when ContentView (new handler) was handling a notification", for: .push_notifications)
@@ -660,28 +656,14 @@ struct ContentView: View {
guard let ndb = mndb else { return }
let pool = RelayPool(ndb: ndb, keypair: keypair)
let model_cache = RelayModelCache()
let relay_filters = RelayFilters(our_pubkey: pubkey)
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
let settings = UserSettingsStore.globally_load_for(pubkey: pubkey)
let new_relay_filters = load_relay_filters(pubkey) == nil
for relay in bootstrap_relays {
let descriptor = RelayDescriptor(url: relay, info: .rw)
add_new_relay(model_cache: model_cache, relay_filters: relay_filters, pool: pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: settings.developer_mode)
}
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
if let nwc_str = settings.nostr_wallet_connect,
let nwc = WalletConnectURL(str: nwc_str) {
try? pool.add_relay(.nwc(url: nwc.relay))
}
self.damus_state = DamusState(pool: pool,
keypair: keypair,
self.damus_state = DamusState(keypair: keypair,
likes: EventCounter(our_pubkey: pubkey),
boosts: EventCounter(our_pubkey: pubkey),
contacts: Contacts(our_pubkey: pubkey),
@@ -697,8 +679,6 @@ struct ContentView: View {
drafts: Drafts(),
events: EventCache(ndb: ndb),
bookmarks: BookmarksManager(pubkey: pubkey),
postbox: PostBox(pool: pool),
bootstrap_relays: bootstrap_relays,
replies: ReplyCounter(our_pubkey: pubkey),
wallet: WalletModel(settings: settings),
nav: self.navigationCoordinator,
@@ -722,7 +702,8 @@ struct ContentView: View {
// Purple API is an experimental feature. If not enabled, do not connect `StoreObserver` with Purple API to avoid leaking receipts
}
pool.connect()
damus_state.nostrNetwork.pool.register_handler(sub_id: sub_id, handler: home.handle_event)
damus_state.nostrNetwork.connect()
}
func music_changed(_ state: MusicState) {
@@ -745,7 +726,7 @@ struct ContentView: View {
pdata.status.music = music
guard let ev = music.to_note(keypair: kp) else { return }
damus_state.postbox.send(ev)
damus_state.nostrNetwork.postbox.send(ev)
}
}
@@ -994,7 +975,7 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
var has_event = false
guard let filter else { return }
state.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in
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
}
@@ -1008,7 +989,7 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
break
case .event(_, let ev):
has_event = true
state.pool.unsubscribe(sub_id: subid)
state.nostrNetwork.pool.unsubscribe(sub_id: subid)
switch query {
case .profile:
@@ -1021,11 +1002,11 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
case .eose:
if !has_event {
attempts += 1
if attempts >= state.pool.our_descriptors.count {
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.pool.unsubscribe(sub_id: subid, to: [relay_id]) // We are only finding an event once, so close subscription on eose
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:
@@ -1044,15 +1025,15 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
/// - naddr: the `naddr` address
/// - callback: A function to handle the found event
func naddrLookup(damus_state: DamusState, naddr: NAddr, callback: @escaping (NostrEvent?) -> ()) {
var nostrKinds: [NostrKind]? = NostrKind(rawValue: naddr.kind).map { [$0] }
let nostrKinds: [NostrKind]? = NostrKind(rawValue: naddr.kind).map { [$0] }
let filter = NostrFilter(kinds: nostrKinds, authors: [naddr.author])
let subid = UUID().description
damus_state.pool.subscribe_to(sub_id: subid, filters: [filter], to: nil) { relay_id, res in
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.pool.unsubscribe(sub_id: subid, to: [relay_id])
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id])
return
}
@@ -1060,14 +1041,14 @@ func naddrLookup(damus_state: DamusState, naddr: NAddr, callback: @escaping (Nos
for tag in ev.tags {
if(tag.count >= 2 && tag[0].string() == "d"){
if (tag[1].string() == naddr.identifier){
damus_state.pool.unsubscribe(sub_id: subid, to: [relay_id])
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id])
callback(ev)
return
}
}
}
}
damus_state.pool.unsubscribe(sub_id: subid, to: [relay_id])
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id])
}
}
@@ -1115,7 +1096,7 @@ func handle_unfollow(state: DamusState, unfollow: FollowRef) -> Bool {
let old_contacts = state.contacts.event
guard let ev = unfollow_reference(postbox: state.postbox, our_contacts: old_contacts, keypair: keypair, unfollow: unfollow)
guard let ev = unfollow_reference(postbox: state.nostrNetwork.postbox, our_contacts: old_contacts, keypair: keypair, unfollow: unfollow)
else {
return false
}
@@ -1141,7 +1122,7 @@ func handle_follow(state: DamusState, follow: FollowRef) -> Bool {
return false
}
guard let ev = follow_reference(box: state.postbox, our_contacts: state.contacts.event, keypair: keypair, follow: follow)
guard let ev = follow_reference(box: state.nostrNetwork.postbox, our_contacts: state.contacts.event, keypair: keypair, follow: follow)
else {
return false
}
@@ -1216,7 +1197,7 @@ extension LossyLocalNotification {
case .nprofile(let nProfile):
// TODO: Improve this by implementing a profile route that handles nprofiles with their relay hints.
return .route(.ProfileByKey(pubkey: nProfile.author))
case .nrelay(let string):
case .nrelay:
// We do not need to implement `nrelay` support, it has been deprecated.
// See https://github.com/nostr-protocol/nips/blob/6e7a618e7f873bb91e743caacc3b09edab7796a0/BREAKING.md?plain=1#L21
return .sheet(.error(ErrorView.UserPresentableError(
@@ -1,32 +0,0 @@
//
// CameraService+Extensions.swift
// damus
//
// Created by Suhail Saqan on 8/5/23.
//
import Foundation
import UIKit
import AVFoundation
extension AVCaptureVideoOrientation {
init?(deviceOrientation: UIDeviceOrientation) {
switch deviceOrientation {
case .portrait: self = .portrait
case .portraitUpsideDown: self = .portraitUpsideDown
case .landscapeLeft: self = .landscapeRight
case .landscapeRight: self = .landscapeLeft
default: return nil
}
}
init?(interfaceOrientation: UIInterfaceOrientation) {
switch interfaceOrientation {
case .portrait: self = .portrait
case .portraitUpsideDown: self = .portraitUpsideDown
case .landscapeLeft: self = .landscapeLeft
case .landscapeRight: self = .landscapeRight
default: return nil
}
}
}
+1 -54
View File
@@ -63,44 +63,10 @@ func follow_user_event(our_contacts: NostrEvent?, keypair: FullKeypair, follow:
}
func decode_json_relays(_ content: String) -> [RelayURL: RelayInfo]? {
func decode_json_relays(_ content: String) -> [RelayURL: LegacyKind3RelayRWConfiguration]? {
return decode_json(content)
}
func remove_relay(ev: NostrEvent, current_relays: [RelayDescriptor], keypair: FullKeypair, relay: RelayURL) -> NostrEvent?{
var relays = ensure_relay_info(relays: current_relays, content: ev.content)
relays.removeValue(forKey: relay)
guard let content = encode_json(relays) else {
return nil
}
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings())
}
func add_relay(ev: NostrEvent, keypair: FullKeypair, current_relays: [RelayDescriptor], relay: RelayURL, info: RelayInfo) -> NostrEvent? {
var relays = ensure_relay_info(relays: current_relays, content: ev.content)
// If kind:3 content is empty, or if the relay doesn't exist in the list,
// we want to create a kind:3 event with the new relay
guard ev.content.isEmpty || relays.index(forKey: relay) == nil else {
return nil
}
relays[relay] = info
guard let content = encode_json(relays) else {
return nil
}
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings())
}
func ensure_relay_info(relays: [RelayDescriptor], content: String) -> [RelayURL: RelayInfo] {
return decode_json_relays(content) ?? make_contact_relays(relays)
}
func is_already_following(contacts: NostrEvent, follow: FollowRef) -> Bool {
return contacts.references.contains { ref in
switch (ref, follow) {
@@ -128,22 +94,3 @@ func follow_with_existing_contacts(keypair: FullKeypair, our_contacts: NostrEven
return NostrEvent(content: our_contacts.content, keypair: keypair.to_keypair(), kind: kind, tags: tags)
}
func make_contact_relays(_ relays: [RelayDescriptor]) -> [RelayURL: RelayInfo] {
return relays.reduce(into: [:]) { acc, relay in
acc[relay.url] = relay.info
}
}
func make_relay_metadata(relays: [RelayDescriptor], keypair: FullKeypair) -> NostrEvent? {
let tags = relays.compactMap { r -> [String]? in
var tag = ["r", r.url.absoluteString]
if (r.info.read ?? true) != (r.info.write ?? true) {
tag += r.info.read == true ? ["read"] : ["write"]
}
if ((r.info.read ?? true) || (r.info.write ?? true)) && r.variant == .regular {
return tag;
}
return nil
}
return NostrEvent(content: "", keypair: keypair.to_keypair(), kind: 10_002, tags: tags)
}
+7
View File
@@ -40,6 +40,12 @@ func get_repost_of_muted_user_filter(damus_state: DamusState) -> ((_ ev: NostrEv
}
}
func timestamp_filter(ev: NostrEvent) -> Bool {
// Allow notes that are created no more than 3 seconds in the future
// to account for natural clock skew between sender and receiver.
ev.age >= -3
}
/// Generic filter with various tweakable settings
struct ContentFilters {
var filters: [(NostrEvent) -> Bool]
@@ -66,6 +72,7 @@ extension ContentFilters {
filters.append(nsfw_tag_filter)
}
filters.append(get_repost_of_muted_user_filter(damus_state: damus_state))
filters.append(timestamp_filter)
return filters
}
}
+4
View File
@@ -27,6 +27,10 @@ class CreateAccountModel: ObservableObject {
return Keypair(pubkey: self.pubkey, privkey: self.privkey)
}
var full_keypair: FullKeypair {
return FullKeypair(pubkey: self.pubkey, privkey: self.privkey)
}
init(display_name: String = "", name: String = "", about: String = "") {
let keypair = generate_new_keypair()
self.pubkey = keypair.pubkey
+32 -27
View File
@@ -10,7 +10,6 @@ import LinkPresentation
import EmojiPicker
class DamusState: HeadlessDamusState {
let pool: RelayPool
let keypair: Keypair
let likes: EventCounter
let boosts: EventCounter
@@ -28,8 +27,6 @@ class DamusState: HeadlessDamusState {
let drafts: Drafts
let events: EventCache
let bookmarks: BookmarksManager
let postbox: PostBox
let bootstrap_relays: [RelayURL]
let replies: ReplyCounter
let wallet: WalletModel
let nav: NavigationCoordinator
@@ -39,9 +36,9 @@ class DamusState: HeadlessDamusState {
var purple: DamusPurple
var push_notification_client: PushNotificationClient
let emoji_provider: EmojiProvider
private(set) var nostrNetwork: NostrNetworkManager
init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [RelayURL], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider) {
self.pool = pool
init(keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider) {
self.keypair = keypair
self.likes = likes
self.boosts = boosts
@@ -58,8 +55,6 @@ class DamusState: HeadlessDamusState {
self.drafts = drafts
self.events = events
self.bookmarks = bookmarks
self.postbox = postbox
self.bootstrap_relays = bootstrap_relays
self.replies = replies
self.wallet = wallet
self.nav = nav
@@ -73,6 +68,9 @@ class DamusState: HeadlessDamusState {
self.quote_reposts = quote_reposts
self.push_notification_client = PushNotificationClient(keypair: keypair, settings: settings)
self.emoji_provider = emoji_provider
let networkManagerDelegate = NostrNetworkManagerDelegate(settings: settings, contacts: contacts, ndb: ndb, keypair: keypair, relayModelCache: relay_model_cache, relayFilters: relay_filters)
self.nostrNetwork = NostrNetworkManager(delegate: networkManagerDelegate)
}
@MainActor
@@ -98,27 +96,13 @@ class DamusState: HeadlessDamusState {
guard let ndb = mndb else { return nil }
let pubkey = keypair.pubkey
let pool = RelayPool(ndb: ndb, keypair: keypair)
let model_cache = RelayModelCache()
let relay_filters = RelayFilters(our_pubkey: pubkey)
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
let settings = UserSettingsStore.globally_load_for(pubkey: pubkey)
let new_relay_filters = load_relay_filters(pubkey) == nil
for relay in bootstrap_relays {
let descriptor = RelayDescriptor(url: relay, info: .rw)
add_new_relay(model_cache: model_cache, relay_filters: relay_filters, pool: pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: settings.developer_mode)
}
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
if let nwc_str = settings.nostr_wallet_connect,
let nwc = WalletConnectURL(str: nwc_str) {
try? pool.add_relay(.nwc(url: nwc.relay))
}
self.init(
pool: pool,
keypair: keypair,
likes: EventCounter(our_pubkey: pubkey),
boosts: EventCounter(our_pubkey: pubkey),
@@ -135,8 +119,6 @@ class DamusState: HeadlessDamusState {
drafts: Drafts(),
events: EventCache(ndb: ndb),
bookmarks: BookmarksManager(pubkey: pubkey),
postbox: PostBox(pool: pool),
bootstrap_relays: bootstrap_relays,
replies: ReplyCounter(our_pubkey: pubkey),
wallet: WalletModel(settings: settings),
nav: navigationCoordinator,
@@ -179,7 +161,7 @@ class DamusState: HeadlessDamusState {
try await self.push_notification_client.revoke_token()
}
wallet.disconnect()
pool.close()
nostrNetwork.pool.close()
ndb.close()
}
@@ -189,7 +171,6 @@ class DamusState: HeadlessDamusState {
let kp = Keypair(pubkey: empty_pub, privkey: nil)
return DamusState.init(
pool: RelayPool(ndb: .empty),
keypair: Keypair(pubkey: empty_pub, privkey: empty_sec),
likes: EventCounter(our_pubkey: empty_pub),
boosts: EventCounter(our_pubkey: empty_pub),
@@ -206,8 +187,6 @@ class DamusState: HeadlessDamusState {
drafts: Drafts(),
events: EventCache(ndb: .empty),
bookmarks: BookmarksManager(pubkey: empty_pub),
postbox: PostBox(pool: RelayPool(ndb: .empty)),
bootstrap_relays: [],
replies: ReplyCounter(our_pubkey: empty_pub),
wallet: WalletModel(settings: UserSettingsStore()),
nav: NavigationCoordinator(),
@@ -219,3 +198,29 @@ class DamusState: HeadlessDamusState {
)
}
}
fileprivate extension DamusState {
struct NostrNetworkManagerDelegate: NostrNetworkManager.Delegate {
let settings: UserSettingsStore
let contacts: Contacts
var ndb: Ndb
var keypair: Keypair
var latestRelayListEventIdHex: String? {
get { self.settings.latestRelayListEventIdHex }
set { self.settings.latestRelayListEventIdHex = newValue }
}
var latestContactListEvent: NostrEvent? { self.contacts.event }
var bootstrapRelays: [RelayURL] { get_default_bootstrap_relays() }
var developerMode: Bool { self.settings.developer_mode }
var relayModelCache: RelayModelCache
var relayFilters: RelayFilters
var nwcWallet: WalletConnectURL? {
guard let nwcString = self.settings.nostr_wallet_connect else { return nil }
return WalletConnectURL(str: nwcString)
}
}
}
+1 -1
View File
@@ -251,7 +251,7 @@ class Drafts: ObservableObject {
// 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
// - 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.pool.send_raw_to_local_ndb(.typical(.event(draft_event)))
damus_state.nostrNetwork.pool.send_raw_to_local_ndb(.typical(.event(draft_event)))
}
damus_state.settings.draft_event_ids = draft_events.map({ $0.id.hex() })
+2 -2
View File
@@ -68,13 +68,13 @@ class EventsModel: ObservableObject {
}
func subscribe() {
state.pool.subscribe(sub_id: sub_id,
state.nostrNetwork.pool.subscribe(sub_id: sub_id,
filters: [get_filter()],
handler: handle_nostr_event)
}
func unsubscribe() {
state.pool.unsubscribe(sub_id: sub_id)
state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
}
private func handle_event(relay_id: RelayURL, ev: NostrEvent) {
+4 -4
View File
@@ -37,11 +37,11 @@ class FollowersModel: ObservableObject {
let filter = get_filter()
let filters = [filter]
//print_filters(relay_id: "following", filters: [filters])
self.damus_state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
self.damus_state.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
}
func unsubscribe() {
self.damus_state.pool.unsubscribe(sub_id: sub_id)
self.damus_state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
}
func handle_contact_event(_ ev: NostrEvent) {
@@ -61,7 +61,7 @@ class FollowersModel: ObservableObject {
let filter = NostrFilter(kinds: [.metadata],
authors: authors)
damus_state.pool.subscribe_to(sub_id: profiles_id, filters: [filter], to: [relay_id], handler: handle_event)
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) {
@@ -86,7 +86,7 @@ class FollowersModel: ObservableObject {
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.pool.unsubscribe(sub_id: profiles_id, to: [relay_id])
damus_state.nostrNetwork.pool.unsubscribe(sub_id: profiles_id, to: [relay_id])
}
case .ok:
+2 -2
View File
@@ -42,7 +42,7 @@ class FollowingModel {
}
let filters = [filter]
//print_filters(relay_id: "following", filters: [filters])
self.damus_state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
self.damus_state.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
}
func unsubscribe() {
@@ -50,7 +50,7 @@ class FollowingModel {
return
}
print("unsubscribing from following \(sub_id)")
self.damus_state.pool.unsubscribe(sub_id: sub_id)
self.damus_state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
}
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
+7 -77
View File
@@ -95,13 +95,13 @@ class HomeModel: ContactsDelegate {
}
var pool: RelayPool {
return damus_state.pool
self.damus_state.nostrNetwork.pool
}
var dms: DirectMessagesModel {
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()
@@ -225,6 +225,8 @@ class HomeModel: ContactsDelegate {
// TODO: Implement draft syncing with relays. We intentionally do not support that as of writing. See `DraftsModel.swift` for other details
// try? damus_state.drafts.load(wrapped_draft_note: ev, with: damus_state)
break
case .relay_list:
break // This will be handled by `UserRelayListManager`
}
}
@@ -266,7 +268,7 @@ class HomeModel: ContactsDelegate {
// since command results are not returned for ephemeral events,
// remove the request from the postbox which is likely failing over and over
if damus_state.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) {
print("nwc: got response, removed \(resp.req_id) from the postbox [\(relay)]")
} else {
print("nwc: \(resp.req_id) not found in the postbox, nothing to remove [\(relay)]")
@@ -478,7 +480,7 @@ class HomeModel: ContactsDelegate {
break
}
update_signal_from_pool(signal: self.signal, pool: damus_state.pool)
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):
@@ -948,7 +950,6 @@ func load_our_stuff(state: DamusState, ev: NostrEvent) {
state.contacts.event = ev
load_our_contacts(state: state, m_old_ev: m_old_ev, ev: ev)
load_our_relays(state: state, m_old_ev: m_old_ev, ev: ev)
}
func process_contact_event(state: DamusState, ev: NostrEvent) {
@@ -956,78 +957,6 @@ func process_contact_event(state: DamusState, ev: NostrEvent) {
add_contact_if_friend(contacts: state.contacts, ev: ev)
}
func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
let bootstrap_dict: [RelayURL: RelayInfo] = [:]
let old_decoded = m_old_ev.flatMap { decode_json_relays($0.content) } ?? state.bootstrap_relays.reduce(into: bootstrap_dict) { (d, r) in
d[r] = .rw
}
guard let decoded: [RelayURL: RelayInfo] = decode_json_relays(ev.content) else {
return
}
var changed = false
var new = Set<RelayURL>()
for key in decoded.keys {
new.insert(key)
}
var old = Set<RelayURL>()
for key in old_decoded.keys {
old.insert(key)
}
let diff = old.symmetricDifference(new)
let new_relay_filters = load_relay_filters(state.pubkey) == nil
for d in diff {
changed = true
if new.contains(d) {
let descriptor = RelayDescriptor(url: d, info: decoded[d] ?? .rw)
add_new_relay(model_cache: state.relay_model_cache, relay_filters: state.relay_filters, pool: state.pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: state.settings.developer_mode)
} else {
state.pool.remove_relay(d)
}
}
if changed {
save_bootstrap_relays(pubkey: state.pubkey, relays: Array(new))
state.pool.connect()
notify(.relays_changed)
}
}
func add_new_relay(model_cache: RelayModelCache, relay_filters: RelayFilters, pool: RelayPool, descriptor: RelayDescriptor, new_relay_filters: Bool, logging_enabled: Bool) {
try? pool.add_relay(descriptor)
let url = descriptor.url
let relay_id = url
guard model_cache.model(withURL: url) == nil else {
return
}
Task.detached(priority: .background) {
guard let meta = try? await fetch_relay_metadata(relay_id: relay_id) else {
return
}
await MainActor.run {
let model = RelayModel(url, metadata: meta)
model_cache.insert(model: model)
if logging_enabled {
pool.setLog(model.log, for: relay_id)
}
// if this is the first time adding filters, we should filter non-paid relays
if new_relay_filters && !meta.is_paid {
relay_filters.insert(timeline: .search, relay_id: relay_id)
}
}
}
}
func fetch_relay_metadata(relay_id: RelayURL) async throws -> RelayMetadata? {
var urlString = relay_id.absoluteString.replacingOccurrences(of: "wss://", with: "https://")
urlString = urlString.replacingOccurrences(of: "ws://", with: "http://")
@@ -1250,3 +1179,4 @@ func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale:
}
}
}
+10 -10
View File
@@ -47,16 +47,16 @@ enum MuteItem: Hashable, Equatable {
// rhs is the item we want to check against (ie. the item in the mute list)
switch (lhs, rhs) {
case (.user(let lhs_pubkey, _), .user(let rhs_pubkey, let rhs_expiration_date)):
return lhs_pubkey == rhs_pubkey && !rhs.is_expired()
case (.hashtag(let lhs_hashtag, _), .hashtag(let rhs_hashtag, let rhs_expiration_date)):
return lhs_hashtag == rhs_hashtag && !rhs.is_expired()
case (.word(let lhs_word, _), .word(let rhs_word, let rhs_expiration_date)):
return lhs_word == rhs_word && !rhs.is_expired()
case (.thread(let lhs_thread, _), .thread(let rhs_thread, let rhs_expiration_date)):
return lhs_thread == rhs_thread && !rhs.is_expired()
default:
return false
case (.user(let lhs_pubkey, _), .user(let rhs_pubkey, _)):
return lhs_pubkey == rhs_pubkey && !rhs.is_expired()
case (.hashtag(let lhs_hashtag, _), .hashtag(let rhs_hashtag, _)):
return lhs_hashtag == rhs_hashtag && !rhs.is_expired()
case (.word(let lhs_word, _), .word(let rhs_word, _)):
return lhs_word == rhs_word && !rhs.is_expired()
case (.thread(let lhs_thread, _), .thread(let rhs_thread, _)):
return lhs_thread == rhs_thread && !rhs.is_expired()
default:
return false
}
}
+1 -1
View File
@@ -33,7 +33,7 @@ func migrate_old_muted_threads_to_new_mutelist(keypair: Keypair, damus_state: Da
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 }
damus_state.mutelist_manager.set_mutelist(new_mutelist_event)
damus_state.postbox.send(new_mutelist_event)
damus_state.nostrNetwork.postbox.send(new_mutelist_event)
// Set existing muted threads to an empty array
UserDefaults.standard.set([], forKey: getMutedThreadsKey(pubkey: keypair.pubkey))
}
@@ -0,0 +1,95 @@
//
// NostrNetworkManager.swift
// damus
//
// Created by Daniel DAquino on 2025-02-26.
//
import Foundation
/// Manages interactions with the Nostr Network.
///
/// This delineates a layer that is responsible for doing mid-level management of interactions with the Nostr network, controlling lower-level classes that perform more network/DB specific code, and providing an easier to use and more semantic interfaces for the rest of the app.
///
/// This is responsible for:
/// - Managing the user's relay list
/// - Establishing a `RelayPool` and maintaining it in sync with the user's relay list as it changes
/// - Abstracting away complexities of interacting with the nostr network, providing an easier-to-use interface to fetch and send content related to the Nostr network
///
/// This is **NOT** responsible for:
/// - Doing actual storage of relay list (delegated via the delegate
/// - Handling low-level relay logic (this will be delegated to lower level classes used in RelayPool/RelayConnection)
class NostrNetworkManager {
/// The relay pool that we manage
///
/// ## 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
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
private var delegate: Delegate
/// Manages the user's relay list, controls RelayPool's connected relays
let userRelayList: UserRelayListManager
/// Handles sending out notes to the network
let postbox: PostBox
/// Handles subscriptions and functions to read or consume data from the Nostr network
let reader: SubscriptionManager
init(delegate: Delegate) {
self.delegate = delegate
let pool = RelayPool(ndb: delegate.ndb, keypair: delegate.keypair)
self.pool = pool
let reader = SubscriptionManager(pool: pool, ndb: delegate.ndb)
let userRelayList = UserRelayListManager(delegate: delegate, pool: pool, reader: reader)
self.reader = reader
self.userRelayList = userRelayList
self.postbox = PostBox(pool: pool)
}
// MARK: - Control functions
/// Connects the app to the Nostr network
func connect() {
self.userRelayList.connect()
}
}
// MARK: - Helper types
extension NostrNetworkManager {
/// The delegate that provides information and structure for the `NostrNetworkManager` to function.
///
/// ## Implementation notes
///
/// This is needed to prevent a circular reference between `DamusState` and `NostrNetworkManager`, and reduce coupling.
protocol Delegate: Sendable {
/// NostrDB instance, used with `RelayPool` to send events for ingestion.
var ndb: Ndb { get }
/// The keypair to use for relay authentication and updating relay lists
var keypair: Keypair { get }
/// The latest relay list event id hex
var latestRelayListEventIdHex: String? { get set } // TODO: Update this once we have full NostrDB query support
/// The latest contact list `NostrEvent`
///
/// Note: Read-only access, because `NostrNetworkManager` does not manage contact lists.
var latestContactListEvent: NostrEvent? { get }
/// Default bootstrap relays to start with when a user relay list is not present
var bootstrapRelays: [RelayURL] { get }
/// Whether the app is in developer mode
var developerMode: Bool { get }
/// The cache of relay model information
var relayModelCache: RelayModelCache { get }
/// Relay filters
var relayFilters: RelayFilters { get }
/// The user's connected NWC wallet
var nwcWallet: WalletConnectURL? { get }
}
}
@@ -0,0 +1,70 @@
//
// SubscriptionManager.swift
// damus
//
// Created by Daniel DAquino on 2025-03-25.
//
extension NostrNetworkManager {
/// Reads or fetches information from RelayPool and NostrDB, and provides an easier and unified higher-level interface.
///
/// ## Implementation notes
///
/// - This class will be a key part of the local relay model migration. Most higher-level code should fetch content from this class, which will properly setup the correct relay pool subscriptions, and provide a stream from NostrDB for higher performance and reliability.
class SubscriptionManager {
private let pool: RelayPool
private var ndb: Ndb
init(pool: RelayPool, ndb: Ndb) {
self.pool = pool
self.ndb = ndb
}
// MARK: - Reading data from Nostr
/// Subscribes to data from the user's relays
///
/// ## Implementation notes
///
/// - When we migrate to the local relay model, we should modify this function to stream directly from NostrDB
///
/// - Parameter filters: The nostr filters to specify what kind of data to subscribe to
/// - 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 {
case .eose: continuation.yield(.eose)
case .event(let nostrEvent):
// At this point of the pipeline, if the note is valid it should have been processed and verified by NostrDB,
// in which case we should pull the note from NostrDB to ensure validity.
// However, NdbNotes are unowned, so we return a function where our callers can temporarily borrow the NostrDB note
let noteId = nostrEvent.id
let lender: NdbNoteLender = { lend in
guard let ndbNoteTxn = self.ndb.lookup_note(noteId) else {
throw NdbNoteLenderError.errorLoadingNote
}
guard let unownedNote = UnownedNdbNote(ndbNoteTxn) else {
throw NdbNoteLenderError.errorLoadingNote
}
lend(unownedNote)
}
continuation.yield(.event(borrow: lender))
}
}
}
continuation.onTermination = { @Sendable _ in
streamTask.cancel() // Close the RelayPool stream when caller stops streaming
}
}
}
}
enum StreamItem {
/// An event which can be borrowed from NostrDB
case event(borrow: NdbNoteLender)
/// The end of stored events
case eose
}
}
@@ -0,0 +1,85 @@
//
// UserRelayListErrors.swift
// damus
//
// Created by Daniel DAquino on 2025-02-27.
//
import Foundation
extension NostrNetworkManager.UserRelayListManager {
/// Models an error that may occur when performing operations that change the user's relay list.
///
/// Callers to functions that throw this error SHOULD handle them in order to provide a better user experience.
enum UpdateError: Error {
/// The user is not authorized to change relay list, usually because the private key is missing.
case notAuthorizedToChangeRelayList
/// An error occurred when forming the relay list Nostr event.
case cannotFormRelayListEvent
/// Cannot add item to the relay list because the relay is already present in the list.
case relayAlreadyExists
/// Cannot update the relay list because we do not have the user's previous relay list.
///
/// Implementers must be careful not to overwrite the user's existing relay list if it exists somewhere else.
case noInitialRelayList
/// Cannot remove or update a specific relay because it is not on the relay list
case noSuchRelay
/// Convert `RelayPool.RelayError` into `UserRelayListUpdateError`
static func from(_ relayPoolError: RelayPool.RelayError) -> Self {
switch relayPoolError {
case .RelayAlreadyExists: return .relayAlreadyExists
}
}
var humanReadableError: ErrorView.UserPresentableError {
switch self {
case .notAuthorizedToChangeRelayList:
ErrorView.UserPresentableError(
user_visible_description: NSLocalizedString("You do not have permission to alter this relay list.", comment: "Human readable error description"),
tip: NSLocalizedString("Please make sure you have logged-in with your private key.", comment: "Human readable tip for error"),
technical_info: nil
)
case .cannotFormRelayListEvent:
ErrorView.UserPresentableError(
user_visible_description: NSLocalizedString("There was a problem creating the relay list event.", comment: "Human readable error description"),
tip: NSLocalizedString("Please try again later or contact support if the issue persists.", comment: "Human readable tip for error"),
technical_info: "Failed forming Nostr event for the relay list update."
)
case .relayAlreadyExists:
ErrorView.UserPresentableError(
user_visible_description: NSLocalizedString("This relay is already in your list.", comment: "Human readable tip for error"),
tip: NSLocalizedString("Check the address and/or the relay list.", comment: "Human readable tip for error"),
technical_info: nil
)
case .noInitialRelayList:
ErrorView.UserPresentableError(
user_visible_description: NSLocalizedString("No initial relay list available to update.", comment: "Human readable error description"),
tip: NSLocalizedString("Please go to Settings > First Aid > Repair relay list, or contact support.", comment: "Human readable tip for error"),
technical_info: "Missing initial relay list data for reference during update."
)
case .noSuchRelay:
ErrorView.UserPresentableError(
user_visible_description: NSLocalizedString("The specified relay that you are trying to udpate was not found in your relay list.", comment: "Human readable error description"),
tip: NSLocalizedString("This is an unexpected error, please contact support.", comment: "Human readable tip for error"),
technical_info: nil
)
}
}
}
enum LoadingError: Error {
case relayListParseError
var humanReadableError: ErrorView.UserPresentableError {
switch self {
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."
)
}
}
}
}
@@ -0,0 +1,311 @@
//
// UserRelayListManager.swift
// damus
//
// Created by Daniel DAquino on 2025-02-27.
//
import Foundation
import Combine
extension NostrNetworkManager {
/// Manages the user's relay list
///
/// - It can compute the user's current relay list
/// - It can compute the best relay list to connect to
/// - It can edit the user's relay list
class UserRelayListManager {
private var delegate: Delegate
private let pool: RelayPool
private let reader: SubscriptionManager
private var relayListObserverTask: Task<Void, Never>? = nil
private var walletUpdatesObserverTask: AnyCancellable? = nil
init(delegate: Delegate, pool: RelayPool, reader: SubscriptionManager) {
self.delegate = delegate
self.pool = pool
self.reader = reader
}
// MARK: - Computing the relays to connect to
private func relaysToConnectTo() -> [RelayPool.RelayDescriptor] {
return self.computeRelaysToConnectTo(with: self.getBestEffortRelayList())
}
private func computeRelaysToConnectTo(with relayList: NIP65.RelayList) -> [RelayPool.RelayDescriptor] {
let regularRelayDescriptorList = relayList.toRelayDescriptors()
if let nwcWallet = delegate.nwcWallet {
return regularRelayDescriptorList + [.nwc(url: nwcWallet.relay)]
}
return regularRelayDescriptorList
}
// MARK: - Getting the user's relay list
/// Gets the "best effort" relay list.
///
/// It attempts to get a relay list from the user. If one is not available, it uses the default bootstrap list.
///
/// This is always guaranteed to return a relay list.
func getBestEffortRelayList() -> NIP65.RelayList {
guard let userCurrentRelayList = self.getUserCurrentRelayList() else {
return NIP65.RelayList(relays: delegate.bootstrapRelays)
}
return userCurrentRelayList
}
/// Gets the user's current relay list.
///
/// It attempts to get a NIP-65 relay list from the local database, or falls back to a legacy list.
func getUserCurrentRelayList() -> NIP65.RelayList? {
if let latestRelayListEvent = try? self.getLatestNIP65RelayList() { return latestRelayListEvent }
if let latestRelayListEvent = try? self.getLatestKind3RelayList() { return latestRelayListEvent }
if let latestRelayListEvent = try? self.getLatestUserDefaultsRelayList() { return latestRelayListEvent }
return nil
}
/// Gets the latest NIP-65 relay list from NostrDB.
///
/// This is `private` because it is part of internal logic. Callers should use the higher level functions.
///
/// - Returns: The latest NIP-65 relay list object
private func getLatestNIP65RelayList() throws(LoadingError) -> NIP65.RelayList? {
guard let latestRelayListEvent = self.getLatestNIP65RelayListEvent() else { return nil }
guard let list = try? NIP65.RelayList(event: latestRelayListEvent) else { throw .relayListParseError }
return list
}
/// Gets the latest NIP-65 relay list event from NostrDB.
///
/// This is `private` because it is part of internal logic. Callers should use the higher level functions.
///
/// It is recommended to use this function only if the NostrEvent metadata is needed. For cases where only the relay list info is needed, use `getLatestNIP65RelayList` instead.
///
/// - Returns: The latest NIP-65 relay list NdbNote
private func getLatestNIP65RelayListEvent() -> NdbNote? {
guard let latestRelayListEventId = delegate.latestRelayListEventIdHex else { return nil }
guard let latestRelayListEventId = NoteId(hex: latestRelayListEventId) else { return nil }
return delegate.ndb.lookup_note(latestRelayListEventId)?.unsafeUnownedValue?.to_owned()
}
/// Gets the latest `kind:3` relay list from NostrDB.
///
/// This is `private` because it is part of internal logic. Callers should use the higher level functions.
private func getLatestKind3RelayList() throws(LoadingError) -> NIP65.RelayList? {
guard let latestContactListEvent = delegate.latestContactListEvent else { return nil }
guard let legacyContactList = try? NIP65.RelayList.fromLegacyContactList(latestContactListEvent) else { throw .relayListParseError }
return legacyContactList
}
/// Gets the latest relay list from `UserDefaults`
///
/// This is `private` because it is part of internal logic. Callers should use the higher level functions.
private func getLatestUserDefaultsRelayList() throws(LoadingError) -> NIP65.RelayList? {
let key = bootstrap_relays_setting_key(pubkey: delegate.keypair.pubkey)
guard let relays = UserDefaults.standard.stringArray(forKey: key) else { return nil }
let relayUrls = relays.compactMap({ RelayURL($0) })
if relayUrls.count == 0 { return nil }
return NIP65.RelayList(relays: relayUrls)
}
// MARK: - Getting metadata from the user's relay list
/// Gets the creation date of the user's current relay list, with preference to NIP-65 relay lists
/// - Returns: The current relay list's creation date
private func getUserCurrentRelayListCreationDate() -> UInt32? {
if let latestNIP65RelayListEvent = self.getLatestNIP65RelayListEvent() { return latestNIP65RelayListEvent.created_at }
if let latestKind3RelayListEvent = delegate.latestContactListEvent { return latestKind3RelayListEvent.created_at }
return nil
}
// MARK: - Listening to and handling relay updates from the network
func connect() {
self.load()
self.relayListObserverTask?.cancel()
self.relayListObserverTask = Task { await self.listenAndHandleRelayUpdates() }
self.walletUpdatesObserverTask?.cancel()
self.walletUpdatesObserverTask = handle_notify(.attached_wallet).sink { _ in self.load() }
}
func listenAndHandleRelayUpdates() async {
let filter = NostrFilter(kinds: [.relay_list], authors: [delegate.keypair.pubkey])
for await item in self.reader.subscribe(filters: [filter]) {
switch item {
case .event(borrow: let borrow): // Signature validity already ensured at this point
let currentRelayListCreationDate = self.getUserCurrentRelayListCreationDate()
try? borrow { note in
guard note.pubkey == self.delegate.keypair.pubkey else { return } // Ensure this new list was ours
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? self.set(userRelayList: relayList) // Set the validated list
}
case .eose: continue
}
}
}
// MARK: - Editing the user's relay list
func upsert(relay: NIP65.RelayList.RelayItem, force: Bool = false, overwriteExisting: Bool = false) throws(UpdateError) {
guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList }
guard !currentUserRelayList.relays.keys.contains(relay.url) || overwriteExisting else { throw .relayAlreadyExists }
var newList = currentUserRelayList.relays
newList[relay.url] = relay
try self.set(userRelayList: NIP65.RelayList(relays: Array(newList.values)))
}
func insert(relay: NIP65.RelayList.RelayItem, force: Bool = false) throws(UpdateError) {
guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList }
guard currentUserRelayList.relays[relay.url] == nil else { throw .relayAlreadyExists }
try self.upsert(relay: relay, force: force)
}
func remove(relayURL: RelayURL, force: Bool = false) throws(UpdateError) {
guard let currentUserRelayList = force ? self.getBestEffortRelayList() : self.getUserCurrentRelayList() else { throw .noInitialRelayList }
guard currentUserRelayList.relays.keys.contains(relayURL) || force else { throw .noSuchRelay }
var newList = currentUserRelayList.relays
newList[relayURL] = nil
try self.set(userRelayList: NIP65.RelayList(relays: Array(newList.values)))
}
func set(userRelayList: NIP65.RelayList) throws(UpdateError) {
guard let fullKeypair = delegate.keypair.to_full() else { throw .notAuthorizedToChangeRelayList }
guard let relayListEvent = userRelayList.toNostrEvent(keypair: fullKeypair) else { throw .cannotFormRelayListEvent }
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
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`
/// Loads the current user relay list
func load() {
self.apply(newRelayList: self.relaysToConnectTo())
}
/// Loads a new relay list into the active relay pool, making sure it matches the specified relay list.
///
/// - Parameters:
/// - state: The state of the app
/// - newRelayList: The new relay list to be applied
///
///
/// ## Implementation notes
///
/// - 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.
private func apply(newRelayList: [RelayPool.RelayDescriptor]) {
let currentRelayList = self.pool.relays.map({ $0.descriptor })
var changed = false
let new_relay_filters = load_relay_filters(delegate.keypair.pubkey) == nil
for index in self.pool.relays.indices {
guard let newDescriptor = newRelayList.first(where: { $0.url == self.pool.relays[index].descriptor.url }) else { continue }
self.pool.relays[index].descriptor.info = newDescriptor.info
// Relay read-write configuration change does not need reconnection to the relay, so we do not set the `changed` flag.
}
// Working with URL Sets for difference analysis
let currentRelayURLs = Set(currentRelayList.map { $0.url })
let newRelayURLs = Set(newRelayList.map { $0.url })
// Analyzing which relays to add or remove
let relaysToRemove = currentRelayURLs.subtracting(newRelayURLs)
let relaysToAdd = newRelayURLs.subtracting(currentRelayURLs)
// Remove relays not in the new list
relaysToRemove.forEach { url in
pool.remove_relay(url)
changed = true
}
// Add new relays from the new list
relaysToAdd.forEach { url in
guard let descriptor = newRelayList.first(where: { $0.url == url }) else { return }
add_new_relay(
model_cache: delegate.relayModelCache,
relay_filters: delegate.relayFilters,
pool: pool,
descriptor: descriptor,
new_relay_filters: new_relay_filters,
logging_enabled: delegate.developerMode
)
changed = true
}
if changed {
pool.connect()
notify(.relays_changed)
}
}
}
}
// MARK: - Helper extensions
fileprivate extension NIP65.RelayList.RelayItem {
func toRelayDescriptor() -> RelayPool.RelayDescriptor {
return RelayPool.RelayDescriptor(url: self.url, info: self.rwConfiguration, variant: .regular) // NIP-65 relays are regular by definition.
}
}
fileprivate extension NIP65.RelayList {
func toRelayDescriptors() -> [RelayPool.RelayDescriptor] {
return self.relays.values.map({ $0.toRelayDescriptor() })
}
}
// MARK: - Helper functions
/// Adds a new relay, taking care of other tangential concerns, such as updating the relay model cache, configuring logging, etc
///
/// ## Implementation notes
///
/// 1. This function used to be in `HomeModel.swift` and moved here when `UserRelayListManager` was first implemented
/// 2. This is `fileprivate` because only `UserRelayListManager` should be able to manage the user's relay list and apply them to the `RelayPool`
///
/// - Parameters:
/// - model_cache: The relay model cache, that keeps metadata cached
/// - relay_filters: Relay filters
/// - pool: The relay pool to add this in
/// - descriptor: The description of the relay being added
/// - new_relay_filters: Whether to insert new relay filters
/// - 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) {
try? pool.add_relay(descriptor)
let url = descriptor.url
let relay_id = url
guard model_cache.model(withURL: url) == nil else {
return
}
Task.detached(priority: .background) {
guard let meta = try? await fetch_relay_metadata(relay_id: relay_id) else {
return
}
await MainActor.run {
let model = RelayModel(url, metadata: meta)
model_cache.insert(model: model)
if logging_enabled {
pool.setLog(model.log, for: relay_id)
}
// if this is the first time adding filters, we should filter non-paid relays
if new_relay_filters && !meta.is_paid {
relay_filters.insert(timeline: .search, relay_id: relay_id)
}
}
}
}
+8 -1
View File
@@ -54,7 +54,14 @@ func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent
guard ev.age < EVENT_MAX_AGE_FOR_NOTIFICATION else {
return false
}
// Don't show notifications for future events.
// Allow notes that are created no more than 3 seconds in the future
// to account for natural clock skew between sender and receiver.
guard ev.age >= -3 else {
return false
}
return true
}
+26 -12
View File
@@ -10,8 +10,18 @@ import Foundation
class ProfileModel: ObservableObject, Equatable {
@Published var contacts: NostrEvent? = nil
@Published var following: Int = 0
@Published var relays: [RelayURL: RelayInfo]? = nil
@Published var relay_list: NIP65.RelayList? = nil
@Published var legacy_relay_list: [RelayURL: LegacyKind3RelayRWConfiguration]? = nil
@Published var progress: Int = 0
var relay_urls: [RelayURL]? {
if let relay_list {
return relay_list.relays.values.map({ $0.url })
}
if let legacy_relay_list {
return Array(legacy_relay_list.keys)
}
return nil
}
private let MAX_SHARE_RELAYS = 4
@@ -59,16 +69,17 @@ class ProfileModel: ObservableObject, Equatable {
func unsubscribe() {
print("unsubscribing from profile \(pubkey) with sub_id \(sub_id)")
damus.pool.unsubscribe(sub_id: sub_id)
damus.pool.unsubscribe(sub_id: prof_subid)
damus.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
damus.nostrNetwork.pool.unsubscribe(sub_id: prof_subid)
if pubkey != damus.pubkey {
damus.pool.unsubscribe(sub_id: conversations_subid)
damus.nostrNetwork.pool.unsubscribe(sub_id: conversations_subid)
}
}
func subscribe() {
var text_filter = NostrFilter(kinds: [.text, .longform, .highlight])
var profile_filter = NostrFilter(kinds: [.contacts, .metadata, .boost])
var relay_list_filter = NostrFilter(kinds: [.relay_list], authors: [pubkey])
profile_filter.authors = [pubkey]
@@ -77,8 +88,8 @@ class ProfileModel: ObservableObject, Equatable {
print("subscribing to textlike events from profile \(pubkey) with sub_id \(sub_id)")
//print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]])
damus.pool.subscribe(sub_id: sub_id, filters: [text_filter], handler: handle_event)
damus.pool.subscribe(sub_id: prof_subid, filters: [profile_filter], handler: handle_event)
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()
}
@@ -94,7 +105,7 @@ class ProfileModel: ObservableObject, Equatable {
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])
print("subscribing to conversation events from and to profile \(pubkey) with sub_id \(conversations_subid)")
damus.pool.subscribe(sub_id: conversations_subid, filters: [conversations_filter_them, conversations_filter_us], handler: handle_event)
damus.nostrNetwork.pool.subscribe(sub_id: conversations_subid, filters: [conversations_filter_them, conversations_filter_us], handler: handle_event)
}
func handle_profile_contact_event(_ ev: NostrEvent) {
@@ -109,7 +120,7 @@ class ProfileModel: ObservableObject, Equatable {
self.contacts = ev
self.following = count_pubkeys(ev.tags)
self.relays = decode_json_relays(ev.content)
self.legacy_relay_list = decode_json_relays(ev.content)
}
private func add_event(_ ev: NostrEvent) {
@@ -120,6 +131,9 @@ class ProfileModel: ObservableObject, Equatable {
} else if ev.known_kind == .contacts {
handle_profile_contact_event(ev)
}
else if ev.known_kind == .relay_list {
self.relay_list = try? NIP65.RelayList(event: ev) // Whether another user's list is malformatted is something beyond our control. Probably best to suppress errors
}
seen_event.insert(ev.id)
}
@@ -192,7 +206,7 @@ class ProfileModel: ObservableObject, Equatable {
private func findRelaysHandler(relay_id: RelayURL, ev: NostrConnectionEvent) {
if case .nostr_event(let resp) = ev, case .event(_, let event) = resp, case .contacts = event.known_kind {
self.relays = decode_json_relays(event.content)
self.legacy_relay_list = decode_json_relays(event.content)
}
}
@@ -200,15 +214,15 @@ class ProfileModel: ObservableObject, Equatable {
var profile_filter = NostrFilter(kinds: [.contacts])
profile_filter.authors = [pubkey]
damus.pool.subscribe(sub_id: findRelay_subid, filters: [profile_filter], handler: findRelaysHandler)
damus.nostrNetwork.pool.subscribe(sub_id: findRelay_subid, filters: [profile_filter], handler: findRelaysHandler)
}
func unsubscribeFindRelays() {
damus.pool.unsubscribe(sub_id: findRelay_subid)
damus.nostrNetwork.pool.unsubscribe(sub_id: findRelay_subid)
}
func getCappedRelayStrings() -> [String] {
return relays?.keys.prefix(MAX_SHARE_RELAYS).map { $0.absoluteString } ?? []
return self.relay_urls?.prefix(MAX_SHARE_RELAYS).map { $0.absoluteString } ?? []
}
}
+5 -5
View File
@@ -41,13 +41,13 @@ class SearchHomeModel: ObservableObject {
func subscribe() {
loading = true
let to_relays = determine_to_relays(pool: damus_state.pool, filters: damus_state.relay_filters)
damus_state.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event, to: to_relays)
let to_relays = determine_to_relays(pool: damus_state.nostrNetwork.pool, filters: damus_state.relay_filters)
damus_state.nostrNetwork.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event, to: to_relays)
}
func unsubscribe(to: RelayURL? = nil) {
loading = false
damus_state.pool.unsubscribe(sub_id: base_subid, to: to.map { [$0] })
damus_state.nostrNetwork.pool.unsubscribe(sub_id: base_subid, to: to.map { [$0] })
}
func handle_event(relay_id: RelayURL, conn_ev: NostrConnectionEvent) {
@@ -140,7 +140,7 @@ func load_profiles<Y>(context: String, profiles_subid: String, relay_id: RelayUR
let filter = NostrFilter(kinds: [.metadata], authors: authors)
damus_state.pool.subscribe_to(sub_id: profiles_subid, filters: [filter], to: [relay_id]) { rid, conn_ev in
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 {
@@ -156,7 +156,7 @@ func load_profiles<Y>(context: String, profiles_subid: String, relay_id: RelayUR
}
case .eose:
print("load_profiles[\(context)]: done loading \(authors.count) profiles from \(relay_id)")
damus_state.pool.unsubscribe(sub_id: profiles_subid, to: [relay_id])
damus_state.nostrNetwork.pool.unsubscribe(sub_id: profiles_subid, to: [relay_id])
case .ok:
break
case .notice:
+4 -4
View File
@@ -41,13 +41,13 @@ class SearchModel: ObservableObject {
//likes_filter.ids = ref_events.referenced_ids!
print("subscribing to search '\(search)' with sub_id \(sub_id)")
state.pool.register_handler(sub_id: sub_id, handler: handle_event)
state.nostrNetwork.pool.register_handler(sub_id: sub_id, handler: handle_event)
loading = true
state.pool.send(.subscribe(.init(filters: [search], sub_id: sub_id)))
state.nostrNetwork.pool.send(.subscribe(.init(filters: [search], sub_id: sub_id)))
}
func unsubscribe() {
state.pool.unsubscribe(sub_id: sub_id)
state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
loading = false
print("unsubscribing from search '\(search)' with sub_id \(sub_id)")
}
@@ -67,7 +67,7 @@ class SearchModel: ObservableObject {
}
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
let (sub_id, done) = handle_subid_event(pool: state.pool, relay_id: relay_id, ev: ev) { sub_id, ev in
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)
}
+9 -9
View File
@@ -88,12 +88,12 @@ class ThreadModel: ObservableObject {
/// Unsubscribe from events in the relay pool. Call this when unloading the view
func unsubscribe() {
self.damus_state.pool.remove_handler(sub_id: base_subid)
self.damus_state.pool.remove_handler(sub_id: meta_subid)
self.damus_state.pool.remove_handler(sub_id: profiles_subid)
self.damus_state.pool.unsubscribe(sub_id: base_subid)
self.damus_state.pool.unsubscribe(sub_id: meta_subid)
self.damus_state.pool.unsubscribe(sub_id: profiles_subid)
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)
}
@@ -129,8 +129,8 @@ class ThreadModel: ObservableObject {
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)
damus_state.pool.subscribe(sub_id: base_subid, filters: base_filters, handler: handle_event)
damus_state.pool.subscribe(sub_id: meta_subid, filters: meta_filters, handler: handle_event)
damus_state.nostrNetwork.pool.subscribe(sub_id: base_subid, filters: base_filters, handler: handle_event)
damus_state.nostrNetwork.pool.subscribe(sub_id: meta_subid, filters: meta_filters, handler: handle_event)
}
/// Adds an event to this thread.
@@ -176,7 +176,7 @@ class ThreadModel: ObservableObject {
/// Marked as private because it is this class' responsibility to load events, not the view's. Simplify the interface
@MainActor
private func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
let (sub_id, done) = handle_subid_event(pool: damus_state.pool, relay_id: relay_id, ev: ev) { sid, ev in
let (sub_id, done) = handle_subid_event(pool: damus_state.nostrNetwork.pool, relay_id: relay_id, ev: ev) { sid, ev in
guard subids.contains(sid) else {
return
}
+4
View File
@@ -342,6 +342,10 @@ class UserSettingsStore: ObservableObject {
@Setting(key: "draft_event_ids", default_value: nil)
var draft_event_ids: [String]?
// TODO: Get rid of this once we have NostrDB query capabilities integrated
@Setting(key: "latest_relay_list_event_id", default_value: nil)
var latestRelayListEventIdHex: String?
// MARK: Helper types
enum NotificationsMode: String, CaseIterable, Identifiable, StringCodable, Equatable {
+2 -2
View File
@@ -31,11 +31,11 @@ class ZapsModel: ObservableObject {
case .note(let note_target):
filter.referenced_ids = [note_target.note_id]
}
state.pool.subscribe(sub_id: zaps_subid, filters: [filter], handler: handle_event)
state.nostrNetwork.pool.subscribe(sub_id: zaps_subid, filters: [filter], handler: handle_event)
}
func unsubscribe() {
state.pool.unsubscribe(sub_id: zaps_subid)
state.nostrNetwork.pool.unsubscribe(sub_id: zaps_subid)
}
@MainActor
+171
View File
@@ -0,0 +1,171 @@
//
// NIP65.swift
// damus
//
// Created by Daniel DAquino on 2025-02-21.
//
// Some text excerpts taken from the Nostr Protocol itself (which are public domain)
import OrderedCollections
import Foundation
/// Includes models and functions for working with NIP-65
struct NIP65: Sendable {}
extension NIP65 {
/// Models a NIP-65 relay list
struct RelayList: NostrEventConvertible, Sendable {
let relays: OrderedDictionary<RelayURL, RelayItem>
// MARK: - Initialization
init(event: NdbNote) throws(NIP65DecodingError) {
try self.init(event: UnownedNdbNote(event))
}
init(event: borrowing UnownedNdbNote) throws(NIP65DecodingError) {
guard event.known_kind == .relay_list else { throw .notRelayList }
var relays: [RelayItem] = []
for tag in event.tags {
guard let relay = try RelayItem.fromTag(tag: tag) else { continue }
relays.append(relay)
}
self.relays = Self.relayOrderedDictionary(from: relays)
}
init?(event: NdbNote?) throws(NIP65DecodingError) {
guard let event else { return nil }
try self.init(event: event)
}
init(relays: [RelayItem]) {
self.relays = Self.relayOrderedDictionary(from: relays)
}
init(relays: [RelayURL]) {
let relayItemList = relays.map({ RelayItem(url: $0, rwConfiguration: .readWrite) })
self.relays = Self.relayOrderedDictionary(from: relayItemList)
}
private static func relayOrderedDictionary(from relayList: [RelayItem]) -> OrderedDictionary<RelayURL, RelayItem> {
var seenUrls: Set<RelayURL> = []
return OrderedDictionary(uniqueKeysWithValues: relayList.compactMap({
// We need to ensure the keys are unique to avoid assertion errors from OrderedDictionary
guard !seenUrls.contains($0.url) else { return nil }
seenUrls.insert($0.url)
return ($0.url, $0)
}))
}
// MARK: - Conversion to a Nostr Event
func toNostrEvent(keypair: FullKeypair, timestamp: UInt32? = nil) -> NostrEvent? {
return NdbNote(
content: "",
keypair: keypair.to_keypair(),
kind: NostrKind.relay_list.rawValue,
tags: self.relays.values.map({ $0.tag }),
createdAt: timestamp ?? UInt32(Date.now.timeIntervalSince1970)
)
}
}
}
extension NIP65 {
/// An error thrown when decoding an item into a NIP-65 relay list
enum NIP65DecodingError: Error {
/// The Nostr event being converted is not a NIP-65 relay list
case notRelayList
/// The relay URL is invalid
case invalidRelayURL
///The relay RW marker is invalid
case invalidRelayMarker
}
}
extension NIP65.RelayList {
/// An item referencing a relay and its configuration inside a relay list
struct RelayItem: ThrowingTagConvertible, Sendable {
typealias E = NIP65.NIP65DecodingError
let url: RelayURL
let rwConfiguration: RWConfiguration
/// The raw tag sequence in a Nostr event
var tag: [String] {
var tag = ["r", url.absoluteString]
if let rwMarker = rwConfiguration.tagItem { tag.append(rwMarker) }
return tag
}
/// Initialize a new relay item from a Nostr event's tag sequence
static func fromTag(tag: TagSequence) throws(E) -> NIP65.RelayList.RelayItem? {
var i = tag.makeIterator()
guard tag.count >= 2,
let t0 = i.next(),
let key = t0.single_char,
let rkey = RefId.RefKey(rawValue: key),
let t1 = i.next()
else { return nil }
let t2 = i.next()
switch rkey {
case .r: return try self.fromRawInfo(urlString: t1.string(), rwMarker: t2?.string())
// Keep options explicit to make compiler prompt developer on whether to ignore or handle new future options
case .e, .p, .q, .t, .d, .a: return nil
}
}
/// Initializes a Relay Item based on raw information
static func fromRawInfo(urlString: String, rwMarker: String?) throws(NIP65.NIP65DecodingError) -> NIP65.RelayList.RelayItem? {
guard let relayUrl = RelayURL(urlString) else { throw .invalidRelayURL }
guard let rwConfiguration = RWConfiguration.fromTagItem(rwMarker) else { throw .invalidRelayMarker }
return NIP65.RelayList.RelayItem(url: relayUrl, rwConfiguration: rwConfiguration)
}
}
}
extension NIP65.RelayList.RelayItem {
/// The read/write configuration for a relay item
enum RWConfiguration: TagItemConvertible {
case read
case write
case readWrite
static let READ_MARKER: String = "read"
static let WRITE_MARKER: String = "write"
var canRead: Bool {
switch self {
case .read, .readWrite: return true
case .write: return false
}
}
var canWrite: Bool {
switch self {
case .write, .readWrite: return true
case .read: return false
}
}
/// A raw Nostr Event tag item
var tagItem: String? {
switch self {
case .read: Self.READ_MARKER
case .write: Self.WRITE_MARKER
case .readWrite: nil
}
}
/// Initialize this from a raw Nostr Event tag item
static func fromTagItem(_ item: String?) -> Self? {
if item == READ_MARKER { return .read }
if item == WRITE_MARKER { return .write }
return .readWrite
}
}
}
+13
View File
@@ -34,6 +34,19 @@ protocol TagConvertible {
static func from_tag(tag: TagSequence) -> Self?
}
/// Protocol for types that can be converted from/to a tag sequence with the possibilty of an error
protocol ThrowingTagConvertible {
associatedtype E: Error
var tag: [String] { get }
static func fromTag(tag: TagSequence) throws(E) -> Self?
}
/// Protocol for types that can be converted from/to a tag item
protocol TagItemConvertible {
var tagItem: String? { get }
static func fromTagItem(_ item: String?) -> Self?
}
struct QuoteId: IdType, TagKey, TagConvertible {
let id: Data
+1 -1
View File
@@ -7,7 +7,7 @@
import Foundation
func make_auth_request(keypair: FullKeypair, challenge_string: String, relay: Relay) -> NostrEvent? {
func make_auth_request(keypair: FullKeypair, challenge_string: String, relay: RelayPool.Relay) -> NostrEvent? {
let tags: [[String]] = [["relay", relay.descriptor.url.absoluteString],["challenge", challenge_string]]
let event = NostrEvent(content: "", keypair: keypair.to_keypair(), kind: 22242, tags: tags)
return event
+3 -3
View File
@@ -7,7 +7,7 @@
import Foundation
func make_zap_request_event(keypair: FullKeypair, content: String, relays: [RelayDescriptor], target: ZapTarget, zap_type: ZapType) -> MakeZapRequest? {
func make_zap_request_event(keypair: FullKeypair, content: String, relays: [RelayPool.RelayDescriptor], target: ZapTarget, zap_type: ZapType) -> MakeZapRequest? {
var tags = zap_target_to_tags(target)
var relay_tag = ["relays"]
relay_tag.append(contentsOf: relays.map { $0.url.absoluteString })
@@ -78,8 +78,8 @@ func make_private_zap_request_event(identity: FullKeypair, enc_key: FullKeypair,
func make_first_contact_event(keypair: Keypair) -> NostrEvent? {
let bootstrap_relays = load_bootstrap_relays(pubkey: keypair.pubkey)
let rw_relay_info = RelayInfo(read: true, write: true)
var relays: [RelayURL: RelayInfo] = [:]
let rw_relay_info = LegacyKind3RelayRWConfiguration(read: true, write: true)
var relays: [RelayURL: LegacyKind3RelayRWConfiguration] = [:]
for relay in bootstrap_relays {
relays[relay] = rw_relay_info
+12
View File
@@ -13,6 +13,18 @@ import CryptoKit
import NaturalLanguage
/// A protocol for structs and classes that can convert themselves from/to a NostrEvent
protocol NostrEventConvertible {
associatedtype E: Error
/// Iniitialize this type from a NostrEvent
init(event: NostrEvent) throws(E)
/// Convert this type into a Nostr Event, using a keypair for signing and a specific timestamp
func toNostrEvent(keypair: FullKeypair, timestamp: UInt32?) -> NostrEvent?
}
enum ValidationResult: Decodable {
case unknown
case ok
+2
View File
@@ -8,6 +8,7 @@
import Foundation
/// A known Nostr event kind, addressable by name, with the actual number assigned by the protocol as the value
enum NostrKind: UInt32, Codable {
case metadata = 0
case text = 1
@@ -18,6 +19,7 @@ enum NostrKind: UInt32, Codable {
case like = 7
case chat = 42
case mute_list = 10000
case relay_list = 10002
case list_deprecated = 30000
case draft = 31234
case longform = 30023
+12 -1
View File
@@ -12,11 +12,14 @@ struct NostrSubscribe {
let sub_id: String
}
/// Models a request/message that is sent to a Nostr relay
enum NostrRequestType {
/// A standard nostr request
case typical(NostrRequest)
/// A customized nostr request. Generally used in the context of a nostrscript.
case custom(String)
/// Whether this request is meant to write data to a relay
var is_write: Bool {
guard case .typical(let req) = self else {
return true
@@ -25,6 +28,7 @@ enum NostrRequestType {
return req.is_write
}
/// Whether this request is meant to read data from a relay
var is_read: Bool {
guard case .typical(let req) = self else {
return true
@@ -34,12 +38,18 @@ enum NostrRequestType {
}
}
/// Models a standard request/message that is sent to a Nostr relay.
enum NostrRequest {
/// Subscribes to receive information from the relay
case subscribe(NostrSubscribe)
/// Unsubscribes from an existing subscription, addressed by its id
case unsubscribe(String)
/// Posts an event
case event(NostrEvent)
/// Authenticate with the relay
case auth(NostrEvent)
/// Whether this request is meant to write data to a relay
var is_write: Bool {
switch self {
case .subscribe:
@@ -53,6 +63,7 @@ enum NostrRequest {
}
}
/// Whether this request is meant to read data from a relay
var is_read: Bool {
return !is_write
}
+26 -1
View File
@@ -115,6 +115,19 @@ enum FollowRef: TagKeys, Hashable, TagConvertible, Equatable {
}
}
/// Models common tag references defined by the Nostr protocol, and their associated values.
///
/// For example, this raw JSON tag sequence:
/// ```json
/// ["p", "8b2be0a0ad34805d76679272c28a77dbede9adcbfdca48c681ec8b624a1208a6"]
/// ```
///
/// would be parsed into something equivalent to `.pubkey(Pubkey(hex: "8b2be0a0ad34805d76679272c28a77dbede9adcbfdca48c681ec8b624a1208a6"))`
///
/// ## Notes
///
/// - Not all tag information from all NIPs can be modelled using this alone, as some NIPs may define extra associated values for specific event types. You may need to use a specialized type for some event kinds
///
enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
case event(NoteId)
case pubkey(Pubkey)
@@ -124,6 +137,7 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
case naddr(NAddr)
case reference(String)
/// The key that defines the type of reference being made
var key: RefKey {
switch self {
case .event: return .e
@@ -136,6 +150,14 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
}
}
/// Defines the type of reference being made on a Nostr event tag
///
/// Example:
/// ```json
/// ["p", "8b2be0a0ad34805d76679272c28a77dbede9adcbfdca48c681ec8b624a1208a6"]
/// ```
///
/// The `RefKey` is "p"
enum RefKey: AsciiCharacter, TagKey, CustomStringConvertible {
case e, p, t, d, q, a, r
@@ -148,10 +170,12 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
}
}
/// A raw nostr-style tag sequence representation of this object
var tag: [String] {
[self.key.description, self.description]
}
/// Describes what is being referenced, as a `String`
var description: String {
switch self {
case .event(let noteId): return noteId.hex()
@@ -166,6 +190,7 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
}
}
/// Parses a raw tag sequence
static func from_tag(tag: TagSequence) -> RefId? {
var i = tag.makeIterator()
+88 -50
View File
@@ -7,16 +7,25 @@
import Foundation
public struct RelayInfo: Codable {
let read: Bool?
let write: Bool?
public struct LegacyKind3RelayRWConfiguration: Codable, Sendable {
public let read: Bool?
public let write: Bool?
init(read: Bool, write: Bool) {
self.read = read
self.write = write
}
static let rw = RelayInfo(read: true, write: true)
static let rw = LegacyKind3RelayRWConfiguration(read: true, write: true)
func toNIP65RWConfiguration() -> NIP65.RelayList.RelayItem.RWConfiguration? {
switch (self.read, self.write) {
case (false, true): return .write
case (true, false): return .read
case (true, true): return .readWrite
default: return nil
}
}
}
enum RelayVariant {
@@ -25,30 +34,33 @@ enum RelayVariant {
case nwc
}
public struct RelayDescriptor {
let url: RelayURL
let info: RelayInfo
let variant: RelayVariant
init(url: RelayURL, info: RelayInfo, variant: RelayVariant = .regular) {
self.url = url
self.info = info
self.variant = variant
}
var ephemeral: Bool {
switch variant {
case .regular:
return false
case .ephemeral:
return true
case .nwc:
return true
extension RelayPool {
/// Describes a relay for use in `RelayPool`
public struct RelayDescriptor {
let url: RelayURL
var info: NIP65.RelayList.RelayItem.RWConfiguration
let variant: RelayVariant
init(url: RelayURL, info: NIP65.RelayList.RelayItem.RWConfiguration, variant: RelayVariant = .regular) {
self.url = url
self.info = info
self.variant = variant
}
var ephemeral: Bool {
switch variant {
case .regular:
return false
case .ephemeral:
return true
case .nwc:
return true
}
}
static func nwc(url: RelayURL) -> RelayDescriptor {
return RelayDescriptor(url: url, info: .readWrite, variant: .nwc)
}
}
static func nwc(url: RelayURL) -> RelayDescriptor {
return RelayDescriptor(url: url, info: .rw, variant: .nwc)
}
}
@@ -129,30 +141,56 @@ struct RelayMetadata: Codable {
}
}
class Relay: Identifiable {
let descriptor: RelayDescriptor
let connection: RelayConnection
var authentication_state: RelayAuthenticationState
var flags: Int
init(descriptor: RelayDescriptor, connection: RelayConnection) {
self.flags = 0
self.descriptor = descriptor
self.connection = connection
self.authentication_state = RelayAuthenticationState.none
extension RelayPool {
class Relay: Identifiable {
var descriptor: RelayDescriptor
let connection: RelayConnection
var authentication_state: RelayAuthenticationState
var flags: Int
init(descriptor: RelayDescriptor, connection: RelayConnection) {
self.flags = 0
self.descriptor = descriptor
self.connection = connection
self.authentication_state = RelayAuthenticationState.none
}
var is_broken: Bool {
return (flags & RelayFlags.broken.rawValue) == RelayFlags.broken.rawValue
}
var id: RelayURL {
return descriptor.url
}
}
var is_broken: Bool {
return (flags & RelayFlags.broken.rawValue) == RelayFlags.broken.rawValue
}
var id: RelayURL {
return descriptor.url
}
}
enum RelayError: Error {
case RelayAlreadyExists
extension RelayPool {
enum RelayError: Error {
case RelayAlreadyExists
}
}
// MARK: - Extension to bridge NIP-65 relay list structs with app-native objects
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
}
}
+69 -9
View File
@@ -24,13 +24,15 @@ struct SeenEvent: Hashable {
let evid: NoteId
}
/// Establishes and manages connections and subscriptions to a list of relays.
class RelayPool {
var relays: [Relay] = []
private(set) var relays: [Relay] = []
var handlers: [RelayHandler] = []
var request_queue: [QueuedRequest] = []
var seen: Set<SeenEvent> = Set()
var counts: [RelayURL: UInt64] = [:]
var ndb: Ndb
/// The keypair used to authenticate with relays
var keypair: Keypair?
var message_received_function: (((String, RelayDescriptor)) -> Void)?
var message_sent_function: (((String, Relay)) -> Void)?
@@ -122,7 +124,7 @@ class RelayPool {
}
}
func add_relay(_ desc: RelayDescriptor) throws {
func add_relay(_ desc: RelayDescriptor) throws(RelayError) {
let relay_id = desc.url
if get_relay(relay_id) != nil {
throw RelayError.RelayAlreadyExists
@@ -200,6 +202,64 @@ class RelayPool {
register_handler(sub_id: sub_id, handler: handler)
send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to)
}
/// Subscribes to data from the `RelayPool` based on a filter and a list of desired relays.
///
/// - Parameters:
/// - 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
/// - eoseTimeout: The maximum timeout which to give up waiting for the eoseSignal, in seconds
/// - 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> {
let desiredRelays = desiredRelays ?? self.relays.map({ $0.descriptor.url })
return AsyncStream<StreamItem> { continuation in
let sub_id = UUID().uuidString
var seenEvents: Set<NoteId> = []
var relaysWhoFinishedInitialResults: Set<RelayURL> = []
var eoseSent = false
self.subscribe(sub_id: sub_id, filters: filters, handler: { (relayUrl, connectionEvent) in
switch connectionEvent {
case .ws_event(let ev):
// Websocket events such as connect/disconnect/error are already handled in `RelayConnection`. Do not perform any handling here.
// For the future, perhaps we should abstract away `.ws_event` in `RelayPool`? Seems like something to be handled on the `RelayConnection` layer.
break
case .nostr_event(let nostrResponse):
guard nostrResponse.subid == sub_id else { return } // Do not stream items that do not belong in this subscription
switch nostrResponse {
case .event(_, let nostrEvent):
if seenEvents.contains(nostrEvent.id) { break } // Don't send two of the same events.
continuation.yield(with: .success(.event(nostrEvent)))
seenEvents.insert(nostrEvent.id)
case .notice(let note):
break // We do not support handling these yet
case .eose(_):
relaysWhoFinishedInitialResults.insert(relayUrl)
if relaysWhoFinishedInitialResults == Set(desiredRelays) {
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
}
}
}, to: desiredRelays)
Task {
try? await Task.sleep(nanoseconds: 1_000_000_000 * UInt64(eoseTimeout))
if !eoseSent { continuation.yield(with: .success(.eose)) }
}
continuation.onTermination = { @Sendable _ in
self.unsubscribe(sub_id: sub_id, to: desiredRelays)
self.remove_handler(sub_id: sub_id)
}
}
}
enum StreamItem {
/// A Nostr event
case event(NostrEvent)
/// The "end of stored events" signal
case eose
}
func subscribe_to(sub_id: String, filters: [NostrFilter], to: [RelayURL]?, handler: @escaping (RelayURL, NostrConnectionEvent) -> ()) {
register_handler(sub_id: sub_id, handler: handler)
@@ -243,19 +303,19 @@ class RelayPool {
func send_raw(_ req: NostrRequestType, to: [RelayURL]? = nil, skip_ephemeral: Bool = true) {
let relays = to.map{ get_relays($0) } ?? self.relays
self.send_raw_to_local_ndb(req)
self.send_raw_to_local_ndb(req) // Always send Nostr events and data to NostrDB for a local copy
for relay in relays {
if req.is_read && !(relay.descriptor.info.read ?? true) {
continue
if req.is_read && !(relay.descriptor.info.canRead) {
continue // Do not send read requests to relays that are not READ relays
}
if req.is_write && !(relay.descriptor.info.write ?? true) {
continue
if req.is_write && !(relay.descriptor.info.canWrite) {
continue // Do not send write requests to relays that are not WRITE relays
}
if relay.descriptor.ephemeral && skip_ephemeral {
continue
continue // Do not send requests to ephemeral relays if we want to skip them
}
guard relay.connection.isConnected else {
@@ -354,7 +414,7 @@ class RelayPool {
}
func add_rw_relay(_ pool: RelayPool, _ url: RelayURL) {
try? pool.add_relay(RelayDescriptor(url: url, info: .rw))
try? pool.add_relay(RelayPool.RelayDescriptor(url: url, info: .readWrite))
}
+1 -4
View File
@@ -83,8 +83,7 @@ var test_damus_state: DamusState = ({
let our_pubkey = test_pubkey
let pool = RelayPool(ndb: ndb)
let settings = UserSettingsStore()
let damus = DamusState(pool: pool,
keypair: test_keypair,
let damus = DamusState(keypair: test_keypair,
likes: .init(our_pubkey: our_pubkey),
boosts: .init(our_pubkey: our_pubkey),
contacts: .init(our_pubkey: our_pubkey),
@@ -100,8 +99,6 @@ var test_damus_state: DamusState = ({
drafts: .init(),
events: .init(ndb: ndb),
bookmarks: .init(pubkey: our_pubkey),
postbox: .init(pool: pool),
bootstrap_relays: .init(),
replies: .init(our_pubkey: our_pubkey),
wallet: .init(settings: settings),
nav: .init(),
+1 -1
View File
@@ -354,7 +354,7 @@ func preload_image(url: URL) {
//print("Preloading image \(url.absoluteString)")
KingfisherManager.shared.retrieveImage(with: Kingfisher.ImageResource(downloadURL: url)) { val in
KingfisherManager.shared.retrieveImage(with: Kingfisher.KF.ImageResource(downloadURL: url)) { val in
//print("Preloaded image \(url.absoluteString)")
}
}
+11 -6
View File
@@ -52,7 +52,7 @@ extension KFOptionSetter {
func onFailure(fallbackUrl: URL?, cacheKey: String?) -> Self {
guard let url = fallbackUrl, let key = cacheKey else { return self }
let imageResource = Kingfisher.ImageResource(downloadURL: url, cacheKey: key)
let imageResource = Kingfisher.KF.ImageResource(downloadURL: url, cacheKey: key)
let source = imageResource.convertToSource()
options.alternativeSources = [source]
@@ -159,20 +159,25 @@ struct CustomCacheSerializer: CacheSerializer {
}
}
class CustomSessionDelegate: SessionDelegate {
override func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
class CustomSessionDelegate: SessionDelegate, @unchecked Sendable {
override func urlSession(
_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive response: URLResponse
) async -> URLSession.ResponseDisposition {
let contentLength = response.expectedContentLength
// Content-Length header is optional (-1 when missing)
if (contentLength != -1 && contentLength > MAX_FILE_SIZE) {
return super.urlSession(session, dataTask: dataTask, didReceive: URLResponse(), completionHandler: completionHandler)
return await super.urlSession(session, dataTask: dataTask, didReceive: URLResponse())
}
super.urlSession(session, dataTask: dataTask, didReceive: response, completionHandler: completionHandler)
return await super.urlSession(session, dataTask: dataTask, didReceive: response)
}
}
class CustomImageDownloader: ImageDownloader {
class CustomImageDownloader: ImageDownloader, @unchecked Sendable {
static let shared = CustomImageDownloader(name: "shared")
+1 -1
View File
@@ -54,7 +54,7 @@ enum CancelSendErr {
}
class PostBox {
let pool: RelayPool
private let pool: RelayPool
var events: [NoteId: PostedEvent]
init(pool: RelayPool) {
+7
View File
@@ -7,6 +7,13 @@
import Foundation
/// Stores information, metadata, and logs about different relays. Generally used as a singleton.
///
/// # Discussion
///
/// This class is primarily used as a shared singleton in `DamusState`, to allow other parts of the app to access information, metadata, and logs about relays without having to fetch it themselves.
///
/// For example, it is used by `RelayView` to supplement information about the relay without having to fetch those again from the network, as well as to display logs collected throughout the use of the app.
final class RelayModelCache: ObservableObject {
private var models = [RelayURL: RelayModel]()
+2 -2
View File
@@ -126,7 +126,7 @@ enum Route: Hashable {
case .FollowersYouKnow(let friendedFollowers, let followers):
FollowersYouKnowView(damus_state: damusState, friended_followers: friendedFollowers, followers: followers)
case .Script(let load_model):
LoadScript(pool: damusState.pool, model: load_model)
LoadScript(pool: damusState.nostrNetwork.pool, model: load_model)
}
}
@@ -209,7 +209,7 @@ enum Route: Hashable {
case .Search(let search):
hasher.combine("search")
hasher.combine(search.search)
case .NDBSearch(let results):
case .NDBSearch:
hasher.combine("results")
case .EULA:
hasher.combine("eula")
+1 -1
View File
@@ -270,7 +270,7 @@ struct EventActionBar: View {
generator.impactOccurred()
damus_state.postbox.send(like_ev)
damus_state.nostrNetwork.postbox.send(like_ev)
}
// MARK: Helper structures
+1 -1
View File
@@ -25,7 +25,7 @@ struct RepostAction: View {
return
}
damus_state.postbox.send(boost)
damus_state.nostrNetwork.postbox.send(boost)
} label: {
Label(NSLocalizedString("Repost", comment: "Button to repost a note"), image: "repost")
.frame(maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .leading)
+20 -24
View File
@@ -15,6 +15,8 @@ struct AddRelayView: View {
@Environment(\.dismiss) var dismiss
typealias UpdateError = NostrNetworkManager.UserRelayListManager.UpdateError
var body: some View {
VStack {
Text("Add relay", comment: "Title text to indicate user to an add a relay.")
@@ -82,38 +84,21 @@ struct AddRelayView: View {
new_relay = "wss://" + new_relay
}
guard let url = RelayURL(new_relay),
let ev = state.contacts.event,
let keypair = state.keypair.to_full() else {
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
}
let info = RelayInfo.rw
let descriptor = RelayDescriptor(url: url, info: info)
do {
try state.pool.add_relay(descriptor)
try state.nostrNetwork.userRelayList.insert(relay: NIP65.RelayList.RelayItem(url: url, rwConfiguration: .readWrite))
relayAddErrorTitle = nil // Clear error title
relayAddErrorMessage = nil // Clear error message
} catch RelayError.RelayAlreadyExists {
relayAddErrorTitle = NSLocalizedString("Duplicate relay", comment: "Title of the duplicate relay error message.")
relayAddErrorMessage = NSLocalizedString("The relay you are trying to add is already added.\nYou're all set!", comment: "An error message that appears when the user attempts to add a relay that has already been added.")
return
} catch {
return
}
catch {
present_sheet(.error(self.humanReadableError(for: error)))
}
state.pool.connect(to: [url])
if let new_ev = add_relay(ev: ev, keypair: keypair, current_relays: state.pool.our_descriptors, relay: url, info: info) {
process_contact_event(state: state, ev: ev)
state.pool.send(.event(new_ev))
}
if let relay_metadata = make_relay_metadata(relays: state.pool.our_descriptors, keypair: keypair) {
state.postbox.send(relay_metadata)
}
new_relay = ""
this_app.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
@@ -134,6 +119,17 @@ struct AddRelayView: View {
}
.padding()
}
func humanReadableError(for error: any Error) -> ErrorView.UserPresentableError {
guard let error = error as? UpdateError else {
return .init(
user_visible_description: NSLocalizedString("An unknown error occurred while adding a relay.", comment: "Title of an unknown relay error message."),
tip: NSLocalizedString("Please contact support.", comment: "Tip for an unknown relay error message."),
technical_info: error.localizedDescription
)
}
return error.humanReadableError
}
}
// TODO
+1 -1
View File
@@ -244,7 +244,7 @@ struct ChatEventView: View {
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.impactOccurred()
damus_state.postbox.send(like_ev)
damus_state.nostrNetwork.postbox.send(like_ev)
}
var action_bar: some View {
+2 -2
View File
@@ -161,7 +161,7 @@ struct ConfigView: View {
}
.navigationTitle(NSLocalizedString("Settings", comment: "Navigation title for Settings view."))
.navigationBarTitleDisplayMode(.large)
.searchable(text: $searchText,prompt: "Search within settings")
.searchable(text: $searchText, prompt: NSLocalizedString("Search within settings", comment: "Text to prompt the user to search settings."))
.alert(NSLocalizedString("WARNING:\n\nTHIS WILL SIGN AN EVENT THAT DELETES THIS ACCOUNT.\n\nYOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.\n\n ARE YOU SURE YOU WANT TO CONTINUE?", comment: "Alert for deleting the users account."), isPresented: $delete_account_warning) {
Button(NSLocalizedString("Cancel", comment: "Cancel deleting the user."), role: .cancel) {
@@ -182,7 +182,7 @@ struct ConfigView: View {
let ev = created_deleted_account_profile(keypair: keypair) else {
return
}
state.postbox.send(ev)
state.nostrNetwork.postbox.send(ev)
logout(state)
}
}
+1 -1
View File
@@ -138,7 +138,7 @@ struct DMChatView: View, KeyboardReadable {
dms.draft = ""
damus_state.postbox.send(dm)
damus_state.nostrNetwork.postbox.send(dm)
handle_incoming_dm(ev: dm, our_pubkey: damus_state.pubkey, dms: damus_state.dms, prev_events: NewEventsBits())
+3 -3
View File
@@ -24,12 +24,12 @@ struct EventLoaderView<Content: View>: View {
}
func unsubscribe() {
damus_state.pool.unsubscribe(sub_id: subscription_uuid)
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subscription_uuid)
}
func subscribe(filters: [NostrFilter]) {
damus_state.pool.register_handler(sub_id: subscription_uuid, handler: handle_event)
damus_state.pool.send(.subscribe(.init(filters: filters, sub_id: subscription_uuid)))
damus_state.nostrNetwork.pool.register_handler(sub_id: subscription_uuid, handler: handle_event)
damus_state.nostrNetwork.pool.send(.subscribe(.init(filters: filters, sub_id: subscription_uuid)))
}
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
+1 -1
View File
@@ -113,7 +113,7 @@ struct MenuItems: View {
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)) {
damus_state.mutelist_manager.set_mutelist(new_mutelist_ev)
damus_state.postbox.send(new_mutelist_ev)
damus_state.nostrNetwork.postbox.send(new_mutelist_ev)
}
let muted = damus_state.mutelist_manager.is_event_muted(event)
isMutedThread = muted
+1 -1
View File
@@ -74,7 +74,7 @@ class LoadableNostrEventViewModel: ObservableObject {
case .zap, .zap_request:
guard let zap = await get_zap(from: ev, state: damus_state) else { return .not_found }
return .loaded(route: Route.Zaps(target: zap.target))
case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .zap, .zap_request, .nwc_request, .nwc_response, .http_auth, .status:
case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .nwc_request, .nwc_response, .http_auth, .status, .relay_list:
return .unknown_or_unsupported_kind
}
case .naddr(let naddr):
+1 -1
View File
@@ -87,7 +87,7 @@ struct AddMuteItemView: View {
}
state.mutelist_manager.set_mutelist(mutelist)
state.postbox.send(mutelist)
state.nostrNetwork.postbox.send(mutelist)
}
new_text = ""
+1 -1
View File
@@ -30,7 +30,7 @@ struct MutelistView: View {
}
damus_state.mutelist_manager.set_mutelist(new_ev)
damus_state.postbox.send(new_ev)
damus_state.nostrNetwork.postbox.send(new_ev)
updateMuteItems()
} label: {
Label(NSLocalizedString("Delete", comment: "Button to remove a user from their mutelist."), image: "delete")
@@ -86,10 +86,10 @@ struct DamusAppNotificationView: View {
Task {
do {
let url = try await damus_state.purple.generate_verified_ln_checkout_link(product_template_name: product_template_name)
await self.open_url(url: url)
self.open_url(url: url)
}
catch {
await self.open_url(url: damus_state.purple.environment.purple_landing_page_url().appendingPathComponent("checkout"))
self.open_url(url: damus_state.purple.environment.purple_landing_page_url().appendingPathComponent("checkout"))
}
}
}
@@ -41,7 +41,10 @@ class NotificationFilter: ObservableObject, Equatable {
if let item = item.filter({ ev in
self.friend_filter.filter(contacts: contacts, pubkey: ev.pubkey) &&
(!hellthread_notifications_disabled || !ev.is_hellthread(max_pubkeys: hellthread_notification_max_pubkeys))
(!hellthread_notifications_disabled || !ev.is_hellthread(max_pubkeys: hellthread_notification_max_pubkeys)) &&
// Allow notes that are created no more than 3 seconds in the future
// to account for natural clock skew between sender and receiver.
ev.age >= -3
}) {
acc.append(item)
}
@@ -77,7 +77,7 @@ class SuggestedUsersViewModel: ObservableObject {
private func subscribeToSuggestedProfiles(pubkeys: [Pubkey]) {
let filter = NostrFilter(kinds: [.metadata], authors: pubkeys)
damus_state.pool.subscribe(sub_id: sub_id, filters: [filter], handler: handle_event)
damus_state.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: [filter], handler: handle_event)
}
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
+3 -1
View File
@@ -763,7 +763,7 @@ func load_draft_for_post(drafts: Drafts, action: PostAction) -> DraftArtifacts?
}
// If there are no exact matches to the highlight, try to load a draft for the same highlight source
// We do this to improve UX, because we don't want to leave the post view blank if they only selected a slightly different piece of text from before.
var other_matches = drafts.highlights
let other_matches = drafts.highlights
.filter { $0.key.source == highlight.source }
// It's not an exact match, so there is no way of telling which one is the preferred draft. So just load the first one we found.
return other_matches.first?.value
@@ -883,6 +883,8 @@ func build_post(state: DamusState, post: NSAttributedString, action: PostAction,
case .quoting(let ev):
content.append("\n\nnostr:" + bech32_note_id(ev.id))
tags.append(["q", ev.id.hex()]);
if let quoted_ev = state.events.lookup(ev.id) {
tags.append(["p", quoted_ev.pubkey.hex()])
}
+1 -1
View File
@@ -65,7 +65,7 @@ struct EditMetadataView: View {
return
}
damus_state.postbox.send(metadata_ev)
damus_state.nostrNetwork.postbox.send(metadata_ev)
}
func is_ln_valid(ln: String) -> Bool {
+1 -1
View File
@@ -287,7 +287,7 @@ struct EditPictureControl: View {
var accessibility_value: String? {
if style.first_time_setup {
if let current_image_url = model.current_image_url {
if model.current_image_url != nil {
switch self.model.context {
case .normal:
return NSLocalizedString("Image is setup", comment: "Accessibility value on image control")
+3 -1
View File
@@ -67,7 +67,9 @@ struct ProfileName: View {
}
func name_choice(profile: Profile?) -> String {
return prefix == "@" ? current_display_name(profile: profile).username.truncate(maxLength: 50) : current_display_name(profile: profile).displayName.truncate(maxLength: 50)
let displayName = current_display_name(profile: profile)
let untruncatedName = prefix == "@" ? displayName.username : displayName.displayName
return untruncatedName.truncate(maxLength: 50)
}
func onlyzapper(profile: Profile?) -> Bool {
+5 -5
View File
@@ -219,7 +219,7 @@ struct ProfileView: View {
}
damus_state.mutelist_manager.set_mutelist(new_ev)
damus_state.postbox.send(new_ev)
damus_state.nostrNetwork.postbox.send(new_ev)
}
} else {
Button(NSLocalizedString("Mute", comment: "Button to mute a profile"), role: .destructive) {
@@ -396,18 +396,18 @@ struct ProfileView: View {
}
}
if let relays = profile.relays {
if let relays = profile.relay_urls {
// Only open relay config view if the user is logged in with private key and they are looking at their own profile.
let noun_string = pluralizedString(key: "relays_count", count: relays.keys.count)
let noun_string = pluralizedString(key: "relays_count", count: relays.count)
let noun_text = Text(noun_string).font(.subheadline).foregroundColor(.gray)
let relay_text = Text("\(Text(verbatim: relays.keys.count.formatted()).font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.")
let relay_text = Text("\(Text(verbatim: relays.count.formatted()).font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.")
if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user {
NavigationLink(value: Route.RelayConfig) {
relay_text
}
.buttonStyle(PlainButtonStyle())
} else {
NavigationLink(value: Route.UserRelays(relays: Array(relays.keys).sorted())) {
NavigationLink(value: Route.UserRelays(relays: relays.sorted())) {
relay_text
}
.buttonStyle(PlainButtonStyle())
+1 -1
View File
@@ -297,7 +297,7 @@ fileprivate struct ProfileActionSheetZapButton: View {
.foregroundColor(Color.primary)
.profile_button_style(scheme: colorScheme)
case .zap_success:
Image("checkmark")
Image("checkmark-damus")
.foregroundColor(Color.green)
.profile_button_style(scheme: colorScheme)
case .zap_failure:
+10 -7
View File
@@ -57,11 +57,14 @@ struct QRScanNSECView: View {
}
}
#Preview {
@State var showQR = true
@State var privKeyFound = false
@State var shouldSaveKey = true
return QRScanNSECView(showQR: $showQR,
privKeyFound: $privKeyFound,
codeScannerCompletion: { _ in })
struct QRScanNSECView_Previews: PreviewProvider {
@State static var showQR = true
@State static var privKeyFound = false
@State static var shouldSaveKey = true
static var previews: some View {
QRScanNSECView(showQR: $showQR,
privKeyFound: $privKeyFound,
codeScannerCompletion: { _ in })
}
}
+3 -3
View File
@@ -15,11 +15,11 @@ struct RelayFilterView: View {
self.state = state
self.timeline = timeline
//_relays = State(initialValue: state.pool.descriptors)
//_relays = State(initialValue: state.networkManager.pool.descriptors)
}
var relays: [RelayDescriptor] {
return state.pool.our_descriptors
var relays: [RelayPool.RelayDescriptor] {
return state.nostrNetwork.pool.our_descriptors
}
var body: some View {
+7 -7
View File
@@ -23,7 +23,7 @@ enum RelayTab: Int, CaseIterable{
struct RelayConfigView: View {
let state: DamusState
@State var relays: [RelayDescriptor]
@State var relays: [RelayPool.RelayDescriptor]
@State private var showActionButtons = false
@State var show_add_relay: Bool = false
@State var selectedTab = 0
@@ -32,15 +32,15 @@ struct RelayConfigView: View {
init(state: DamusState) {
self.state = state
_relays = State(initialValue: state.pool.our_descriptors)
_relays = State(initialValue: state.nostrNetwork.pool.our_descriptors)
UITabBar.appearance().isHidden = true
}
var recommended: [RelayDescriptor] {
let rs: [RelayDescriptor] = []
var recommended: [RelayPool.RelayDescriptor] {
let rs: [RelayPool.RelayDescriptor] = []
let recommended_relay_addresses = get_default_bootstrap_relays()
return recommended_relay_addresses.reduce(into: rs) { xs, x in
xs.append(RelayDescriptor(url: x, info: .rw))
xs.append(RelayPool.RelayDescriptor(url: x, info: .readWrite))
}
}
@@ -98,7 +98,7 @@ struct RelayConfigView: View {
}
}
.onReceive(handle_notify(.relays_changed)) { _ in
self.relays = state.pool.our_descriptors
self.relays = state.nostrNetwork.pool.our_descriptors
}
.onAppear {
notify(.display_tabbar(false))
@@ -109,7 +109,7 @@ struct RelayConfigView: View {
.ignoresSafeArea(.all)
}
func RelayList(title: String, relayList: [RelayDescriptor], recommended: Bool) -> some View {
func RelayList(title: String, relayList: [RelayPool.RelayDescriptor], recommended: Bool) -> some View {
ScrollView(showsIndicators: false) {
HStack {
Text(title)
+25 -37
View File
@@ -25,32 +25,12 @@ struct RelayDetailView: View {
}
func check_connection() -> Bool {
for relay in state.pool.relays {
if relay.id == self.relay {
return true
}
}
return false
return state.nostrNetwork.userRelayList.getUserCurrentRelayList()?.relays.keys.contains(self.relay) == true
}
func RemoveRelayButton(_ keypair: FullKeypair) -> some View {
Button(action: {
guard let ev = state.contacts.event else {
return
}
let descriptors = state.pool.our_descriptors
guard let new_ev = remove_relay( ev: ev, current_relays: descriptors, keypair: keypair, relay: relay) else {
return
}
process_contact_event(state: state, ev: new_ev)
state.postbox.send(new_ev)
if let relay_metadata = make_relay_metadata(relays: state.pool.our_descriptors, keypair: keypair) {
state.postbox.send(relay_metadata)
}
dismiss()
self.removeRelay()
}) {
HStack {
Text("Disconnect", comment: "Button to disconnect from the relay.")
@@ -63,19 +43,7 @@ struct RelayDetailView: View {
func ConnectRelayButton(_ keypair: FullKeypair) -> some View {
Button(action: {
guard let ev_before_add = state.contacts.event else {
return
}
guard let ev_after_add = add_relay(ev: ev_before_add, keypair: keypair, current_relays: state.pool.our_descriptors, relay: relay, info: .rw) else {
return
}
process_contact_event(state: state, ev: ev_after_add)
state.postbox.send(ev_after_add)
if let relay_metadata = make_relay_metadata(relays: state.pool.our_descriptors, keypair: keypair) {
state.postbox.send(relay_metadata)
}
dismiss()
self.connectRelay()
}) {
HStack {
Text("Connect", comment: "Button to connect to the relay.")
@@ -208,13 +176,33 @@ struct RelayDetailView: View {
}
}
private var relay_object: Relay? {
state.pool.get_relay(relay)
private var relay_object: RelayPool.Relay? {
state.nostrNetwork.pool.get_relay(relay)
}
private var relay_connection: RelayConnection? {
relay_object?.connection
}
func removeRelay() {
do {
try state.nostrNetwork.userRelayList.remove(relayURL: self.relay)
dismiss()
}
catch {
present_sheet(.error(error.humanReadableError))
}
}
func connectRelay() {
do {
try state.nostrNetwork.userRelayList.insert(relay: NIP65.RelayList.RelayItem(url: relay, rwConfiguration: .readWrite))
dismiss()
}
catch {
present_sheet(.error(error.humanReadableError))
}
}
}
struct RelayDetailView_Previews: PreviewProvider {
+1 -1
View File
@@ -56,7 +56,7 @@ struct RelayStatusView: View {
struct RelayStatusView_Previews: PreviewProvider {
static var previews: some View {
let connection = test_damus_state.pool.get_relay(RelayURL("wss://relay.damus.io")!)!.connection
let connection = test_damus_state.nostrNetwork.pool.get_relay(RelayURL("wss://relay.damus.io")!)!.connection
RelayStatusView(connection: connection)
}
}
+1 -1
View File
@@ -36,7 +36,7 @@ struct RelayToggle: View {
}
private var relay_connection: RelayConnection? {
state.pool.get_relay(relay_id)?.connection
state.nostrNetwork.pool.get_relay(relay_id)?.connection
}
}
+16 -32
View File
@@ -22,7 +22,7 @@ struct RelayView: View {
self.recommended = recommended
self.model_cache = state.relay_model_cache
_showActionButtons = showActionButtons
let relay_state = RelayView.get_relay_state(pool: state.pool, relay: relay)
let relay_state = RelayView.get_relay_state(pool: state.nostrNetwork.pool, relay: relay)
self._relay_state = State(initialValue: relay_state)
}
@@ -80,7 +80,7 @@ struct RelayView: View {
AddButton(keypair: keypair)
} else {
Button(action: {
remove_action(privkey: keypair.privkey)
Task { await remove_action(privkey: keypair.privkey) }
}) {
Text("Added", comment: "Button to show relay server is already added to list.")
.font(.caption)
@@ -105,7 +105,7 @@ struct RelayView: View {
.contentShape(Rectangle())
}
.onReceive(handle_notify(.relays_changed)) { _ in
self.relay_state = RelayView.get_relay_state(pool: state.pool, relay: self.relay)
self.relay_state = RelayView.get_relay_state(pool: state.nostrNetwork.pool, relay: self.relay)
}
.onTapGesture {
state.nav.push(route: Route.RelayDetail(relay: relay, metadata: model_cache.model(with_relay_id: relay)?.metadata))
@@ -113,46 +113,30 @@ struct RelayView: View {
}
private var relay_connection: RelayConnection? {
state.pool.get_relay(relay)?.connection
state.nostrNetwork.pool.get_relay(relay)?.connection
}
func add_action(keypair: FullKeypair) {
guard let ev_before_add = state.contacts.event else {
return
func add_action(keypair: FullKeypair) async {
do {
try await state.nostrNetwork.userRelayList.insert(relay: NIP65.RelayList.RelayItem(url: relay, rwConfiguration: .readWrite))
}
guard let ev_after_add = add_relay(ev: ev_before_add, keypair: keypair, current_relays: state.pool.our_descriptors, relay: relay, info: .rw) else {
return
}
process_contact_event(state: state, ev: ev_after_add)
state.postbox.send(ev_after_add)
if let relay_metadata = make_relay_metadata(relays: state.pool.our_descriptors, keypair: keypair) {
state.postbox.send(relay_metadata)
catch {
present_sheet(.error(error.humanReadableError))
}
}
func remove_action(privkey: Privkey) {
guard let ev = state.contacts.event else {
return
func remove_action(privkey: Privkey) async {
do {
try await state.nostrNetwork.userRelayList.remove(relayURL: relay)
}
let descriptors = state.pool.our_descriptors
guard let keypair = state.keypair.to_full(),
let new_ev = remove_relay(ev: ev, current_relays: descriptors, keypair: keypair, relay: relay) else {
return
}
process_contact_event(state: state, ev: new_ev)
state.postbox.send(new_ev)
if let relay_metadata = make_relay_metadata(relays: state.pool.our_descriptors, keypair: keypair) {
state.postbox.send(relay_metadata)
catch {
present_sheet(.error(error.humanReadableError))
}
}
func AddButton(keypair: FullKeypair) -> some View {
Button(action: {
add_action(keypair: keypair)
Task { await add_action(keypair: keypair) }
}) {
Text("Add", comment: "Button to add relay server to list.")
.font(.caption)
@@ -170,7 +154,7 @@ struct RelayView: View {
func RemoveButton(privkey: Privkey, showText: Bool) -> some View {
Button(action: {
remove_action(privkey: privkey)
Task { await remove_action(privkey: privkey) }
}) {
if showText {
Text("Disconnect", comment: "Button to disconnect from a relay server.")
+2 -2
View File
@@ -132,9 +132,9 @@ struct ReportView_Previews: PreviewProvider {
let ds = test_damus_state
VStack {
ReportView(postbox: ds.postbox, target: ReportTarget.user(test_pubkey), keypair: test_keypair.to_full()!)
ReportView(postbox: ds.nostrNetwork.postbox, target: ReportTarget.user(test_pubkey), keypair: test_keypair.to_full()!)
ReportView(postbox: ds.postbox, target: ReportTarget.user(test_pubkey), keypair: test_keypair.to_full()!, report_sent: true, report_id: "report_id")
ReportView(postbox: ds.nostrNetwork.postbox, target: ReportTarget.user(test_pubkey), keypair: test_keypair.to_full()!, report_sent: true, report_id: "report_id")
}
}
+14 -2
View File
@@ -20,10 +20,12 @@ struct SaveKeysView: View {
@FocusState var privkey_focused: Bool
let first_contact_event: NdbNote?
let first_relay_list_event: NdbNote?
init(account: CreateAccountModel) {
self.account = account
self.first_contact_event = make_first_contact_event(keypair: account.keypair)
self.first_relay_list_event = NIP65.RelayList(relays: get_default_bootstrap_relays()).toNostrEvent(keypair: account.full_keypair)
}
var body: some View {
@@ -128,8 +130,12 @@ struct SaveKeysView: View {
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
}
guard let first_relay_list_event else {
error = NSLocalizedString("Could not create your initial relay list. 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 relay list failed to be created.")
return
}
// Save contact list to storage right away so that we don't need to depend on the network to complete this important step
self.save_to_storage(first_contact_event: first_contact_event, for: account)
self.save_to_storage(first_contact_event: first_contact_event, first_relay_list_event: first_relay_list_event, for: account)
let bootstrap_relays = load_bootstrap_relays(pubkey: account.pubkey)
for relay in bootstrap_relays {
@@ -143,13 +149,15 @@ struct SaveKeysView: View {
self.pool.connect()
}
func save_to_storage(first_contact_event: NdbNote, for account: CreateAccountModel) {
func save_to_storage(first_contact_event: NdbNote, first_relay_list_event: NdbNote, for account: CreateAccountModel) {
// Send to NostrDB so that we have a local copy in storage
self.pool.send_raw_to_local_ndb(.typical(.event(first_contact_event)))
self.pool.send_raw_to_local_ndb(.typical(.event(first_relay_list_event)))
// Save the ID to user settings so that we can easily find it later.
let settings = UserSettingsStore.globally_load_for(pubkey: account.pubkey)
settings.latest_contact_event_id_hex = first_contact_event.id.hex()
settings.latestRelayListEventIdHex = first_relay_list_event.id.hex()
}
func handle_event(relay: RelayURL, ev: NostrConnectionEvent) {
@@ -168,6 +176,10 @@ struct SaveKeysView: View {
self.pool.send(.event(first_contact_event))
}
if let first_relay_list_event {
self.pool.send(.event(first_relay_list_event))
}
do {
try save_keypair(pubkey: account.pubkey, privkey: account.privkey)
notify(.login(account.keypair))
+2 -2
View File
@@ -69,7 +69,7 @@ struct SearchView: View {
}
appstate.mutelist_manager.set_mutelist(mutelist)
appstate.postbox.send(mutelist)
appstate.nostrNetwork.postbox.send(mutelist)
} 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.")
}
@@ -104,7 +104,7 @@ struct SearchView: View {
}
appstate.mutelist_manager.set_mutelist(mutelist)
appstate.postbox.send(mutelist)
appstate.nostrNetwork.postbox.send(mutelist)
}
var described_search: DescribedSearch {
+132 -56
View File
@@ -10,72 +10,148 @@ import SwiftUI
struct FirstAidSettingsView: View {
let damus_state: DamusState
@ObservedObject var settings: UserSettingsStore
@State var reset_contact_list_state: ContactListResetState = .not_started
enum ContactListResetState: Equatable {
case not_started
case confirming_with_user
case error(String)
case in_progress
case completed
}
@State var contactListInitiallyPresent: Bool = true
@State var relayListInitiallyPresent: Bool = true
var body: some View {
Form {
if damus_state.contacts.event == nil {
Section(
header: Text(NSLocalizedString("Contact list (Follows + Relay list)", comment: "Section title for Contact list first aid tools")),
footer: Text(NSLocalizedString("No contact list was found. You might experience issues using the app. If you suspect you have permanently lost your contact list (or if you never had one), you can fix this by resetting it", comment: "Section footer for Contact list first aid tools"))
) {
Button(action: {
reset_contact_list_state = .confirming_with_user
}, label: {
HStack(spacing: 6) {
switch reset_contact_list_state {
case .not_started, .error:
Label(NSLocalizedString("Reset contact list", comment: "Button to reset contact list."), image: "broom")
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundColor(.red)
case .confirming_with_user, .in_progress:
ProgressView()
Text(NSLocalizedString("In progress…", comment: "Loading message indicating that a contact list reset operation is in progress."))
case .completed:
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text(NSLocalizedString("Contact list has been reset", comment: "Message indicating that the contact list was successfully reset."))
}
}
})
.disabled(reset_contact_list_state == .in_progress || reset_contact_list_state == .completed)
if case let .error(error_message) = reset_contact_list_state {
Text(error_message)
.foregroundStyle(.red)
if !contactListInitiallyPresent {
ItemResetSection(
damus_state: self.damus_state,
settings: self.settings,
itemName: NSLocalizedString("Contact list", comment: "Section title for Contact list first aid tools"),
hintMessage: NSLocalizedString(
"No contact list was found. You might experience issues using the app. If you suspect you have permanently lost your contact list (or if you never had one), you can fix this by resetting it",
comment: "Section footer for Contact list first aid tools"
),
resetButtonLabel: NSLocalizedString("Reset contact list", comment: "Button to reset contact list."),
warningMessage: NSLocalizedString(
"WARNING:\n\nThis will reset your contact list, including the list of everyone you follow and potentially the list of all relays you usually connect to. ONLY PROCEED IF YOU ARE SURE YOU HAVE LOST YOUR CONTACT LIST BEYOND RECOVERABILITY.",
comment: "Alert for resetting the user's contact list."),
successMessage: NSLocalizedString("Contact list has been reset", comment: "Message indicating that the contact list was successfully reset."),
performOperation: {
try await self.resetContactList()
}
}
.alert(NSLocalizedString("WARNING:\n\nThis will reset your contact list, including the list of everyone you follow and the list of all relays you usually connect to. ONLY PROCEED IF YOU ARE SURE YOU HAVE LOST YOUR CONTACT LIST BEYOND RECOVERABILITY.", comment: "Alert for resetting the user's contact list."),
isPresented: Binding(get: { reset_contact_list_state == .confirming_with_user }, set: { _ in return })
) {
Button(NSLocalizedString("Cancel", comment: "Cancel resetting the contact list."), role: .cancel) {
reset_contact_list_state = .not_started
}
Button(NSLocalizedString("Continue", comment: "Continue with resetting the contact list.")) {
guard let new_contact_list_event = make_first_contact_event(keypair: damus_state.keypair) else {
reset_contact_list_state = .error(NSLocalizedString("An unexpected error happened while trying to create the new contact list. Please contact support.", comment: "Error message for a failed contact list reset operation"))
return
}
damus_state.pool.send(.event(new_contact_list_event))
reset_contact_list_state = .completed
}
}
)
}
if damus_state.contacts.event != nil {
if !relayListInitiallyPresent {
ItemResetSection(
damus_state: self.damus_state,
settings: self.settings,
itemName: NSLocalizedString("Relay list", comment: "Section title for Relay list first aid tools"),
hintMessage: NSLocalizedString(
"No relay list was found. You might experience issues using the app. If you suspect you have permanently lost your relay list (or if you never had one), you can fix this by resetting it",
comment: "Section footer for relay list first aid tools"
),
resetButtonLabel: NSLocalizedString("Repair relay list", comment: "Button to repair relay list."),
warningMessage: NSLocalizedString("WARNING:\n\nThis will attempt to repair your relay list based on other information we have. You may lose any relays you have added manually. Only proceed if you have lost your relay list beyond recoverability or if you are ok with losing any manually added relays.", comment: "Alert for repairing the user's relay list."),
successMessage: NSLocalizedString("Relay list has been repaired", comment: "Message indicating that the relay list was successfully repaired."),
performOperation: {
try await self.resetRelayList()
}
)
}
if contactListInitiallyPresent && contactListInitiallyPresent {
Text("We did not detect any issues that we can automatically fix for you. If you are having issues, please contact Damus support: [support@damus.io](mailto:support@damus.io)", comment: "Message indicating that no First Aid actions are available.")
}
}
.navigationTitle(NSLocalizedString("First Aid", comment: "Navigation title for first aid settings and tools"))
.onAppear {
self.contactListInitiallyPresent = damus_state.contacts.event != nil
self.relayListInitiallyPresent = damus_state.nostrNetwork.userRelayList.getUserCurrentRelayList() != nil
}
}
func resetContactList() async throws {
guard let new_contact_list_event = make_first_contact_event(keypair: damus_state.keypair) else {
throw FirstAidError.cannotMakeFirstContactEvent
}
damus_state.nostrNetwork.pool.send(.event(new_contact_list_event))
damus_state.settings.latest_contact_event_id_hex = new_contact_list_event.id.hex()
}
func resetRelayList() async throws {
let bestEffortRelayList = damus_state.nostrNetwork.userRelayList.getBestEffortRelayList()
try damus_state.nostrNetwork.userRelayList.set(userRelayList: bestEffortRelayList)
}
enum FirstAidError: Error {
case cannotMakeFirstContactEvent
}
}
extension FirstAidSettingsView {
struct ItemResetSection: View {
let damus_state: DamusState
@ObservedObject var settings: UserSettingsStore
@State var reset_item_state: ItemResetState = .not_started
let itemName: String
let hintMessage: String
let resetButtonLabel: String
let warningMessage: String
let successMessage: String
var performOperation: () async throws -> Void
enum ItemResetState: Equatable {
case not_started
case confirming_with_user
case error(String)
case in_progress
case completed
}
var body: some View {
Section(
header: Text(itemName),
footer: Text(hintMessage)
) {
Button(action: {
reset_item_state = .confirming_with_user
}, label: {
HStack(spacing: 6) {
switch reset_item_state {
case .not_started, .error:
Label(resetButtonLabel, image: "broom")
.frame(maxWidth: .infinity, alignment: .leading)
.foregroundColor(.red)
case .confirming_with_user, .in_progress:
ProgressView()
Text(NSLocalizedString("In progress…", comment: "Loading message indicating that a first aid operation is in progress."))
case .completed:
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text(successMessage)
}
}
})
.disabled(reset_item_state == .in_progress || reset_item_state == .completed)
if case let .error(error_message) = reset_item_state {
Text(error_message)
.foregroundStyle(.red)
}
}
.alert(warningMessage, isPresented: Binding(get: { reset_item_state == .confirming_with_user }, set: { _ in return })
) {
Button(NSLocalizedString("Cancel", comment: "Cancel the user-requested operation."), role: .cancel) {
reset_item_state = .not_started
}
Button(NSLocalizedString("Continue", comment: "Continue with the user-requested operation.")) {
Task {
do {
try await performOperation()
reset_item_state = .completed
}
catch {
reset_item_state = .error(NSLocalizedString("An unexpected error happened while trying to perform this action. Please contact support.", comment: "Error message for a failed reset/repair operation"))
}
}
}
}
}
}
}
+3 -3
View File
@@ -16,13 +16,13 @@ struct UserRelaysView: View {
init(state: DamusState, relays: [RelayURL]) {
self.state = state
self.relays = relays
let relay_state = UserRelaysView.make_relay_state(pool: state.pool, relays: relays)
let relay_state = UserRelaysView.make_relay_state(state: state, relays: relays)
self._relay_state = State(initialValue: relay_state)
}
static func make_relay_state(pool: RelayPool, relays: [RelayURL]) -> [(RelayURL, Bool)] {
static func make_relay_state(state: DamusState, relays: [RelayURL]) -> [(RelayURL, Bool)] {
return relays.map({ r in
return (r, pool.get_relay(r) == nil)
return (r, state.nostrNetwork.pool.get_relay(r) == nil)
}).sorted { (a, b) in a.0 < b.0 }
}
+1 -1
View File
@@ -173,7 +173,7 @@ struct NWCSettings: View {
guard let meta = make_metadata_event(keypair: keypair, metadata: prof) else {
return
}
damus_state.postbox.send(meta)
damus_state.nostrNetwork.postbox.send(meta)
}
}
+2 -2
View File
@@ -84,8 +84,8 @@ struct WalletView: View {
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.pool, post: damus_state.postbox, delay: delay, on_flush: flusher)
WalletConnect.request_balance_information(url: nwc, pool: damus_state.pool, post: damus_state.postbox, delay: delay, on_flush: flusher)
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
}
}
Binary file not shown.
@@ -2,7 +2,7 @@
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
<file original="damus/en-US.lproj/InfoPlist.strings" source-language="en-US" target-language="en-US" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.3" build-num="16E140"/>
</header>
<body>
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
@@ -44,7 +44,7 @@
</file>
<file original="damus/en-US.lproj/Localizable.strings" source-language="en-US" target-language="en-US" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.3" build-num="16E140"/>
</header>
<body>
<trans-unit id="#%@" xml:space="preserve">
@@ -2292,6 +2292,16 @@ Button to save key, complete account creation, and start using the app.</note>
<note>Default title for the search screen when it is in an unknown state.
Title of the text field for searching.</note>
</trans-unit>
<trans-unit id="Search / Universe" xml:space="preserve">
<source>Search / Universe</source>
<target>Search / Universe</target>
<note>Section header for search/universe settings</note>
</trans-unit>
<trans-unit id="Search within settings" xml:space="preserve">
<source>Search within settings</source>
<target>Search within settings</target>
<note>Text to prompt the user to search settings.</note>
</trans-unit>
<trans-unit id="Search word: %@" xml:space="preserve">
<source>Search word: %@</source>
<target>Search word: %@</target>
@@ -2305,8 +2315,7 @@ Title of the text field for searching.</note>
<trans-unit id="Search/Universe" xml:space="preserve">
<source>Search/Universe</source>
<target>Search/Universe</target>
<note>Navigation title for universe/search settings.
Section header for search/universe settings</note>
<note>Navigation title for universe/search settings.</note>
</trans-unit>
<trans-unit id="Secret Account Login Key" xml:space="preserve">
<source>Secret Account Login Key</source>
@@ -2455,11 +2464,6 @@ Button to show more of a long profile description.</note>
<target>Sign In</target>
<note>Button to continue to login page.</note>
</trans-unit>
<trans-unit id="Sign Out" xml:space="preserve">
<source>Sign Out</source>
<target>Sign Out</target>
<note>Section title for signing out</note>
</trans-unit>
<trans-unit id="Sign in" xml:space="preserve">
<source>Sign in</source>
<target>Sign in</target>
@@ -3397,7 +3401,7 @@ String indicating that a given timestamp just occurred</note>
</file>
<file original="damus/en-US.lproj/Localizable.stringsdict" source-language="en-US" target-language="en-US" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.3" build-num="16E140"/>
</header>
<body>
<trans-unit id="/followed_by_three_and_others:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
@@ -3794,7 +3798,7 @@ String indicating that a given timestamp just occurred</note>
</file>
<file original="damus/InfoPlist.xcstrings" source-language="en-US" target-language="en-US" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.3" build-num="16E140"/>
</header>
<body>
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
@@ -3816,7 +3820,7 @@ String indicating that a given timestamp just occurred</note>
</file>
<file original="damus/Localizable.xcstrings" source-language="en-US" target-language="en-US" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.3" build-num="16E140"/>
</header>
<body>
<trans-unit id="" xml:space="preserve">
@@ -6057,6 +6061,16 @@ Button to save key, complete account creation, and start using the app.</note>
<note>Default title for the search screen when it is in an unknown state.
Title of the text field for searching.</note>
</trans-unit>
<trans-unit id="Search / Universe" xml:space="preserve">
<source>Search / Universe</source>
<target state="new">Search / Universe</target>
<note>Section header for search/universe settings</note>
</trans-unit>
<trans-unit id="Search within settings" xml:space="preserve">
<source>Search within settings</source>
<target state="new">Search within settings</target>
<note>Text to prompt the user to search settings.</note>
</trans-unit>
<trans-unit id="Search word: %@" xml:space="preserve">
<source>Search word: %@</source>
<target state="new">Search word: %@</target>
@@ -6070,8 +6084,7 @@ Title of the text field for searching.</note>
<trans-unit id="Search/Universe" xml:space="preserve">
<source>Search/Universe</source>
<target state="new">Search/Universe</target>
<note>Navigation title for universe/search settings.
Section header for search/universe settings</note>
<note>Navigation title for universe/search settings.</note>
</trans-unit>
<trans-unit id="Secret Account Login Key" xml:space="preserve">
<source>Secret Account Login Key</source>
@@ -6230,11 +6243,6 @@ Button to show more of a long profile description.</note>
<target state="new">Sign In</target>
<note>Button to continue to login page.</note>
</trans-unit>
<trans-unit id="Sign Out" xml:space="preserve">
<source>Sign Out</source>
<target state="new">Sign Out</target>
<note>Section title for signing out</note>
</trans-unit>
<trans-unit id="Sign in" xml:space="preserve">
<source>Sign in</source>
<target state="new">Sign in</target>
@@ -7037,7 +7045,7 @@ String indicating that a given timestamp just occurred</note>
</file>
<file original="damus/Resources/InfoPlist.xcstrings" source-language="en-US" target-language="en-US" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.3" build-num="16E140"/>
</header>
<body>
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
@@ -7059,7 +7067,7 @@ String indicating that a given timestamp just occurred</note>
</file>
<file original="highlighter action extension/InfoPlist.xcstrings" source-language="en-US" target-language="en-US" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.3" build-num="16E140"/>
</header>
<body>
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
@@ -1386,6 +1386,12 @@
"Search" : {
"comment" : "Default title for the search screen when it is in an unknown state.\nTitle of the text field for searching."
},
"Search / Universe" : {
"comment" : "Section header for search/universe settings"
},
"Search within settings" : {
"comment" : "Text to prompt the user to search settings."
},
"Search word: %@" : {
"comment" : "Navigation link to search for a word."
},
@@ -1393,7 +1399,7 @@
"comment" : "Placeholder text to prompt entry of search query."
},
"Search/Universe" : {
"comment" : "Navigation title for universe/search settings.\nSection header for search/universe settings"
"comment" : "Navigation title for universe/search settings."
},
"Secret Account Login Key" : {
"comment" : "Section title for user's secret account login key."
@@ -1494,9 +1500,6 @@
"Sign out" : {
"comment" : "Sidebar menu label to sign out of the account."
},
"Sign Out" : {
"comment" : "Section title for signing out"
},
"Skip" : {
"comment" : "Button to dismiss the suggested users screen"
},
@@ -1,14 +1,21 @@
/* Bundle display name */
"CFBundleDisplayName" = "Damus";
/* Bundle name */
"CFBundleName" = "damus";
/* Privacy - Media Library Usage Description */
"NSAppleMusicUsageDescription" = "Damus needs access to your media library for playback statuses";
/* Privacy - Camera Usage Description */
"NSCameraUsageDescription" = "Damus needs access to your camera in order to upload photos and scan QR codes.";
/* Privacy - Face ID Usage Description */
"NSFaceIDUsageDescription" = "Local authentication to access private key";
/* Privacy - Microphone Usage Description */
"NSMicrophoneUsageDescription" = "Damus needs access to your microphone to allow you to create video recordings that you can choose to post publicly on the network";
/* Privacy - Photo Library Additions Usage Description */
"NSPhotoLibraryAddUsageDescription" = "Granting Damus access to your photos allows you to save images.";
+2 -2
View File
@@ -3,10 +3,10 @@
"project" : "damus.xcodeproj",
"targetLocale" : "en-US",
"toolInfo" : {
"toolBuildNumber" : "16C5032a",
"toolBuildNumber" : "16E140",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "16.2"
"toolVersion" : "16.3"
},
"version" : "1.0"
}
Binary file not shown.
Binary file not shown.
+2 -2
View File
@@ -98,7 +98,7 @@ final class AuthIntegrationTests: XCTestCase {
sent_messages.append(str)
}
XCTAssertEqual(pool.relays.count, 0)
let relay_descriptor = RelayDescriptor.init(url: relay_url, info: .rw)
let relay_descriptor = RelayPool.RelayDescriptor.init(url: relay_url, info: .readWrite)
try! pool.add_relay(relay_descriptor)
XCTAssertEqual(pool.relays.count, 1)
let connection_expectation = XCTestExpectation(description: "Waiting for connection")
@@ -142,7 +142,7 @@ final class AuthIntegrationTests: XCTestCase {
sent_messages.append(str)
}
XCTAssertEqual(pool.relays.count, 0)
let relay_descriptor = RelayDescriptor.init(url: relay_url, info: .rw)
let relay_descriptor = RelayPool.RelayDescriptor.init(url: relay_url, info: .readWrite)
try! pool.add_relay(relay_descriptor)
XCTAssertEqual(pool.relays.count, 1)
let connection_expectation = XCTestExpectation(description: "Waiting for connection")
+1 -4
View File
@@ -27,8 +27,7 @@ func generate_test_damus_state(
}()
let mutelist_manager = MutelistManager(user_keypair: test_keypair)
let damus = DamusState(pool: pool,
keypair: test_keypair,
let damus = DamusState(keypair: test_keypair,
likes: .init(our_pubkey: our_pubkey),
boosts: .init(our_pubkey: our_pubkey),
contacts: .init(our_pubkey: our_pubkey), mutelist_manager: mutelist_manager,
@@ -43,8 +42,6 @@ func generate_test_damus_state(
drafts: .init(),
events: .init(ndb: ndb),
bookmarks: .init(pubkey: our_pubkey),
postbox: .init(pool: pool),
bootstrap_relays: .init(),
replies: .init(our_pubkey: our_pubkey),
wallet: .init(settings: settings),
nav: .init(),
+1 -1
View File
@@ -35,7 +35,7 @@ final class MutingTests: XCTestCase {
}
test_damus_state.mutelist_manager.set_mutelist(mutelist)
test_damus_state.postbox.send(mutelist)
test_damus_state.nostrNetwork.postbox.send(mutelist)
XCTAssert(test_damus_state.mutelist_manager.is_event_muted(spammy_test_note))
XCTAssertFalse(test_damus_state.mutelist_manager.is_event_muted(test_note))
+6
View File
@@ -170,6 +170,12 @@ final class PostViewTests: XCTestCase {
nonAlphaNumerics.forEach { testAddingStringAfterLink(str: $0)}
}
func testQuoteRepost() {
let post = build_post(state: test_damus_state, post: .init(), action: .quoting(test_note), uploadedMedias: [], pubkeys: [])
XCTAssertEqual(post.tags, [["q", test_note.id.hex()]])
}
}
func checkMentionLinkEditorHandling(
+2 -3
View File
@@ -20,13 +20,12 @@ final class RequestTests: XCTestCase {
func testMakeAuthRequest() {
let challenge_string = "8bc847dd-f2f6-4b3a-9c8a-71776ad9b071"
let url = RelayURL("wss://example.com")!
let relayInfo = RelayInfo(read: true, write: true)
let relayDescriptor = RelayDescriptor(url: url, info: relayInfo)
let relayDescriptor = RelayPool.RelayDescriptor(url: url, info: .readWrite)
let relayConnection = RelayConnection(url: url) { _ in
} processEvent: { _ in
}
let relay = Relay(descriptor: relayDescriptor, connection: relayConnection)
let relay = RelayPool.Relay(descriptor: relayDescriptor, connection: relayConnection)
let event = make_auth_request(keypair: FullKeypair.init(pubkey: Pubkey.empty, privkey: Privkey.empty), challenge_string: challenge_string, relay: relay)!
let result = make_nostr_auth_event(ev: event)
@@ -163,7 +163,7 @@ struct ShareExtensionView: View {
break
case .active:
print("txn: 📙 HIGHLIGHTER ACTIVE")
state.pool.ping()
state.nostrNetwork.pool.ping()
@unknown default:
break
}
@@ -238,7 +238,7 @@ struct ShareExtensionView: View {
self.highlighter_state = .failed(error: "Cannot convert post data into a nostr event")
return
}
state.postbox.send(posted_event, on_flush: .once({ flushed_event in
state.nostrNetwork.postbox.send(posted_event, on_flush: .once({ flushed_event in
if flushed_event.event.id == posted_event.id {
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { // Offset labor perception bias
self.highlighter_state = .posted(event: flushed_event.event)
+12
View File
@@ -7,6 +7,18 @@
import Foundation
/// The sequence of strings in a single nostr event tag
///
/// Example 1:
/// ```json
/// ["r", "wss://nostr-relay.example.com", "read"]
/// ```
///
/// Example 2:
/// ```json
/// ["p", "8b2be0a0ad34805d76679272c28a77dbede9adcbfdca48c681ec8b624a1208a6"]
/// ```
struct TagSequence: Sequence {
let note: NdbNote
let tag: UnsafeMutablePointer<ndb_tag>
+78
View File
@@ -0,0 +1,78 @@
//
// UnownedNdbNote.swift
// damus
//
// Created by Daniel DAquino on 2025-03-25.
//
/// A function that allows an unowned NdbNote to be lent out temporarily
///
/// Use this to provide access to NostrDB unowned notes in a way that has much better compile-time safety guarantees.
///
/// # Usage examples
///
/// ## Lending out or providing Ndb notes
///
/// ```swift
/// // Define the lender
/// let lender: NdbNoteLender = { lend in
/// guard let ndbNoteTxn = ndb.lookup_note(noteId) else { // Note: Must have access to `Ndb`
/// throw NdbNoteLenderError.errorLoadingNote // Throw errors if loading fails
/// }
/// guard let unownedNote = UnownedNdbNote(ndbNoteTxn) else {
/// throw NdbNoteLenderError.errorLoadingNote
/// }
/// lend(unownedNote) // Lend out the Unowned Ndb note
/// }
/// return lender // Return or pass the lender to another class
/// ```
///
/// ## Borrowing Ndb notes
///
/// Assuming you are given a lender, here is how you can use it:
///
/// ```swift
/// let borrow: NdbNoteLender = functionThatProvidesALender()
/// try? borrow { note in // You can optionally handle errors if borrowing fails
/// self.date = note.createdAt // You can do things with the note without copying it over
/// // self.note = note // Not allowed by the compiler
/// self.note = note.toOwned() // You can copy the note if needed
/// }
/// ```
typealias NdbNoteLender = ((_: borrowing UnownedNdbNote) -> Void) throws -> Void
enum NdbNoteLenderError: Error {
case errorLoadingNote
}
/// A wrapper to NdbNote that allows unowned NdbNotes to be safely handled
struct UnownedNdbNote: ~Copyable {
private let _ndbNote: NdbNote
init(_ txn: NdbTxn<NdbNote>) {
self._ndbNote = txn.unsafeUnownedValue
}
init?(_ txn: NdbTxn<NdbNote?>) {
guard let note = txn.unsafeUnownedValue else { return nil }
self._ndbNote = note
}
init(_ ndbNote: NdbNote) {
self._ndbNote = ndbNote
}
var kind: UInt32 { _ndbNote.kind }
var known_kind: NostrKind? { _ndbNote.known_kind }
var content: String { _ndbNote.content }
var tags: TagsSequence { _ndbNote.tags }
var pubkey: Pubkey { _ndbNote.pubkey }
var createdAt: UInt32 { _ndbNote.created_at }
var id: NoteId { _ndbNote.id }
var sig: Signature { _ndbNote.sig }
func toOwned() -> NdbNote {
return _ndbNote.to_owned()
}
}
+1 -1
View File
@@ -309,7 +309,7 @@ public func nscript_nostr_cmd(interp: UnsafeMutablePointer<wasm_interp>?, cmd: I
func nscript_add_relay(script: NostrScript, relay: String) -> Bool {
guard let url = RelayURL(relay) else { return false }
let desc = RelayDescriptor(url: url, info: .rw, variant: .ephemeral)
let desc = RelayPool.RelayDescriptor(url: url, info: .readWrite, variant: .ephemeral)
return (try? script.pool.add_relay(desc)) != nil
}
+2 -2
View File
@@ -193,7 +193,7 @@ struct ShareExtensionView: View {
break
case .active:
print("txn: 📙 SHARE ACTIVE")
state.pool.ping()
state.nostrNetwork.pool.ping()
@unknown default:
break
}
@@ -230,7 +230,7 @@ struct ShareExtensionView: View {
self.share_state = .failed(error: "Cannot convert post data into a nostr event")
return
}
state.postbox.send(posted_event, on_flush: .once({ flushed_event in
state.nostrNetwork.postbox.send(posted_event, on_flush: .once({ flushed_event in
if flushed_event.event.id == posted_event.id {
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: { // Offset labor perception bias
self.share_state = .posted(event: flushed_event.event)