Compare commits

..

2 Commits

Author SHA1 Message Date
tyiu dc2c714192 Replace deprecated usage of UIMenuController with UITextViewDelegate
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-04-12 18:17:36 -04:00
tyiu c4ed5ff6bc Fix some compiler warnings
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-04-12 18:08:58 -04:00
97 changed files with 661 additions and 1823 deletions
+5 -71
View File
@@ -207,7 +207,7 @@
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */; };
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */; };
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 4C649880286E0EE300EAE2B3 /* secp256k1 */; };
4C684A552A7E91FE005E6031 /* LargeEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C684A542A7E91FE005E6031 /* LargeEventTests.swift */; };
4C684A552A7E91FE005E6031 /* LongPostTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C684A542A7E91FE005E6031 /* LongPostTests.swift */; };
4C684A572A7FFAE6005E6031 /* UrlTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C684A562A7FFAE6005E6031 /* UrlTests.swift */; };
4C687C212A5F7ED00092C550 /* DamusBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C687C202A5F7ED00092C550 /* DamusBackground.swift */; };
4C687C242A5FA86D0092C550 /* SearchHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C687C232A5FA86D0092C550 /* SearchHeaderView.swift */; };
@@ -1090,13 +1090,6 @@
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 */; };
@@ -1105,15 +1098,6 @@
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 */; };
@@ -1665,9 +1649,6 @@
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 */; };
@@ -2192,7 +2173,7 @@
4C64305B2A945AFF00B0C0E9 /* MusicController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicController.swift; sourceTree = "<group>"; };
4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesView.swift; sourceTree = "<group>"; };
4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesModel.swift; sourceTree = "<group>"; };
4C684A542A7E91FE005E6031 /* LargeEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeEventTests.swift; sourceTree = "<group>"; };
4C684A542A7E91FE005E6031 /* LongPostTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongPostTests.swift; sourceTree = "<group>"; };
4C684A562A7FFAE6005E6031 /* UrlTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UrlTests.swift; sourceTree = "<group>"; };
4C687C202A5F7ED00092C550 /* DamusBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusBackground.swift; sourceTree = "<group>"; };
4C687C232A5FA86D0092C550 /* SearchHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHeaderView.swift; sourceTree = "<group>"; };
@@ -2493,16 +2474,11 @@
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>"; };
@@ -2561,7 +2537,6 @@
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>"; };
@@ -2766,7 +2741,6 @@
4C0A3F8D280F63FF000448DE /* Models */ = {
isa = PBXGroup;
children = (
D73BDB122D71212600D69970 /* NostrNetworkManager */,
D74F43082B23F09300425B75 /* Purple */,
BA3759882ABCCDE30018D73B /* Camera */,
4C190F1E2A535FC200027FD5 /* Zaps */,
@@ -3381,7 +3355,6 @@
4C9054862A6AEB4500811EEC /* nostrdb */ = {
isa = PBXGroup;
children = (
D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */,
4C47928D2A9939BD00489948 /* flatcc */,
4C478E2A2A9935D300489948 /* bindings */,
4CE9FBBB2A6B3D9C007E485C /* Test */,
@@ -3673,7 +3646,6 @@
4CE6DEE527F7A08100C66700 /* damus */ = {
isa = PBXGroup;
children = (
D7DB93082D69478400DA1EE5 /* NIP65 */,
D7DB1FDC2D5A77E500CF06DA /* NIP44 */,
D755B28B2D3E7D6500BBEEFA /* NIP37 */,
D78F08152D7F7F5F00FC6C75 /* NIP04 */,
@@ -3743,7 +3715,7 @@
4C19AE542A5D977400C90DB7 /* HashtagTests.swift */,
3AAC7A012A60FE72002B50DF /* LocalizationUtilTests.swift */,
D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */,
4C684A542A7E91FE005E6031 /* LargeEventTests.swift */,
4C684A542A7E91FE005E6031 /* LongPostTests.swift */,
4C684A562A7FFAE6005E6031 /* UrlTests.swift */,
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */,
D71DC1EB2A9129C3006E207C /* PostViewTests.swift */,
@@ -3976,17 +3948,6 @@
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 = (
@@ -4083,14 +4044,6 @@
path = NIP44;
sourceTree = "<group>";
};
D7DB93082D69478400DA1EE5 /* NIP65 */ = {
isa = PBXGroup;
children = (
D7DB93092D69485A00DA1EE5 /* NIP65.swift */,
);
path = NIP65;
sourceTree = "<group>";
};
E06336A72B7582D600A88E6B /* Assets */ = {
isa = PBXGroup;
children = (
@@ -4480,7 +4433,6 @@
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 */,
@@ -4528,7 +4480,6 @@
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 */,
@@ -4615,7 +4566,6 @@
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 */,
@@ -4652,7 +4602,6 @@
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 */,
@@ -4761,7 +4710,6 @@
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 */,
@@ -4965,7 +4913,6 @@
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 */,
@@ -5025,7 +4972,7 @@
D753CEAA2BE9DE04001C3A5D /* MutingTests.swift in Sources */,
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */,
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */,
4C684A552A7E91FE005E6031 /* LargeEventTests.swift in Sources */,
4C684A552A7E91FE005E6031 /* LongPostTests.swift in Sources */,
E02B54182B4DFADA0077FF42 /* Bech32ObjectTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -5174,7 +5121,6 @@
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 */,
@@ -5189,7 +5135,6 @@
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 */,
@@ -5333,7 +5278,6 @@
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 */,
@@ -5367,7 +5311,6 @@
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 */,
@@ -5422,7 +5365,6 @@
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 */,
@@ -5476,7 +5418,6 @@
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 */,
@@ -5648,7 +5589,6 @@
D73E5E882C6A97F4007EB227 /* StoreObserver.swift in Sources */,
D73E5E892C6A97F4007EB227 /* DamusPurpleURL.swift in Sources */,
D73E5E8A2C6A97F4007EB227 /* PurpleStoreKitManager.swift in Sources */,
D733F9E72D92C76100317B11 /* UnownedNdbNote.swift in Sources */,
D73E5E8E2C6A97F4007EB227 /* ImageResizer.swift in Sources */,
D78F080E2D7F78EF00FC6C75 /* Request.swift in Sources */,
D73E5E8F2C6A97F4007EB227 /* PhotoCaptureProcessor.swift in Sources */,
@@ -5775,7 +5715,6 @@
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 */,
@@ -5816,7 +5755,6 @@
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 */,
@@ -5855,7 +5793,6 @@
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 */,
@@ -5891,7 +5828,6 @@
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 */,
@@ -5943,7 +5879,6 @@
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 */,
@@ -6038,7 +5973,6 @@
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 */,
@@ -6920,7 +6854,7 @@
repositoryURL = "https://github.com/onevcat/Kingfisher";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 8.3.1;
minimumVersion = 7.0.0;
};
};
4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = {
@@ -49,8 +49,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/onevcat/Kingfisher",
"state" : {
"revision" : "4c6b067f96953ee19526e49e4189403a2be21fb3",
"version" : "8.3.1"
"revision" : "415b1d97fb38bda1e5a6b2dde63354720832110b",
"version" : "7.6.1"
}
},
{
+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.nostrNetwork.postbox, zapcache: damus_state.zaps, evcache: damus_state.events) else {
guard let res = cancel_zap(zap: pzap, box: damus_state.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.nostrNetwork.pool.our_descriptors.prefix(10))
let relays = Array(damus_state.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.nostrNetwork.pool, postbox: damus_state.nostrNetwork.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat)
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)
}
})
}
@@ -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.nostrNetwork.pool, post: damus_state.nostrNetwork.postbox, invoice: inv, delay: delay, on_flush: flusher)
let nwc_req = WalletConnect.pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.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)")
@@ -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.nostrNetwork.postbox, keypair: test_keypair, status: .init())
UserStatusSheet(damus_state: test_damus_state, postbox: test_damus_state.postbox, keypair: test_keypair, status: .init())
}
}
+44 -25
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.nostrNetwork.postbox, target: target, keypair: keypair)
ReportView(postbox: damus_state.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!.nostrNetwork.postbox, keypair: damus_state!.keypair, status: damus_state!.profiles.profile_data(damus_state!.pubkey).status)
UserStatusSheet(damus_state: damus_state!, postbox: damus_state!.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?.nostrNetwork.postbox.try_flushing_events()
self.damus_state?.postbox.try_flushing_events()
self.damus_state!.profiles.profile_data(self.damus_state!.pubkey).status.try_expire()
}
.onReceive(handle_notify(.report)) { target in
@@ -367,6 +367,10 @@ 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,
@@ -387,12 +391,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.nostrNetwork.postbox.send(ev)
ds.postbox.send(ev)
}
.onReceive(handle_notify(.broadcast)) { ev in
guard let ds = self.damus_state else { return }
ds.nostrNetwork.postbox.send(ev)
ds.postbox.send(ev)
}
.onReceive(handle_notify(.unfollow)) { target in
guard let state = self.damus_state else { return }
@@ -414,7 +418,7 @@ struct ContentView: View {
return
}
if !handle_post_notification(keypair: keypair, postbox: state.nostrNetwork.postbox, events: state.events, post: post) {
if !handle_post_notification(keypair: keypair, postbox: state.postbox, events: state.events, post: post) {
self.active_sheet = nil
}
}
@@ -458,7 +462,7 @@ struct ContentView: View {
}
}
.onReceive(handle_notify(.disconnect_relays)) { () in
damus_state.nostrNetwork.pool.disconnect()
damus_state.pool.disconnect()
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { obj in
print("txn: 📙 DAMUS ACTIVE NOTIFY")
@@ -504,7 +508,7 @@ struct ContentView: View {
break
case .active:
print("txn: 📙 DAMUS ACTIVE")
damus_state.nostrNetwork.pool.ping()
damus_state.pool.ping()
@unknown default:
break
}
@@ -523,7 +527,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.nostrNetwork.postbox.send(profile_ev)
ds.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.")) {
@@ -555,7 +559,7 @@ struct ContentView: View {
}
ds.mutelist_manager.set_mutelist(mutelist)
ds.nostrNetwork.postbox.send(mutelist)
ds.postbox.send(mutelist)
confirm_overwrite_mutelist = false
confirm_mute = false
@@ -587,7 +591,7 @@ struct ContentView: View {
}
ds.mutelist_manager.set_mutelist(ev)
ds.nostrNetwork.postbox.send(ev)
ds.postbox.send(ev)
}
}
}, message: {
@@ -656,14 +660,28 @@ 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)
}
self.damus_state = DamusState(keypair: keypair,
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,
likes: EventCounter(our_pubkey: pubkey),
boosts: EventCounter(our_pubkey: pubkey),
contacts: Contacts(our_pubkey: pubkey),
@@ -679,6 +697,8 @@ 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,
@@ -702,8 +722,7 @@ struct ContentView: View {
// Purple API is an experimental feature. If not enabled, do not connect `StoreObserver` with Purple API to avoid leaking receipts
}
damus_state.nostrNetwork.pool.register_handler(sub_id: sub_id, handler: home.handle_event)
damus_state.nostrNetwork.connect()
pool.connect()
}
func music_changed(_ state: MusicState) {
@@ -726,7 +745,7 @@ struct ContentView: View {
pdata.status.music = music
guard let ev = music.to_note(keypair: kp) else { return }
damus_state.nostrNetwork.postbox.send(ev)
damus_state.postbox.send(ev)
}
}
@@ -975,7 +994,7 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
var has_event = false
guard let filter else { return }
state.nostrNetwork.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in
state.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in
guard case .nostr_event(let ev) = res else {
return
}
@@ -989,7 +1008,7 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
break
case .event(_, let ev):
has_event = true
state.nostrNetwork.pool.unsubscribe(sub_id: subid)
state.pool.unsubscribe(sub_id: subid)
switch query {
case .profile:
@@ -1002,11 +1021,11 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
case .eose:
if !has_event {
attempts += 1
if attempts >= state.nostrNetwork.pool.our_descriptors.count {
if attempts >= state.pool.our_descriptors.count {
callback(nil) // If we could not find any events in any of the relays we are connected to, send back nil
}
}
state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id]) // We are only finding an event once, so close subscription on eose
state.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:
@@ -1031,9 +1050,9 @@ func naddrLookup(damus_state: DamusState, naddr: NAddr, callback: @escaping (Nos
let subid = UUID().description
damus_state.nostrNetwork.pool.subscribe_to(sub_id: subid, filters: [filter], to: nil) { relay_id, res in
damus_state.pool.subscribe_to(sub_id: subid, filters: [filter], to: nil) { relay_id, res in
guard case .nostr_event(let ev) = res else {
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id])
damus_state.pool.unsubscribe(sub_id: subid, to: [relay_id])
return
}
@@ -1041,14 +1060,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.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id])
damus_state.pool.unsubscribe(sub_id: subid, to: [relay_id])
callback(ev)
return
}
}
}
}
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id])
damus_state.pool.unsubscribe(sub_id: subid, to: [relay_id])
}
}
@@ -1096,7 +1115,7 @@ func handle_unfollow(state: DamusState, unfollow: FollowRef) -> Bool {
let old_contacts = state.contacts.event
guard let ev = unfollow_reference(postbox: state.nostrNetwork.postbox, our_contacts: old_contacts, keypair: keypair, unfollow: unfollow)
guard let ev = unfollow_reference(postbox: state.postbox, our_contacts: old_contacts, keypair: keypair, unfollow: unfollow)
else {
return false
}
@@ -1122,7 +1141,7 @@ func handle_follow(state: DamusState, follow: FollowRef) -> Bool {
return false
}
guard let ev = follow_reference(box: state.nostrNetwork.postbox, our_contacts: state.contacts.event, keypair: keypair, follow: follow)
guard let ev = follow_reference(box: state.postbox, our_contacts: state.contacts.event, keypair: keypair, follow: follow)
else {
return false
}
+54 -1
View File
@@ -63,10 +63,44 @@ func follow_user_event(our_contacts: NostrEvent?, keypair: FullKeypair, follow:
}
func decode_json_relays(_ content: String) -> [RelayURL: LegacyKind3RelayRWConfiguration]? {
func decode_json_relays(_ content: String) -> [RelayURL: RelayInfo]? {
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) {
@@ -94,3 +128,22 @@ 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,12 +40,6 @@ 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]
@@ -72,7 +66,6 @@ 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,10 +27,6 @@ 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
+27 -32
View File
@@ -10,6 +10,7 @@ import LinkPresentation
import EmojiPicker
class DamusState: HeadlessDamusState {
let pool: RelayPool
let keypair: Keypair
let likes: EventCounter
let boosts: EventCounter
@@ -27,6 +28,8 @@ 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
@@ -36,9 +39,9 @@ class DamusState: HeadlessDamusState {
var purple: DamusPurple
var push_notification_client: PushNotificationClient
let emoji_provider: EmojiProvider
private(set) var nostrNetwork: NostrNetworkManager
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) {
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
self.keypair = keypair
self.likes = likes
self.boosts = boosts
@@ -55,6 +58,8 @@ 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
@@ -68,9 +73,6 @@ 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
@@ -96,13 +98,27 @@ 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),
@@ -119,6 +135,8 @@ 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,
@@ -161,7 +179,7 @@ class DamusState: HeadlessDamusState {
try await self.push_notification_client.revoke_token()
}
wallet.disconnect()
nostrNetwork.pool.close()
pool.close()
ndb.close()
}
@@ -171,6 +189,7 @@ 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),
@@ -187,6 +206,8 @@ 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(),
@@ -198,29 +219,3 @@ 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.nostrNetwork.pool.send_raw_to_local_ndb(.typical(.event(draft_event)))
damus_state.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.nostrNetwork.pool.subscribe(sub_id: sub_id,
state.pool.subscribe(sub_id: sub_id,
filters: [get_filter()],
handler: handle_nostr_event)
}
func unsubscribe() {
state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
state.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.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
self.damus_state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
}
func unsubscribe() {
self.damus_state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
self.damus_state.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.nostrNetwork.pool.subscribe_to(sub_id: profiles_id, filters: [filter], to: [relay_id], handler: handle_event)
damus_state.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.nostrNetwork.pool.unsubscribe(sub_id: profiles_id, to: [relay_id])
damus_state.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.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
self.damus_state.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.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
self.damus_state.pool.unsubscribe(sub_id: sub_id)
}
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
+76 -6
View File
@@ -95,7 +95,7 @@ class HomeModel: ContactsDelegate {
}
var pool: RelayPool {
self.damus_state.nostrNetwork.pool
return damus_state.pool
}
var dms: DirectMessagesModel {
@@ -225,8 +225,6 @@ 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`
}
}
@@ -268,7 +266,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.nostrNetwork.postbox.remove_relayer(relay_id: nwc.relay, event_id: resp.req_id) {
if damus_state.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)]")
@@ -480,7 +478,7 @@ class HomeModel: ContactsDelegate {
break
}
update_signal_from_pool(signal: self.signal, pool: damus_state.nostrNetwork.pool)
update_signal_from_pool(signal: self.signal, pool: damus_state.pool)
case .nostr_event(let ev):
switch ev {
case .event(let sub_id, let ev):
@@ -950,6 +948,7 @@ 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) {
@@ -957,6 +956,78 @@ 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://")
@@ -1179,4 +1250,3 @@ func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale:
}
}
}
+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.nostrNetwork.postbox.send(new_mutelist_event)
damus_state.postbox.send(new_mutelist_event)
// Set existing muted threads to an empty array
UserDefaults.standard.set([], forKey: getMutedThreadsKey(pubkey: keypair.pubkey))
}
@@ -1,95 +0,0 @@
//
// 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 }
}
}
@@ -1,70 +0,0 @@
//
// 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
}
}
@@ -1,85 +0,0 @@
//
// 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."
)
}
}
}
}
@@ -1,311 +0,0 @@
//
// 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)
}
}
}
}
-11
View File
@@ -41,10 +41,6 @@ func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent
return false
}
if state.settings.hellthread_notifications_disabled && ev.is_hellthread(max_pubkeys: state.settings.hellthread_notification_max_pubkeys) {
return false
}
// Don't show notifications that match mute list.
if state.mutelist_manager.is_event_muted(ev) {
return false
@@ -55,13 +51,6 @@ func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent
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
}
+12 -26
View File
@@ -10,18 +10,8 @@ import Foundation
class ProfileModel: ObservableObject, Equatable {
@Published var contacts: NostrEvent? = nil
@Published var following: Int = 0
@Published var relay_list: NIP65.RelayList? = nil
@Published var legacy_relay_list: [RelayURL: LegacyKind3RelayRWConfiguration]? = nil
@Published var relays: [RelayURL: RelayInfo]? = 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
@@ -69,17 +59,16 @@ class ProfileModel: ObservableObject, Equatable {
func unsubscribe() {
print("unsubscribing from profile \(pubkey) with sub_id \(sub_id)")
damus.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
damus.nostrNetwork.pool.unsubscribe(sub_id: prof_subid)
damus.pool.unsubscribe(sub_id: sub_id)
damus.pool.unsubscribe(sub_id: prof_subid)
if pubkey != damus.pubkey {
damus.nostrNetwork.pool.unsubscribe(sub_id: conversations_subid)
damus.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]
@@ -88,8 +77,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.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)
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)
subscribe_to_conversations()
}
@@ -105,7 +94,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.nostrNetwork.pool.subscribe(sub_id: conversations_subid, filters: [conversations_filter_them, conversations_filter_us], handler: handle_event)
damus.pool.subscribe(sub_id: conversations_subid, filters: [conversations_filter_them, conversations_filter_us], handler: handle_event)
}
func handle_profile_contact_event(_ ev: NostrEvent) {
@@ -120,7 +109,7 @@ class ProfileModel: ObservableObject, Equatable {
self.contacts = ev
self.following = count_pubkeys(ev.tags)
self.legacy_relay_list = decode_json_relays(ev.content)
self.relays = decode_json_relays(ev.content)
}
private func add_event(_ ev: NostrEvent) {
@@ -131,9 +120,6 @@ 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)
}
@@ -206,7 +192,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.legacy_relay_list = decode_json_relays(event.content)
self.relays = decode_json_relays(event.content)
}
}
@@ -214,15 +200,15 @@ class ProfileModel: ObservableObject, Equatable {
var profile_filter = NostrFilter(kinds: [.contacts])
profile_filter.authors = [pubkey]
damus.nostrNetwork.pool.subscribe(sub_id: findRelay_subid, filters: [profile_filter], handler: findRelaysHandler)
damus.pool.subscribe(sub_id: findRelay_subid, filters: [profile_filter], handler: findRelaysHandler)
}
func unsubscribeFindRelays() {
damus.nostrNetwork.pool.unsubscribe(sub_id: findRelay_subid)
damus.pool.unsubscribe(sub_id: findRelay_subid)
}
func getCappedRelayStrings() -> [String] {
return self.relay_urls?.prefix(MAX_SHARE_RELAYS).map { $0.absoluteString } ?? []
return relays?.keys.prefix(MAX_SHARE_RELAYS).map { $0.absoluteString } ?? []
}
}
+7 -33
View File
@@ -7,12 +7,6 @@
import Foundation
// Minimum threshold the hellthread pubkey tag count setting can go down to.
let HELLTHREAD_MIN_PUBKEYS: Int = 6
// Maximum threshold the hellthread pubkey tag count setting can go up to.
let HELLTHREAD_MAX_PUBKEYS: Int = 24
struct PushNotificationClient {
let keypair: Keypair
let settings: UserSettingsStore
@@ -181,33 +175,15 @@ extension PushNotificationClient {
}
struct NotificationSettings: Codable, Equatable {
let zap_notifications_enabled: Bool?
let mention_notifications_enabled: Bool?
let repost_notifications_enabled: Bool?
let reaction_notifications_enabled: Bool?
let dm_notifications_enabled: Bool?
let only_notifications_from_following_enabled: Bool?
let hellthread_notifications_disabled: Bool?
let hellthread_notifications_max_pubkeys: Int?
let zap_notifications_enabled: Bool
let mention_notifications_enabled: Bool
let repost_notifications_enabled: Bool
let reaction_notifications_enabled: Bool
let dm_notifications_enabled: Bool
let only_notifications_from_following_enabled: Bool
static func from(json_data: Data) -> Self? {
guard let decoded = try? JSONDecoder().decode(Self.self, from: json_data) else { return nil }
// Normalize hellthread_notifications_max_pubkeys in case
// it goes beyond the expected range supported on the client.
if let max_pubkeys = decoded.hellthread_notifications_max_pubkeys, max_pubkeys < HELLTHREAD_MIN_PUBKEYS || max_pubkeys > HELLTHREAD_MAX_PUBKEYS {
return NotificationSettings(
zap_notifications_enabled: decoded.zap_notifications_enabled,
mention_notifications_enabled: decoded.mention_notifications_enabled,
repost_notifications_enabled: decoded.repost_notifications_enabled,
reaction_notifications_enabled: decoded.reaction_notifications_enabled,
dm_notifications_enabled: decoded.dm_notifications_enabled,
only_notifications_from_following_enabled: decoded.only_notifications_from_following_enabled,
hellthread_notifications_disabled: decoded.hellthread_notifications_disabled,
hellthread_notifications_max_pubkeys: max(min(HELLTHREAD_MAX_PUBKEYS, max_pubkeys), HELLTHREAD_MIN_PUBKEYS)
)
}
return decoded
}
@@ -218,9 +194,7 @@ extension PushNotificationClient {
repost_notifications_enabled: settings.repost_notification,
reaction_notifications_enabled: settings.like_notification,
dm_notifications_enabled: settings.dm_notification,
only_notifications_from_following_enabled: settings.notification_only_from_following,
hellthread_notifications_disabled: settings.hellthread_notifications_disabled,
hellthread_notifications_max_pubkeys: settings.hellthread_notification_max_pubkeys
only_notifications_from_following_enabled: settings.notification_only_from_following
)
}
+5 -5
View File
@@ -41,13 +41,13 @@ class SearchHomeModel: ObservableObject {
func subscribe() {
loading = true
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)
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)
}
func unsubscribe(to: RelayURL? = nil) {
loading = false
damus_state.nostrNetwork.pool.unsubscribe(sub_id: base_subid, to: to.map { [$0] })
damus_state.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.nostrNetwork.pool.subscribe_to(sub_id: profiles_subid, filters: [filter], to: [relay_id]) { rid, conn_ev in
damus_state.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.nostrNetwork.pool.unsubscribe(sub_id: profiles_subid, to: [relay_id])
damus_state.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.nostrNetwork.pool.register_handler(sub_id: sub_id, handler: handle_event)
state.pool.register_handler(sub_id: sub_id, handler: handle_event)
loading = true
state.nostrNetwork.pool.send(.subscribe(.init(filters: [search], sub_id: sub_id)))
state.pool.send(.subscribe(.init(filters: [search], sub_id: sub_id)))
}
func unsubscribe() {
state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
state.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.nostrNetwork.pool, relay_id: relay_id, ev: ev) { sub_id, ev in
let (sub_id, done) = handle_subid_event(pool: state.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.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)
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)
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.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)
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)
}
/// 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.nostrNetwork.pool, relay_id: relay_id, ev: ev) { sid, ev in
let (sub_id, done) = handle_subid_event(pool: damus_state.pool, relay_id: relay_id, ev: ev) { sid, ev in
guard subids.contains(sid) else {
return
}
-10
View File
@@ -161,12 +161,6 @@ class UserSettingsStore: ObservableObject {
@Setting(key: "notification_only_from_following", default_value: false)
var notification_only_from_following: Bool
@Setting(key: "hellthread_notifications_disabled", default_value: false)
var hellthread_notifications_disabled: Bool
@Setting(key: "hellthread_notification_max_pubkeys", default_value: DEFAULT_HELLTHREAD_MAX_PUBKEYS)
var hellthread_notification_max_pubkeys: Int
@Setting(key: "translate_dms", default_value: false)
var translate_dms: Bool
@@ -342,10 +336,6 @@ 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.nostrNetwork.pool.subscribe(sub_id: zaps_subid, filters: [filter], handler: handle_event)
state.pool.subscribe(sub_id: zaps_subid, filters: [filter], handler: handle_event)
}
func unsubscribe() {
state.nostrNetwork.pool.unsubscribe(sub_id: zaps_subid)
state.pool.unsubscribe(sub_id: zaps_subid)
}
@MainActor
-171
View File
@@ -1,171 +0,0 @@
//
// 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,19 +34,6 @@ 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: RelayPool.Relay) -> NostrEvent? {
func make_auth_request(keypair: FullKeypair, challenge_string: String, relay: 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: [RelayPool.RelayDescriptor], target: ZapTarget, zap_type: ZapType) -> MakeZapRequest? {
func make_zap_request_event(keypair: FullKeypair, content: String, relays: [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 = LegacyKind3RelayRWConfiguration(read: true, write: true)
var relays: [RelayURL: LegacyKind3RelayRWConfiguration] = [:]
let rw_relay_info = RelayInfo(read: true, write: true)
var relays: [RelayURL: RelayInfo] = [:]
for relay in bootstrap_relays {
relays[relay] = rw_relay_info
-12
View File
@@ -13,18 +13,6 @@ 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,7 +8,6 @@
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
@@ -19,7 +18,6 @@ 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
+1 -12
View File
@@ -12,14 +12,11 @@ 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
@@ -28,7 +25,6 @@ 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
@@ -38,18 +34,12 @@ 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:
@@ -63,7 +53,6 @@ enum NostrRequest {
}
}
/// Whether this request is meant to read data from a relay
var is_read: Bool {
return !is_write
}
-25
View File
@@ -115,19 +115,6 @@ 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)
@@ -137,7 +124,6 @@ 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
@@ -150,14 +136,6 @@ 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
@@ -170,12 +148,10 @@ 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()
@@ -190,7 +166,6 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
}
}
/// Parses a raw tag sequence
static func from_tag(tag: TagSequence) -> RefId? {
var i = tag.makeIterator()
+45 -83
View File
@@ -7,25 +7,16 @@
import Foundation
public struct LegacyKind3RelayRWConfiguration: Codable, Sendable {
public let read: Bool?
public let write: Bool?
public struct RelayInfo: Codable {
let read: Bool?
let write: Bool?
init(read: Bool, write: Bool) {
self.read = read
self.write = write
}
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
}
}
static let rw = RelayInfo(read: true, write: true)
}
enum RelayVariant {
@@ -34,33 +25,30 @@ enum RelayVariant {
case nwc
}
extension RelayPool {
/// Describes a relay for use in `RelayPool`
public struct RelayDescriptor {
let url: RelayURL
var info: NIP65.RelayList.RelayItem.RWConfiguration
let variant: RelayVariant
public struct RelayDescriptor {
let url: RelayURL
let info: RelayInfo
let variant: RelayVariant
init(url: RelayURL, info: NIP65.RelayList.RelayItem.RWConfiguration, variant: RelayVariant = .regular) {
self.url = url
self.info = info
self.variant = variant
}
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
}
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)
}
}
@@ -141,56 +129,30 @@ struct RelayMetadata: Codable {
}
}
extension RelayPool {
class Relay: Identifiable {
var descriptor: RelayDescriptor
let connection: RelayConnection
var authentication_state: RelayAuthenticationState
class Relay: Identifiable {
let descriptor: RelayDescriptor
let connection: RelayConnection
var authentication_state: RelayAuthenticationState
var flags: Int
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
}
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
}
}
extension RelayPool {
enum RelayError: Error {
case RelayAlreadyExists
}
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
}
}
+9 -69
View File
@@ -24,15 +24,13 @@ struct SeenEvent: Hashable {
let evid: NoteId
}
/// Establishes and manages connections and subscriptions to a list of relays.
class RelayPool {
private(set) var relays: [Relay] = []
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)?
@@ -124,7 +122,7 @@ class RelayPool {
}
}
func add_relay(_ desc: RelayDescriptor) throws(RelayError) {
func add_relay(_ desc: RelayDescriptor) throws {
let relay_id = desc.url
if get_relay(relay_id) != nil {
throw RelayError.RelayAlreadyExists
@@ -203,64 +201,6 @@ class RelayPool {
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)
send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to)
@@ -303,19 +243,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) // Always send Nostr events and data to NostrDB for a local copy
self.send_raw_to_local_ndb(req)
for relay in relays {
if req.is_read && !(relay.descriptor.info.canRead) {
continue // Do not send read requests to relays that are not READ relays
if req.is_read && !(relay.descriptor.info.read ?? true) {
continue
}
if req.is_write && !(relay.descriptor.info.canWrite) {
continue // Do not send write requests to relays that are not WRITE relays
if req.is_write && !(relay.descriptor.info.write ?? true) {
continue
}
if relay.descriptor.ephemeral && skip_ephemeral {
continue // Do not send requests to ephemeral relays if we want to skip them
continue
}
guard relay.connection.isConnected else {
@@ -414,7 +354,7 @@ class RelayPool {
}
func add_rw_relay(_ pool: RelayPool, _ url: RelayURL) {
try? pool.add_relay(RelayPool.RelayDescriptor(url: url, info: .readWrite))
try? pool.add_relay(RelayDescriptor(url: url, info: .rw))
}
+4 -1
View File
@@ -83,7 +83,8 @@ var test_damus_state: DamusState = ({
let our_pubkey = test_pubkey
let pool = RelayPool(ndb: ndb)
let settings = UserSettingsStore()
let damus = DamusState(keypair: test_keypair,
let damus = DamusState(pool: pool,
keypair: test_keypair,
likes: .init(our_pubkey: our_pubkey),
boosts: .init(our_pubkey: our_pubkey),
contacts: .init(our_pubkey: our_pubkey),
@@ -99,6 +100,8 @@ 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.KF.ImageResource(downloadURL: url)) { val in
KingfisherManager.shared.retrieveImage(with: Kingfisher.ImageResource(downloadURL: url)) { val in
//print("Preloaded image \(url.absoluteString)")
}
}
+6 -11
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.KF.ImageResource(downloadURL: url, cacheKey: key)
let imageResource = Kingfisher.ImageResource(downloadURL: url, cacheKey: key)
let source = imageResource.convertToSource()
options.alternativeSources = [source]
@@ -159,25 +159,20 @@ struct CustomCacheSerializer: CacheSerializer {
}
}
class CustomSessionDelegate: SessionDelegate, @unchecked Sendable {
override func urlSession(
_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive response: URLResponse
) async -> URLSession.ResponseDisposition {
class CustomSessionDelegate: SessionDelegate {
override func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
let contentLength = response.expectedContentLength
// Content-Length header is optional (-1 when missing)
if (contentLength != -1 && contentLength > MAX_FILE_SIZE) {
return await super.urlSession(session, dataTask: dataTask, didReceive: URLResponse())
return super.urlSession(session, dataTask: dataTask, didReceive: URLResponse(), completionHandler: completionHandler)
}
return await super.urlSession(session, dataTask: dataTask, didReceive: response)
super.urlSession(session, dataTask: dataTask, didReceive: response, completionHandler: completionHandler)
}
}
class CustomImageDownloader: ImageDownloader, @unchecked Sendable {
class CustomImageDownloader: ImageDownloader {
static let shared = CustomImageDownloader(name: "shared")
+1 -1
View File
@@ -54,7 +54,7 @@ enum CancelSendErr {
}
class PostBox {
private let pool: RelayPool
let pool: RelayPool
var events: [NoteId: PostedEvent]
init(pool: RelayPool) {
-7
View File
@@ -7,13 +7,6 @@
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]()
+1 -1
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.nostrNetwork.pool, model: load_model)
LoadScript(pool: damusState.pool, model: load_model)
}
}
+1 -1
View File
@@ -270,7 +270,7 @@ struct EventActionBar: View {
generator.impactOccurred()
damus_state.nostrNetwork.postbox.send(like_ev)
damus_state.postbox.send(like_ev)
}
// MARK: Helper structures
+1 -1
View File
@@ -25,7 +25,7 @@ struct RepostAction: View {
return
}
damus_state.nostrNetwork.postbox.send(boost)
damus_state.postbox.send(boost)
} label: {
Label(NSLocalizedString("Repost", comment: "Button to repost a note"), image: "repost")
.frame(maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .leading)
+24 -20
View File
@@ -15,8 +15,6 @@ 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.")
@@ -84,21 +82,38 @@ struct AddRelayView: View {
new_relay = "wss://" + new_relay
}
guard let url = RelayURL(new_relay) else {
relayAddErrorTitle = NSLocalizedString("Invalid relay address", comment: "Heading for an error when adding a relay")
relayAddErrorMessage = NSLocalizedString("Please check the address and try again", comment: "Tip for an error where the relay address being added is invalid")
guard let url = RelayURL(new_relay),
let ev = state.contacts.event,
let keypair = state.keypair.to_full() else {
return
}
let info = RelayInfo.rw
let descriptor = RelayDescriptor(url: url, info: info)
do {
try state.nostrNetwork.userRelayList.insert(relay: NIP65.RelayList.RelayItem(url: url, rwConfiguration: .readWrite))
try state.pool.add_relay(descriptor)
relayAddErrorTitle = nil // Clear error title
relayAddErrorMessage = nil // Clear error message
}
catch {
present_sheet(.error(self.humanReadableError(for: error)))
} 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
}
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)
@@ -119,17 +134,6 @@ 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.nostrNetwork.postbox.send(like_ev)
damus_state.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: NSLocalizedString("Search within settings", comment: "Text to prompt the user to search settings."))
.searchable(text: $searchText,prompt: "Search within 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.nostrNetwork.postbox.send(ev)
state.postbox.send(ev)
logout(state)
}
}
+1 -1
View File
@@ -138,7 +138,7 @@ struct DMChatView: View, KeyboardReadable {
dms.draft = ""
damus_state.nostrNetwork.postbox.send(dm)
damus_state.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.nostrNetwork.pool.unsubscribe(sub_id: subscription_uuid)
damus_state.pool.unsubscribe(sub_id: subscription_uuid)
}
func subscribe(filters: [NostrFilter]) {
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)))
damus_state.pool.register_handler(sub_id: subscription_uuid, handler: handle_event)
damus_state.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.nostrNetwork.postbox.send(new_mutelist_ev)
damus_state.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, .nwc_request, .nwc_response, .http_auth, .status, .relay_list:
case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .nwc_request, .nwc_response, .http_auth, .status:
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.nostrNetwork.postbox.send(mutelist)
state.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.nostrNetwork.postbox.send(new_ev)
damus_state.postbox.send(new_ev)
updateMuteItems()
} label: {
Label(NSLocalizedString("Delete", comment: "Button to remove a user from their mutelist."), image: "delete")
@@ -9,27 +9,15 @@ import SwiftUI
class NotificationFilter: ObservableObject, Equatable {
@Published var state: NotificationFilterState
@Published var friend_filter: FriendFilter
@Published var hellthread_notifications_disabled: Bool
@Published var hellthread_notification_max_pubkeys: Int
@Published var fine_filter: FriendFilter
static func == (lhs: NotificationFilter, rhs: NotificationFilter) -> Bool {
return lhs.state == rhs.state
&& lhs.friend_filter == rhs.friend_filter
&& lhs.hellthread_notifications_disabled == rhs.hellthread_notifications_disabled
&& lhs.hellthread_notification_max_pubkeys == rhs.hellthread_notification_max_pubkeys
return lhs.state == rhs.state && lhs.fine_filter == rhs.fine_filter
}
init(
state: NotificationFilterState = .all,
friend_filter: FriendFilter = .all,
hellthread_notifications_disabled: Bool = false,
hellthread_notification_max_pubkeys: Int = DEFAULT_HELLTHREAD_MAX_PUBKEYS
) {
init(state: NotificationFilterState = .all, fine_filter: FriendFilter = .all) {
self.state = state
self.friend_filter = friend_filter
self.hellthread_notifications_disabled = hellthread_notifications_disabled
self.hellthread_notification_max_pubkeys = hellthread_notification_max_pubkeys
self.fine_filter = fine_filter
}
func filter(contacts: Contacts, items: [NotificationItem]) -> [NotificationItem] {
@@ -39,13 +27,7 @@ class NotificationFilter: ObservableObject, Equatable {
return
}
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)) &&
// 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
}) {
if let item = item.filter({ self.fine_filter.filter(contacts: contacts, pubkey: $0.pubkey) }) {
acc.append(item)
}
}
@@ -83,9 +65,7 @@ struct NotificationsView: View {
NotificationTab(
NotificationFilter(
state: .all,
friend_filter: filter.friend_filter,
hellthread_notifications_disabled: state.settings.hellthread_notifications_disabled,
hellthread_notification_max_pubkeys: state.settings.hellthread_notification_max_pubkeys
fine_filter: filter.fine_filter
)
)
.tag(NotificationFilterState.all)
@@ -93,9 +73,7 @@ struct NotificationsView: View {
NotificationTab(
NotificationFilter(
state: .zaps,
friend_filter: filter.friend_filter,
hellthread_notifications_disabled: state.settings.hellthread_notifications_disabled,
hellthread_notification_max_pubkeys: state.settings.hellthread_notification_max_pubkeys
fine_filter: filter.fine_filter
)
)
.tag(NotificationFilterState.zaps)
@@ -103,9 +81,7 @@ struct NotificationsView: View {
NotificationTab(
NotificationFilter(
state: .replies,
friend_filter: filter.friend_filter,
hellthread_notifications_disabled: state.settings.hellthread_notifications_disabled,
hellthread_notification_max_pubkeys: state.settings.hellthread_notification_max_pubkeys
fine_filter: filter.fine_filter
)
)
.tag(NotificationFilterState.replies)
@@ -122,20 +98,20 @@ struct NotificationsView: View {
}
ToolbarItem(placement: .navigationBarTrailing) {
if would_filter_non_friends_from_notifications(contacts: state.contacts, state: filter_state, items: self.notifications.notifications) {
FriendsButton(filter: $filter.friend_filter)
FriendsButton(filter: $filter.fine_filter)
}
}
}
.onChange(of: filter.friend_filter) { val in
.onChange(of: filter.fine_filter) { val in
state.settings.friend_filter = val
self.subtitle = filter.friend_filter.description()
self.subtitle = filter.fine_filter.description()
}
.onChange(of: filter_state) { val in
filter.state = val
}
.onAppear {
self.filter.friend_filter = state.settings.friend_filter
self.subtitle = filter.friend_filter.description()
self.filter.fine_filter = state.settings.friend_filter
self.subtitle = filter.fine_filter.description()
filter.state = filter_state
}
.safeAreaInset(edge: .top, spacing: 0) {
@@ -77,7 +77,7 @@ class SuggestedUsersViewModel: ObservableObject {
private func subscribeToSuggestedProfiles(pubkeys: [Pubkey]) {
let filter = NostrFilter(kinds: [.metadata], authors: pubkeys)
damus_state.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: [filter], handler: handle_event)
damus_state.pool.subscribe(sub_id: sub_id, filters: [filter], handler: handle_event)
}
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
-2
View File
@@ -883,8 +883,6 @@ 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.nostrNetwork.postbox.send(metadata_ev)
damus_state.postbox.send(metadata_ev)
}
func is_ln_valid(ln: String) -> Bool {
+1 -3
View File
@@ -67,9 +67,7 @@ struct ProfileName: View {
}
func name_choice(profile: Profile?) -> String {
let displayName = current_display_name(profile: profile)
let untruncatedName = prefix == "@" ? displayName.username : displayName.displayName
return untruncatedName.truncate(maxLength: 50)
return prefix == "@" ? current_display_name(profile: profile).username.truncate(maxLength: 50) : current_display_name(profile: profile).displayName.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.nostrNetwork.postbox.send(new_ev)
damus_state.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.relay_urls {
if let relays = profile.relays {
// 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.count)
let noun_string = pluralizedString(key: "relays_count", count: relays.keys.count)
let noun_text = Text(noun_string).font(.subheadline).foregroundColor(.gray)
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'.")
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'.")
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: relays.sorted())) {
NavigationLink(value: Route.UserRelays(relays: Array(relays.keys).sorted())) {
relay_text
}
.buttonStyle(PlainButtonStyle())
+3 -3
View File
@@ -15,11 +15,11 @@ struct RelayFilterView: View {
self.state = state
self.timeline = timeline
//_relays = State(initialValue: state.networkManager.pool.descriptors)
//_relays = State(initialValue: state.pool.descriptors)
}
var relays: [RelayPool.RelayDescriptor] {
return state.nostrNetwork.pool.our_descriptors
var relays: [RelayDescriptor] {
return state.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: [RelayPool.RelayDescriptor]
@State var relays: [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.nostrNetwork.pool.our_descriptors)
_relays = State(initialValue: state.pool.our_descriptors)
UITabBar.appearance().isHidden = true
}
var recommended: [RelayPool.RelayDescriptor] {
let rs: [RelayPool.RelayDescriptor] = []
var recommended: [RelayDescriptor] {
let rs: [RelayDescriptor] = []
let recommended_relay_addresses = get_default_bootstrap_relays()
return recommended_relay_addresses.reduce(into: rs) { xs, x in
xs.append(RelayPool.RelayDescriptor(url: x, info: .readWrite))
xs.append(RelayDescriptor(url: x, info: .rw))
}
}
@@ -98,7 +98,7 @@ struct RelayConfigView: View {
}
}
.onReceive(handle_notify(.relays_changed)) { _ in
self.relays = state.nostrNetwork.pool.our_descriptors
self.relays = state.pool.our_descriptors
}
.onAppear {
notify(.display_tabbar(false))
@@ -109,7 +109,7 @@ struct RelayConfigView: View {
.ignoresSafeArea(.all)
}
func RelayList(title: String, relayList: [RelayPool.RelayDescriptor], recommended: Bool) -> some View {
func RelayList(title: String, relayList: [RelayDescriptor], recommended: Bool) -> some View {
ScrollView(showsIndicators: false) {
HStack {
Text(title)
+37 -25
View File
@@ -25,12 +25,32 @@ struct RelayDetailView: View {
}
func check_connection() -> Bool {
return state.nostrNetwork.userRelayList.getUserCurrentRelayList()?.relays.keys.contains(self.relay) == true
for relay in state.pool.relays {
if relay.id == self.relay {
return true
}
}
return false
}
func RemoveRelayButton(_ keypair: FullKeypair) -> some View {
Button(action: {
self.removeRelay()
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()
}) {
HStack {
Text("Disconnect", comment: "Button to disconnect from the relay.")
@@ -43,7 +63,19 @@ struct RelayDetailView: View {
func ConnectRelayButton(_ keypair: FullKeypair) -> some View {
Button(action: {
self.connectRelay()
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()
}) {
HStack {
Text("Connect", comment: "Button to connect to the relay.")
@@ -176,33 +208,13 @@ struct RelayDetailView: View {
}
}
private var relay_object: RelayPool.Relay? {
state.nostrNetwork.pool.get_relay(relay)
private var relay_object: Relay? {
state.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.nostrNetwork.pool.get_relay(RelayURL("wss://relay.damus.io")!)!.connection
let connection = test_damus_state.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.nostrNetwork.pool.get_relay(relay_id)?.connection
state.pool.get_relay(relay_id)?.connection
}
}
+32 -16
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.nostrNetwork.pool, relay: relay)
let relay_state = RelayView.get_relay_state(pool: state.pool, relay: relay)
self._relay_state = State(initialValue: relay_state)
}
@@ -80,7 +80,7 @@ struct RelayView: View {
AddButton(keypair: keypair)
} else {
Button(action: {
Task { await remove_action(privkey: keypair.privkey) }
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.nostrNetwork.pool, relay: self.relay)
self.relay_state = RelayView.get_relay_state(pool: state.pool, relay: self.relay)
}
.onTapGesture {
state.nav.push(route: Route.RelayDetail(relay: relay, metadata: model_cache.model(with_relay_id: relay)?.metadata))
@@ -113,30 +113,46 @@ struct RelayView: View {
}
private var relay_connection: RelayConnection? {
state.nostrNetwork.pool.get_relay(relay)?.connection
state.pool.get_relay(relay)?.connection
}
func add_action(keypair: FullKeypair) async {
do {
try await state.nostrNetwork.userRelayList.insert(relay: NIP65.RelayList.RelayItem(url: relay, rwConfiguration: .readWrite))
func add_action(keypair: FullKeypair) {
guard let ev_before_add = state.contacts.event else {
return
}
catch {
present_sheet(.error(error.humanReadableError))
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)
}
}
func remove_action(privkey: Privkey) async {
do {
try await state.nostrNetwork.userRelayList.remove(relayURL: relay)
func remove_action(privkey: Privkey) {
guard let ev = state.contacts.event else {
return
}
catch {
present_sheet(.error(error.humanReadableError))
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)
}
}
func AddButton(keypair: FullKeypair) -> some View {
Button(action: {
Task { await add_action(keypair: keypair) }
add_action(keypair: keypair)
}) {
Text("Add", comment: "Button to add relay server to list.")
.font(.caption)
@@ -154,7 +170,7 @@ struct RelayView: View {
func RemoveButton(privkey: Privkey, showText: Bool) -> some View {
Button(action: {
Task { await remove_action(privkey: privkey) }
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.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()!)
ReportView(postbox: ds.nostrNetwork.postbox, target: ReportTarget.user(test_pubkey), keypair: test_keypair.to_full()!, report_sent: true, report_id: "report_id")
ReportView(postbox: ds.postbox, target: ReportTarget.user(test_pubkey), keypair: test_keypair.to_full()!, report_sent: true, report_id: "report_id")
}
}
+2 -14
View File
@@ -20,12 +20,10 @@ 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 {
@@ -130,12 +128,8 @@ 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, first_relay_list_event: first_relay_list_event, for: account)
self.save_to_storage(first_contact_event: first_contact_event, for: account)
let bootstrap_relays = load_bootstrap_relays(pubkey: account.pubkey)
for relay in bootstrap_relays {
@@ -149,15 +143,13 @@ struct SaveKeysView: View {
self.pool.connect()
}
func save_to_storage(first_contact_event: NdbNote, first_relay_list_event: NdbNote, for account: CreateAccountModel) {
func save_to_storage(first_contact_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) {
@@ -176,10 +168,6 @@ 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.nostrNetwork.postbox.send(mutelist)
appstate.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.nostrNetwork.postbox.send(mutelist)
appstate.postbox.send(mutelist)
}
var described_search: DescribedSearch {
+56 -132
View File
@@ -10,148 +10,72 @@ import SwiftUI
struct FirstAidSettingsView: View {
let damus_state: DamusState
@ObservedObject var settings: UserSettingsStore
@State var contactListInitiallyPresent: Bool = true
@State var relayListInitiallyPresent: Bool = true
@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
}
var body: some View {
Form {
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()
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)
}
)
}
.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 !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 {
if damus_state.contacts.event != nil {
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"))
}
}
}
}
}
}
}
@@ -29,14 +29,6 @@ struct NotificationSettingsView: View {
})
}
var hellthread_notification_max_pubkeys_binding: Binding<Double> {
Binding<Double>(get: {
return Double(settings.hellthread_notification_max_pubkeys)
}, set: {
settings.hellthread_notification_max_pubkeys = Int($0)
})
}
func try_to_set_notifications_mode(new_value: UserSettingsStore.NotificationsMode) {
notification_mode_setting_error = nil
if new_value == .push {
@@ -120,23 +112,6 @@ struct NotificationSettingsView: View {
// MARK: - View layout
func hellthread_notification_settings_text() -> String {
if !settings.hellthread_notifications_disabled {
return NSLocalizedString("Hide notifications that tag many profiles", comment: "Label for notification settings toggle that hides notifications that tag many people.")
}
return pluralizedString(key: "hellthread_notifications_disabled", count: $settings.hellthread_notification_max_pubkeys.wrappedValue)
}
var hellthread_notifications_max_pubkeys_view: some View {
VStack(alignment: .leading) {
Slider(
value: self.notification_preference_binding(hellthread_notification_max_pubkeys_binding),
in: Double(HELLTHREAD_MIN_PUBKEYS)...Double(HELLTHREAD_MAX_PUBKEYS),
step: 1
)
}
}
var body: some View {
Form {
if settings.enable_push_notifications {
@@ -200,13 +175,6 @@ struct NotificationSettingsView: View {
.toggleStyle(.switch)
Toggle(NSLocalizedString("Show only from users you follow", comment: "Setting to Show notifications only associated to users your follow"), isOn: self.notification_preference_binding($settings.notification_only_from_following))
.toggleStyle(.switch)
VStack {
Toggle(hellthread_notification_settings_text(), isOn: self.notification_preference_binding($settings.hellthread_notifications_disabled))
.toggleStyle(.switch)
if settings.hellthread_notifications_disabled {
hellthread_notifications_max_pubkeys_view
}
}
}
Section(
+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(state: state, relays: relays)
let relay_state = UserRelaysView.make_relay_state(pool: state.pool, relays: relays)
self._relay_state = State(initialValue: relay_state)
}
static func make_relay_state(state: DamusState, relays: [RelayURL]) -> [(RelayURL, Bool)] {
static func make_relay_state(pool: RelayPool, relays: [RelayURL]) -> [(RelayURL, Bool)] {
return relays.map({ r in
return (r, state.nostrNetwork.pool.get_relay(r) == nil)
return (r, 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.nostrNetwork.postbox.send(meta)
damus_state.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.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)
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)
return
}
}
Binary file not shown.
+16 -32
View File
@@ -50,22 +50,6 @@
<string>Following</string>
</dict>
</dict>
<key>hellthread_notifications_disabled</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@HELLTHREAD_PROFILES@</string>
<key>HELLTHREAD_PROFILES</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Hide notifications that tag more than %d profile</string>
<key>other</key>
<string>Hide notifications that tag more than %d profiles</string>
</dict>
</dict>
<key>imports_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
@@ -98,22 +82,6 @@
<string>%2$@ and %1$d others reposted</string>
</dict>
</dict>
<key>quoted_reposts_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@QUOTE_REPOSTS@</string>
<key>QUOTE_REPOSTS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Quote</string>
<key>other</key>
<string>Quotes</string>
</dict>
</dict>
<key>reacted_tagged_in_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
@@ -274,6 +242,22 @@
<string>Reposts</string>
</dict>
</dict>
<key>quoted_reposts_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@QUOTE_REPOSTS@</string>
<key>QUOTE_REPOSTS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Quote</string>
<key>other</key>
<string>Quotes</string>
</dict>
</dict>
<key>sats</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
@@ -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.3" build-num="16E140"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
</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.3" build-num="16E140"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
</header>
<body>
<trans-unit id="#%@" xml:space="preserve">
@@ -2292,16 +2292,6 @@ 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>
@@ -2315,7 +2305,8 @@ 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.</note>
<note>Navigation title for universe/search settings.
Section header for search/universe settings</note>
</trans-unit>
<trans-unit id="Secret Account Login Key" xml:space="preserve">
<source>Secret Account Login Key</source>
@@ -2464,6 +2455,11 @@ 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>
@@ -3401,7 +3397,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.3" build-num="16E140"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
</header>
<body>
<trans-unit id="/followed_by_three_and_others:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
@@ -3798,7 +3794,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.3" build-num="16E140"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
</header>
<body>
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
@@ -3820,7 +3816,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.3" build-num="16E140"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
</header>
<body>
<trans-unit id="" xml:space="preserve">
@@ -6061,16 +6057,6 @@ 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>
@@ -6084,7 +6070,8 @@ 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.</note>
<note>Navigation title for universe/search settings.
Section header for search/universe settings</note>
</trans-unit>
<trans-unit id="Secret Account Login Key" xml:space="preserve">
<source>Secret Account Login Key</source>
@@ -6243,6 +6230,11 @@ 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>
@@ -7045,7 +7037,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.3" build-num="16E140"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
</header>
<body>
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
@@ -7067,7 +7059,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.3" build-num="16E140"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="16.2" build-num="16C5032a"/>
</header>
<body>
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
@@ -1386,12 +1386,6 @@
"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."
},
@@ -1399,7 +1393,7 @@
"comment" : "Placeholder text to prompt entry of search query."
},
"Search/Universe" : {
"comment" : "Navigation title for universe/search settings."
"comment" : "Navigation title for universe/search settings.\nSection header for search/universe settings"
},
"Secret Account Login Key" : {
"comment" : "Section title for user's secret account login key."
@@ -1500,6 +1494,9 @@
"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,21 +1,14 @@
/* 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" : "16E140",
"toolBuildNumber" : "16C5032a",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "16.3"
"toolVersion" : "16.2"
},
"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 = RelayPool.RelayDescriptor.init(url: relay_url, info: .readWrite)
let relay_descriptor = RelayDescriptor.init(url: relay_url, info: .rw)
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 = RelayPool.RelayDescriptor.init(url: relay_url, info: .readWrite)
let relay_descriptor = RelayDescriptor.init(url: relay_url, info: .rw)
try! pool.add_relay(relay_descriptor)
XCTAssertEqual(pool.relays.count, 1)
let connection_expectation = XCTestExpectation(description: "Waiting for connection")
-50
View File
@@ -1,50 +0,0 @@
//
// LargeEventTests.swift
// damusTests
//
// Created by William Casarin on 2023-08-05.
//
import XCTest
@testable import damus
final class LargeEventTests: XCTestCase {
func testLongPost() throws {
let json = "[\"EVENT\",\"subid\",\(test_failing_nostr_report)]"
let resp = NostrResponse.owned_from_json(json: json)
XCTAssertNotNil(resp)
guard let resp,
case .event(let subid, let ev) = resp
else {
XCTAssertFalse(true)
return
}
XCTAssertEqual(subid, "subid")
XCTAssertTrue(ev.should_show_event)
XCTAssertTrue(!ev.too_big)
XCTAssertTrue(should_show_event(state: test_damus_state, ev: ev))
XCTAssertTrue(validate_event(ev: ev) == .ok)
}
func testIsHellthread() throws {
let json = "[\"EVENT\",\"subid\",\(test_failing_nostr_report)]"
let resp = NostrResponse.owned_from_json(json: json)
XCTAssertNotNil(resp)
guard let resp,
case .event(let subid, let ev) = resp
else {
XCTAssertFalse(true)
return
}
XCTAssertEqual(subid, "subid")
XCTAssertTrue(ev.should_show_event)
XCTAssertTrue(ev.is_hellthread(max_pubkeys: 10))
XCTAssertTrue(validate_event(ev: ev) == .ok)
}
}
+2 -5
View File
@@ -17,16 +17,13 @@ final class LocalizationUtilTests: XCTestCase {
let keys = [
["followers_count", "Followers", "Follower", "Followers"],
["following_count", "Following", "Following", "Following"],
["hellthread_notifications_disabled", "Hide notifications that tag more than 0 profiles", "Hide notifications that tag more than 1 profile", "Hide notifications that tag more than 2 profiles"],
["imports_count", "Imports", "Import", "Imports"],
["quoted_reposts_count", "Quotes", "Quote", "Quotes"],
["reactions_count", "Reactions", "Reaction", "Reactions"],
["relays_count", "Relays", "Relay", "Relays"],
["reposts_count", "Reposts", "Repost", "Reposts"],
["sats", "sats", "sat", "sats"],
["users_talking_about_it", "0 users talking about it", "1 user talking about it", "2 users talking about it"],
["word_count", "0 Words", "1 Word", "2 Words"],
["zaps_count", "Zaps", "Zap", "Zaps"]
["zaps_count", "Zaps", "Zap", "Zaps"],
["word_count", "0 Words", "1 Word", "2 Words"]
]
for key in keys {
+48
View File
@@ -0,0 +1,48 @@
//
// LongPostTests.swift
// damusTests
//
// Created by William Casarin on 2023-08-05.
//
import XCTest
@testable import damus
final class LongPostTests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testLongPost() throws {
let contacts = Contacts(our_pubkey: test_keypair.pubkey)
let json = "[\"EVENT\",\"subid\",\(test_failing_nostr_report)]"
let resp = NostrResponse.owned_from_json(json: json)
XCTAssertNotNil(resp)
guard let resp,
case .event(let subid, let ev) = resp
else {
XCTAssertFalse(true)
return
}
XCTAssertEqual(subid, "subid")
XCTAssertTrue(ev.should_show_event)
XCTAssertTrue(!ev.too_big)
XCTAssertTrue(should_show_event(state: test_damus_state, ev: ev))
XCTAssertTrue(validate_event(ev: ev) == .ok )
}
func testPerformanceExample() throws {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}
+4 -1
View File
@@ -27,7 +27,8 @@ func generate_test_damus_state(
}()
let mutelist_manager = MutelistManager(user_keypair: test_keypair)
let damus = DamusState(keypair: test_keypair,
let damus = DamusState(pool: pool,
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,
@@ -42,6 +43,8 @@ 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.nostrNetwork.postbox.send(mutelist)
test_damus_state.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,12 +170,6 @@ 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(
+3 -2
View File
@@ -20,12 +20,13 @@ final class RequestTests: XCTestCase {
func testMakeAuthRequest() {
let challenge_string = "8bc847dd-f2f6-4b3a-9c8a-71776ad9b071"
let url = RelayURL("wss://example.com")!
let relayDescriptor = RelayPool.RelayDescriptor(url: url, info: .readWrite)
let relayInfo = RelayInfo(read: true, write: true)
let relayDescriptor = RelayDescriptor(url: url, info: relayInfo)
let relayConnection = RelayConnection(url: url) { _ in
} processEvent: { _ in
}
let relay = RelayPool.Relay(descriptor: relayDescriptor, connection: relayConnection)
let relay = 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.nostrNetwork.pool.ping()
state.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.nostrNetwork.postbox.send(posted_event, on_flush: .once({ flushed_event in
state.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
@@ -14,9 +14,6 @@ import CryptoKit
let MAX_NOTE_SIZE: Int = 2 << 18
// Default threshold of the hellthread pubkey tag count setting if it is not set.
let DEFAULT_HELLTHREAD_MAX_PUBKEYS: Int = 10
struct NdbStr {
let note: NdbNote
let str: UnsafePointer<CChar>
@@ -302,15 +299,6 @@ extension NdbNote {
return !too_big
}
func is_hellthread(max_pubkeys: Int) -> Bool {
switch known_kind {
case .text, .boost, .like, .zap:
Set(referenced_pubkeys).count > max_pubkeys
default:
false
}
}
func get_blocks(keypair: Keypair) -> Blocks {
return parse_note_content(content: .init(note: self, keypair: keypair))
}
-12
View File
@@ -7,18 +7,6 @@
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
@@ -1,78 +0,0 @@
//
// 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 = RelayPool.RelayDescriptor(url: url, info: .readWrite, variant: .ephemeral)
let desc = RelayDescriptor(url: url, info: .rw, 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.nostrNetwork.pool.ping()
state.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.nostrNetwork.postbox.send(posted_event, on_flush: .once({ flushed_event in
state.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)