Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
20af086273
|
|||
| e9c1671d06 | |||
| d02847d466 | |||
| 580fa954b2 | |||
| aef516ae9f | |||
| eb4e3b692b | |||
| fe52381d63 | |||
| ab8d52e685 | |||
| 1d32200ae3 | |||
| 309b00380d | |||
| 7fa2118480 | |||
| 1a6c17e308 | |||
| 82a6046620 | |||
| 241755c8c4 | |||
| b26f66f15c | |||
| 28bd0c81e8 | |||
| 0bd1814877 | |||
| ee94f67b94 | |||
| 3a25075473 | |||
| d16ff8f78f | |||
| 38dc90cb33 | |||
| 52bbc698b2 | |||
| 496a11f597 | |||
| 4a8a0ea1bd | |||
| c424d4da99 | |||
| 69d5fc1553 | |||
| bcb59896db | |||
| e1e6d9eb3d | |||
| f1fdae5957 | |||
| f96647fa40 | |||
| 5ea522d306 | |||
| 54d6161acd | |||
| b1fd84fd75 | |||
| 9dbdf7928a | |||
| 67f0e3d296 | |||
| e498418c2d | |||
| 33150a42c5 | |||
| e7fe4ab9b4 | |||
| c146bab08a | |||
| d1cced8d54 | |||
| 8849b6105c | |||
| 3a0acfaba1 | |||
| 0ec2b05070 | |||
| 130bbfafb4 | |||
| ffc75772f9 | |||
| 5b3fac70ed | |||
| 53e3f6d86b | |||
| c28ab7a57c | |||
| 09ce3af11e | |||
| e42c09883a | |||
| 77e3924809 | |||
| 3511b1ee91 | |||
| 78a62c8ef0 | |||
| 8b96b9f4e6 | |||
| 649a857c3a | |||
| cdae2c7558 | |||
| 3639110c51 | |||
|
186668512e
|
|||
|
f63666fae2
|
|||
|
68d25059b1
|
|||
|
9aef6b7f5b
|
|||
|
d2e712575f
|
|||
|
bf9674e6e4
|
|||
|
4815390cbe
|
|||
|
6ce903f1f6
|
|||
|
b2c91ffce4
|
|||
|
ae335b18bf
|
|||
|
6391819fb2
|
|||
|
5d0e56b7c7
|
|||
|
50ccc7bd7f
|
|||
|
b3a6bcf3b2
|
|||
|
38b2988bbe
|
|||
|
446c541dcb
|
|||
|
31fd48ee52
|
|||
| b35cc33c32 |
@@ -1,3 +1,51 @@
|
||||
## [1.14] - 2025-05-25
|
||||
|
||||
### Added
|
||||
|
||||
- Added safety reminder to wallets with higher balance (Daniel D’Aquino)
|
||||
- Added one-click Coinos wallet setup (Daniel D’Aquino)
|
||||
- Add notification setting to hide hellthreads (Terry Yiu)
|
||||
- Added separated first aid option for relay lists that does not need a contact list reset (Daniel D’Aquino)
|
||||
- Added NIP-65 relay list support (Daniel D’Aquino)
|
||||
- Added Unicode 16 emoji reactions for iOS 18.4+ by upgrading EmojiPicker (Terry Yiu)
|
||||
- Added a search interface to the settings screen (SanjaySiddharth)
|
||||
- Added view introducing users to Zaps (ericholguin)
|
||||
- Added new wallet view with balance and transactions list (ericholguin)
|
||||
- Added copy technical info button to user visible errors, so that users can more easily share errors with developers (Daniel D’Aquino)
|
||||
- Add dismiss button to wallet high balance reminders (Daniel D’Aquino)
|
||||
- Zap receiver information now included for outgoing zaps (Daniel D’Aquino)
|
||||
- Added inline note rendering of invoices to pull up wallet selector sheet (Terry Yiu)
|
||||
- Added route to profile page from wallet tx list (ericholguin)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Added additional information on top of blurred images (SanjaySiddharth)
|
||||
- Improved robustness of relay list handling (Daniel D’Aquino)
|
||||
- Updated image cache for better stability (Daniel D’Aquino)
|
||||
- Improved integration with Nostr Wallet Connect wallets (ericholguin)
|
||||
- Added relay connectivity information to NWC settings (Daniel D’Aquino)
|
||||
- Improved handling around NWC responses (Daniel D’Aquino)
|
||||
- Added more human visible errors on NWC wallets to aid with troubleshooting (Daniel D’Aquino)
|
||||
- Re-enabled note zaps as permitted by the new App Store guidelines (Daniel D’Aquino)
|
||||
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Hide future notes from timeline (Terry Yiu)
|
||||
- Fixed issue where profiles with a NIP-65 relay list would not display on Damus (Daniel D’Aquino)
|
||||
- Fix quote notes to include missing q tag (Terry Yiu)
|
||||
- Fixed issue where the side menu would close when copying the npub (SanjaySiddharth)
|
||||
- Fixed issue where cached images would be backed up to iCloud (Daniel D’Aquino)
|
||||
- Optimized classify_url function (Terry Yiu)
|
||||
- Fixed note rendering for those that contain previewable items or leading and trailing whitespaces (Terry Yiu)
|
||||
- Fixed issue where some videos would become unplayable after some time using the app (Daniel D’Aquino)
|
||||
|
||||
|
||||
[1.14]: https://github.com/damus-io/damus/releases/tag/v1.14
|
||||
|
||||
|
||||
## [1.13.1] - 2025-03-21
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -18,7 +18,7 @@ struct NotificationExtensionState: HeadlessDamusState {
|
||||
let lnurls: LNUrls
|
||||
|
||||
init?() {
|
||||
guard let ndb = try? Ndb(owns_db_file: false) else { return nil }
|
||||
guard let ndb = Ndb(owns_db_file: false) else { return nil }
|
||||
self.ndb = ndb
|
||||
|
||||
guard let keypair = get_saved_keypair() else { return nil }
|
||||
|
||||
+171
-12
@@ -14,6 +14,12 @@
|
||||
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; };
|
||||
3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 3A0A30BA2C21397A00F8C9BC /* EmojiPicker */; };
|
||||
3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */; };
|
||||
3A2BAC5A2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */; };
|
||||
3A2BAC5B2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */; };
|
||||
3A2BAC5C2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */; };
|
||||
3A2BAC5E2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */; };
|
||||
3A2BAC5F2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */; };
|
||||
3A2BAC602DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */; };
|
||||
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */; };
|
||||
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */; };
|
||||
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; };
|
||||
@@ -22,6 +28,10 @@
|
||||
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */; };
|
||||
3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; };
|
||||
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; };
|
||||
3A92C0FE2DE16E9800CEEBAC /* FaviconCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A92C0FD2DE16E9800CEEBAC /* FaviconCache.swift */; };
|
||||
3A92C0FF2DE16E9800CEEBAC /* FaviconCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A92C0FD2DE16E9800CEEBAC /* FaviconCache.swift */; };
|
||||
3A92C1002DE16E9800CEEBAC /* FaviconCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A92C0FD2DE16E9800CEEBAC /* FaviconCache.swift */; };
|
||||
3A92C1022DE17ACA00CEEBAC /* NIP05DomainTimelineHeaderViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A92C1012DE17ACA00CEEBAC /* NIP05DomainTimelineHeaderViewTests.swift */; };
|
||||
3A96E3FE2D6BCE3800AE1630 /* RepostedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A96E3FD2D6BCE3800AE1630 /* RepostedTests.swift */; };
|
||||
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
|
||||
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
|
||||
@@ -33,6 +43,15 @@
|
||||
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685A297633BC00C46468 /* InfoPlist.strings */; };
|
||||
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685D297633BC00C46468 /* Localizable.strings */; };
|
||||
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; };
|
||||
3ACF94382DA9A52F00971A4E /* FaviconFinder in Frameworks */ = {isa = PBXBuildFile; productRef = 3ACF94372DA9A52F00971A4E /* FaviconFinder */; };
|
||||
3ACF943E2DA9B10800971A4E /* FaviconFinder in Frameworks */ = {isa = PBXBuildFile; productRef = 3ACF943D2DA9B10800971A4E /* FaviconFinder */; };
|
||||
3ACF94402DA9B11200971A4E /* FaviconFinder in Frameworks */ = {isa = PBXBuildFile; productRef = 3ACF943F2DA9B11200971A4E /* FaviconFinder */; };
|
||||
3ACF94422DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94412DA9FCAB00971A4E /* NIP05DomainTimelineView.swift */; };
|
||||
3ACF94432DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94412DA9FCAB00971A4E /* NIP05DomainTimelineView.swift */; };
|
||||
3ACF94442DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94412DA9FCAB00971A4E /* NIP05DomainTimelineView.swift */; };
|
||||
3ACF94462DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94452DAA006500971A4E /* NIP05DomainEventsModel.swift */; };
|
||||
3ACF94472DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94452DAA006500971A4E /* NIP05DomainEventsModel.swift */; };
|
||||
3ACF94482DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94452DAA006500971A4E /* NIP05DomainEventsModel.swift */; };
|
||||
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; };
|
||||
3CCD1E6A2A874C4E0099A953 /* Nip98HTTPAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCD1E692A874C4E0099A953 /* Nip98HTTPAuth.swift */; };
|
||||
4C011B5E2BD0A56A002F2F9B /* ChatEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C011B5C2BD0A56A002F2F9B /* ChatEventView.swift */; };
|
||||
@@ -642,7 +661,6 @@
|
||||
82D6FB6C2CD99F7900C925F4 /* DamusPurpleURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADD3DD2B53854300F104C4 /* DamusPurpleURL.swift */; };
|
||||
82D6FB6D2CD99F7900C925F4 /* DamusPurpleEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72341182B6864F200E1E135 /* DamusPurpleEnvironment.swift */; };
|
||||
82D6FB6E2CD99F7900C925F4 /* PurpleStoreKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C5D2B7709ED00C59298 /* PurpleStoreKitManager.swift */; };
|
||||
82D6FB6F2CD99F7900C925F4 /* CameraService+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */; };
|
||||
82D6FB702CD99F7900C925F4 /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; };
|
||||
82D6FB712CD99F7900C925F4 /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; };
|
||||
82D6FB722CD99F7900C925F4 /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; };
|
||||
@@ -933,7 +951,6 @@
|
||||
BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; };
|
||||
BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; };
|
||||
BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; };
|
||||
BA3759922ABCCEBA0018D73B /* CameraService+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */; };
|
||||
BA3759932ABCCEBA0018D73B /* CameraModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759902ABCCEBA0018D73B /* CameraModel.swift */; };
|
||||
BA3759942ABCCEBA0018D73B /* CameraService.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759912ABCCEBA0018D73B /* CameraService.swift */; };
|
||||
BA3759972ABCCF360018D73B /* CameraPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759962ABCCF360018D73B /* CameraPreview.swift */; };
|
||||
@@ -1092,6 +1109,13 @@
|
||||
D72E127A2BEEEED000F4F781 /* NostrFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */; };
|
||||
D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; };
|
||||
D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; };
|
||||
D733F9E12D92C1D900317B11 /* SubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E02D92C1AA00317B11 /* SubscriptionManager.swift */; };
|
||||
D733F9E22D92C1D900317B11 /* SubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E02D92C1AA00317B11 /* SubscriptionManager.swift */; };
|
||||
D733F9E32D92C1D900317B11 /* SubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E02D92C1AA00317B11 /* SubscriptionManager.swift */; };
|
||||
D733F9E52D92C76100317B11 /* UnownedNdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */; };
|
||||
D733F9E62D92C76100317B11 /* UnownedNdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */; };
|
||||
D733F9E72D92C76100317B11 /* UnownedNdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */; };
|
||||
D733F9E82D92C76100317B11 /* UnownedNdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */; };
|
||||
D734B1452CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */ = {isa = PBXBuildFile; fileRef = D734B1442CCC19B1000B5C97 /* DamusFullScreenCover.swift */; };
|
||||
D734B1462CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */ = {isa = PBXBuildFile; fileRef = D734B1442CCC19B1000B5C97 /* DamusFullScreenCover.swift */; };
|
||||
D7373BA62B688EA300F7783D /* DamusPurpleTranslationSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7373BA52B688EA200F7783D /* DamusPurpleTranslationSetupView.swift */; };
|
||||
@@ -1100,6 +1124,15 @@
|
||||
D73B74E12D8365BA0067BDBC /* ExtraFonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73B74E02D8365B40067BDBC /* ExtraFonts.swift */; };
|
||||
D73B74E22D8365BA0067BDBC /* ExtraFonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73B74E02D8365B40067BDBC /* ExtraFonts.swift */; };
|
||||
D73B74E32D8365BA0067BDBC /* ExtraFonts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73B74E02D8365B40067BDBC /* ExtraFonts.swift */; };
|
||||
D73BDB0D2D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB0C2D6FF58600D69970 /* NostrNetworkManager.swift */; };
|
||||
D73BDB0E2D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB0C2D6FF58600D69970 /* NostrNetworkManager.swift */; };
|
||||
D73BDB102D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB0C2D6FF58600D69970 /* NostrNetworkManager.swift */; };
|
||||
D73BDB142D71216500D69970 /* UserRelayListManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB132D71215F00D69970 /* UserRelayListManager.swift */; };
|
||||
D73BDB152D71216500D69970 /* UserRelayListManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB132D71215F00D69970 /* UserRelayListManager.swift */; };
|
||||
D73BDB162D71216500D69970 /* UserRelayListManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB132D71215F00D69970 /* UserRelayListManager.swift */; };
|
||||
D73BDB182D71311900D69970 /* UserRelayListErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB172D71310C00D69970 /* UserRelayListErrors.swift */; };
|
||||
D73BDB192D71311900D69970 /* UserRelayListErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB172D71310C00D69970 /* UserRelayListErrors.swift */; };
|
||||
D73BDB1A2D71311900D69970 /* UserRelayListErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73BDB172D71310C00D69970 /* UserRelayListErrors.swift */; };
|
||||
D73E5E162C6A9619007EB227 /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; };
|
||||
D73E5E172C6A962A007EB227 /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; };
|
||||
D73E5E182C6A963D007EB227 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; };
|
||||
@@ -1209,7 +1242,6 @@
|
||||
D73E5E882C6A97F4007EB227 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; };
|
||||
D73E5E892C6A97F4007EB227 /* DamusPurpleURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADD3DD2B53854300F104C4 /* DamusPurpleURL.swift */; };
|
||||
D73E5E8A2C6A97F4007EB227 /* PurpleStoreKitManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7100C5D2B7709ED00C59298 /* PurpleStoreKitManager.swift */; };
|
||||
D73E5E8D2C6A97F4007EB227 /* CameraService+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */; };
|
||||
D73E5E8E2C6A97F4007EB227 /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; };
|
||||
D73E5E8F2C6A97F4007EB227 /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; };
|
||||
D73E5E902C6A97F4007EB227 /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; };
|
||||
@@ -1466,6 +1498,9 @@
|
||||
D74AAFD22B155E78006CF0F4 /* WalletConnect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09612A098D0E00943473 /* WalletConnect.swift */; };
|
||||
D74AAFD42B155ECB006CF0F4 /* Zaps+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */; };
|
||||
D74AAFD62B155F0C006CF0F4 /* WalletConnect+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */; };
|
||||
D74E64132DC95CC7004C7892 /* HumanReadableErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74E64112DC95CBE004C7892 /* HumanReadableErrors.swift */; };
|
||||
D74E64142DC95CC7004C7892 /* HumanReadableErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74E64112DC95CBE004C7892 /* HumanReadableErrors.swift */; };
|
||||
D74E64152DC95CC7004C7892 /* HumanReadableErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74E64112DC95CBE004C7892 /* HumanReadableErrors.swift */; };
|
||||
D74EA08A2D2BF2A7002290DD /* URLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D767066E2C8BB3CE00F09726 /* URLHandler.swift */; };
|
||||
D74EA08E2D2E271E002290DD /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA08D2D2E271E002290DD /* ErrorView.swift */; };
|
||||
D74EA08F2D2E271E002290DD /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA08D2D2E271E002290DD /* ErrorView.swift */; };
|
||||
@@ -1636,6 +1671,9 @@
|
||||
D7CE1B472B0BE719002EDAD4 /* NativeObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B9462A9AD44700DC3548 /* NativeObject.swift */; };
|
||||
D7CE1B482B0BE719002EDAD4 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B93D2A9AD44700DC3548 /* Message.swift */; };
|
||||
D7CE1B492B0BE729002EDAD4 /* DisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */; };
|
||||
D7D09AB52DADCA5C00AB170D /* CoinosDeterministicAccountClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D09AB42DADCA5600AB170D /* CoinosDeterministicAccountClient.swift */; };
|
||||
D7D09AB62DADCA5C00AB170D /* CoinosDeterministicAccountClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D09AB42DADCA5600AB170D /* CoinosDeterministicAccountClient.swift */; };
|
||||
D7D09AB72DADCA5C00AB170D /* CoinosDeterministicAccountClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D09AB42DADCA5600AB170D /* CoinosDeterministicAccountClient.swift */; };
|
||||
D7D2A3812BF815D000E4B42B /* PushNotificationClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */; };
|
||||
D7D68FF92C9E01BE0015A515 /* KFClickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D68FF82C9E01B60015A515 /* KFClickable.swift */; };
|
||||
D7D68FFA2C9E01BE0015A515 /* KFClickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D68FF82C9E01B60015A515 /* KFClickable.swift */; };
|
||||
@@ -1652,6 +1690,9 @@
|
||||
D7DB93052D66A44100DA1EE5 /* Undistractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */; };
|
||||
D7DB93062D66A44100DA1EE5 /* Undistractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */; };
|
||||
D7DB93072D66A44100DA1EE5 /* Undistractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */; };
|
||||
D7DB930A2D69486700DA1EE5 /* NIP65.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93092D69485A00DA1EE5 /* NIP65.swift */; };
|
||||
D7DB930B2D69486700DA1EE5 /* NIP65.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93092D69485A00DA1EE5 /* NIP65.swift */; };
|
||||
D7DB930C2D69486700DA1EE5 /* NIP65.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93092D69485A00DA1EE5 /* NIP65.swift */; };
|
||||
D7DBD41F2B02F15E002A6197 /* NostrKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */; };
|
||||
D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */; };
|
||||
D7EB00B02CD59C8D00660C07 /* PresentFullScreenItemNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */; };
|
||||
@@ -1684,6 +1725,9 @@
|
||||
D7F360262CEBBD8B009D34DA /* PresentFullScreenItemNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */; };
|
||||
D7F360272CEBBDC0009D34DA /* DamusVideoControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EFBA362CC322F300F45588 /* DamusVideoControlsView.swift */; };
|
||||
D7F360292CEBBE34009D34DA /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = D7F360282CEBBE34009D34DA /* CodeScanner */; };
|
||||
D7FA46E52DBDAA7E002C9BB0 /* ImageCacheMigrations.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FA46E42DBDAA75002C9BB0 /* ImageCacheMigrations.swift */; };
|
||||
D7FA46E62DBDAA7E002C9BB0 /* ImageCacheMigrations.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FA46E42DBDAA75002C9BB0 /* ImageCacheMigrations.swift */; };
|
||||
D7FA46E72DBDAA7E002C9BB0 /* ImageCacheMigrations.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FA46E42DBDAA75002C9BB0 /* ImageCacheMigrations.swift */; };
|
||||
D7FB10A72B0C371A00FA8D42 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2B10272A7B0F5C008AA43E /* Log.swift */; };
|
||||
D7FB14222BE5970000398331 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D7FB14212BE5970000398331 /* PrivacyInfo.xcprivacy */; };
|
||||
D7FB14252BE5A9A800398331 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D7FB14242BE5A9A800398331 /* PrivacyInfo.xcprivacy */; };
|
||||
@@ -1783,6 +1827,8 @@
|
||||
3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A2B8B0A296A8982009CC16D /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "en-US"; path = "en-US.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainTimelineHeaderView.swift; sourceTree = "<group>"; };
|
||||
3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainPubkeysView.swift; sourceTree = "<group>"; };
|
||||
3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyDescriptionTests.swift; sourceTree = "<group>"; };
|
||||
3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationUtil.swift; sourceTree = "<group>"; };
|
||||
3A3040F229A91366008A0F29 /* ProfileViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewTests.swift; sourceTree = "<group>"; };
|
||||
@@ -1828,6 +1874,8 @@
|
||||
3A929C20297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A929C21297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A929C22297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "it-IT"; path = "it-IT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A92C0FD2DE16E9800CEEBAC /* FaviconCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconCache.swift; sourceTree = "<group>"; };
|
||||
3A92C1012DE17ACA00CEEBAC /* NIP05DomainTimelineHeaderViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainTimelineHeaderViewTests.swift; sourceTree = "<group>"; };
|
||||
3A93342929884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pl-PL"; path = "pl-PL.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A93342A29884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pl-PL"; path = "pl-PL.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A93342B29884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pl-PL"; path = "pl-PL.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
@@ -1866,6 +1914,8 @@
|
||||
3ACB685B297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3ACB685E297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeAgoTests.swift; sourceTree = "<group>"; };
|
||||
3ACF94412DA9FCAB00971A4E /* NIP05DomainTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainTimelineView.swift; sourceTree = "<group>"; };
|
||||
3ACF94452DAA006500971A4E /* NIP05DomainEventsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainEventsModel.swift; sourceTree = "<group>"; };
|
||||
3AD14EB529C40F38009D2D9C /* hu-HU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "hu-HU"; path = "hu-HU.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3AD14EB629C40F38009D2D9C /* hu-HU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hu-HU"; path = "hu-HU.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3AD14EB729C40F38009D2D9C /* hu-HU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hu-HU"; path = "hu-HU.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
@@ -2442,7 +2492,6 @@
|
||||
BA3759892ABCCDE30018D73B /* ImageResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageResizer.swift; sourceTree = "<group>"; };
|
||||
BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureProcessor.swift; sourceTree = "<group>"; };
|
||||
BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCaptureProcessor.swift; sourceTree = "<group>"; };
|
||||
BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "CameraService+Extensions.swift"; sourceTree = "<group>"; };
|
||||
BA3759902ABCCEBA0018D73B /* CameraModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraModel.swift; sourceTree = "<group>"; };
|
||||
BA3759912ABCCEBA0018D73B /* CameraService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraService.swift; sourceTree = "<group>"; };
|
||||
BA3759962ABCCF360018D73B /* CameraPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPreview.swift; sourceTree = "<group>"; };
|
||||
@@ -2478,11 +2527,16 @@
|
||||
D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrFilterTests.swift; sourceTree = "<group>"; };
|
||||
D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = "<group>"; };
|
||||
D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = "<group>"; };
|
||||
D733F9E02D92C1AA00317B11 /* SubscriptionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionManager.swift; sourceTree = "<group>"; };
|
||||
D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnownedNdbNote.swift; sourceTree = "<group>"; };
|
||||
D734B1442CCC19B1000B5C97 /* DamusFullScreenCover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusFullScreenCover.swift; sourceTree = "<group>"; };
|
||||
D7373BA52B688EA200F7783D /* DamusPurpleTranslationSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleTranslationSetupView.swift; sourceTree = "<group>"; };
|
||||
D7373BA72B68974500F7783D /* DamusPurpleNewUserOnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleNewUserOnboardingView.swift; sourceTree = "<group>"; };
|
||||
D7373BA92B68A65A00F7783D /* PurpleAccountUpdateNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurpleAccountUpdateNotify.swift; sourceTree = "<group>"; };
|
||||
D73B74E02D8365B40067BDBC /* ExtraFonts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtraFonts.swift; sourceTree = "<group>"; };
|
||||
D73BDB0C2D6FF58600D69970 /* NostrNetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrNetworkManager.swift; sourceTree = "<group>"; };
|
||||
D73BDB132D71215F00D69970 /* UserRelayListManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRelayListManager.swift; sourceTree = "<group>"; };
|
||||
D73BDB172D71310C00D69970 /* UserRelayListErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserRelayListErrors.swift; sourceTree = "<group>"; };
|
||||
D73E5F7E2C6AA066007EB227 /* DamusAliases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusAliases.swift; sourceTree = "<group>"; };
|
||||
D73E5F802C6AA07A007EB227 /* HighlighterExtensionAliases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlighterExtensionAliases.swift; sourceTree = "<group>"; };
|
||||
D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadlessDamusState.swift; sourceTree = "<group>"; };
|
||||
@@ -2491,6 +2545,7 @@
|
||||
D74AAFCE2B155D8C006CF0F4 /* ZapDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapDataModel.swift; sourceTree = "<group>"; };
|
||||
D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Zaps+.swift"; sourceTree = "<group>"; };
|
||||
D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WalletConnect+.swift"; sourceTree = "<group>"; };
|
||||
D74E64112DC95CBE004C7892 /* HumanReadableErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HumanReadableErrors.swift; sourceTree = "<group>"; };
|
||||
D74EA08D2D2E271E002290DD /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
|
||||
D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableNostrEventView.swift; sourceTree = "<group>"; };
|
||||
D74F43092B23F0BE00425B75 /* DamusPurple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurple.swift; sourceTree = "<group>"; };
|
||||
@@ -2534,6 +2589,7 @@
|
||||
D7CB5D5E2B11770C00AD4105 /* FollowState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowState.swift; sourceTree = "<group>"; };
|
||||
D7CBD1D32B8D21DC00BFD889 /* DamusPurpleNotificationManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleNotificationManagement.swift; sourceTree = "<group>"; };
|
||||
D7CBD1D52B8D509800BFD889 /* DamusPurpleImpendingExpirationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleImpendingExpirationTests.swift; sourceTree = "<group>"; };
|
||||
D7D09AB42DADCA5600AB170D /* CoinosDeterministicAccountClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoinosDeterministicAccountClient.swift; sourceTree = "<group>"; };
|
||||
D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationClient.swift; sourceTree = "<group>"; };
|
||||
D7D68FF82C9E01B60015A515 /* KFClickable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFClickable.swift; sourceTree = "<group>"; };
|
||||
D7DB1FDD2D5A78CE00CF06DA /* NIP44.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP44.swift; sourceTree = "<group>"; };
|
||||
@@ -2541,6 +2597,7 @@
|
||||
D7DB1FF02D5AC5D700CF06DA /* nip44.vectors.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = nip44.vectors.json; sourceTree = "<group>"; };
|
||||
D7DB1FF22D5AC5E400CF06DA /* LICENSES */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSES; sourceTree = "<group>"; };
|
||||
D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Undistractor.swift; sourceTree = "<group>"; };
|
||||
D7DB93092D69485A00DA1EE5 /* NIP65.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP65.swift; sourceTree = "<group>"; };
|
||||
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = "<group>"; };
|
||||
D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentFullScreenItemNotify.swift; sourceTree = "<group>"; };
|
||||
D7EDED1B2B1178FE0018B19C /* NoteContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContent.swift; sourceTree = "<group>"; };
|
||||
@@ -2549,6 +2606,7 @@
|
||||
D7EDED2D2B128E8A0018B19C /* CollectionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtension.swift; sourceTree = "<group>"; };
|
||||
D7EDED322B12ACAE0018B19C /* DamusUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusUserDefaults.swift; sourceTree = "<group>"; };
|
||||
D7EFBA362CC322F300F45588 /* DamusVideoControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusVideoControlsView.swift; sourceTree = "<group>"; };
|
||||
D7FA46E42DBDAA75002C9BB0 /* ImageCacheMigrations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCacheMigrations.swift; sourceTree = "<group>"; };
|
||||
D7FB14212BE5970000398331 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
D7FB14242BE5A9A800398331 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
D7FD12252BD345A700CF195B /* FirstAidSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstAidSettingsView.swift; sourceTree = "<group>"; };
|
||||
@@ -2584,6 +2642,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3ACF94382DA9A52F00971A4E /* FaviconFinder in Frameworks */,
|
||||
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
|
||||
D7DB1FE42D5A9AC900CF06DA /* CryptoSwift in Frameworks */,
|
||||
3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */,
|
||||
@@ -2615,6 +2674,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3ACF94402DA9B11200971A4E /* FaviconFinder in Frameworks */,
|
||||
82D6FC862CD9A4A600C925F4 /* MarkdownUI in Frameworks */,
|
||||
D7DB1FEC2D5A9F6500CF06DA /* CryptoSwift in Frameworks */,
|
||||
82D6FC8A2CD9A54600C925F4 /* SwipeActions in Frameworks */,
|
||||
@@ -2631,6 +2691,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D703D7AF2C670FB700A400EA /* MarkdownUI in Frameworks */,
|
||||
3ACF943E2DA9B10800971A4E /* FaviconFinder in Frameworks */,
|
||||
D73E5F9D2C6AA8E3007EB227 /* SwipeActions in Frameworks */,
|
||||
D7DB1FE82D5A9F5300CF06DA /* CryptoSwift in Frameworks */,
|
||||
D73E5F762C6A997E007EB227 /* EmojiPicker in Frameworks */,
|
||||
@@ -2745,6 +2806,7 @@
|
||||
4C0A3F8D280F63FF000448DE /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D73BDB122D71212600D69970 /* NostrNetworkManager */,
|
||||
D74F43082B23F09300425B75 /* Purple */,
|
||||
BA3759882ABCCDE30018D73B /* Camera */,
|
||||
4C190F1E2A535FC200027FD5 /* Zaps */,
|
||||
@@ -2807,6 +2869,7 @@
|
||||
5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */,
|
||||
D773BC5E2C6D538500349F0A /* CommentItem.swift */,
|
||||
D767066E2C8BB3CE00F09726 /* URLHandler.swift */,
|
||||
3ACF94452DAA006500971A4E /* NIP05DomainEventsModel.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
@@ -3221,6 +3284,9 @@
|
||||
D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */,
|
||||
D71AD8FC2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift */,
|
||||
D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */,
|
||||
3ACF94412DA9FCAB00971A4E /* NIP05DomainTimelineView.swift */,
|
||||
3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */,
|
||||
3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@@ -3286,6 +3352,7 @@
|
||||
4C7FF7D628233637009601DB /* Util */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D7FA46E42DBDAA75002C9BB0 /* ImageCacheMigrations.swift */,
|
||||
D73B74E02D8365B40067BDBC /* ExtraFonts.swift */,
|
||||
D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */,
|
||||
D73E5F7E2C6AA066007EB227 /* DamusAliases.swift */,
|
||||
@@ -3341,6 +3408,8 @@
|
||||
D7EDED2D2B128E8A0018B19C /* CollectionExtension.swift */,
|
||||
D74AAFCE2B155D8C006CF0F4 /* ZapDataModel.swift */,
|
||||
D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */,
|
||||
D7D09AB42DADCA5600AB170D /* CoinosDeterministicAccountClient.swift */,
|
||||
3A92C0FD2DE16E9800CEEBAC /* FaviconCache.swift */,
|
||||
);
|
||||
path = Util;
|
||||
sourceTree = "<group>";
|
||||
@@ -3359,6 +3428,7 @@
|
||||
4C9054862A6AEB4500811EEC /* nostrdb */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D733F9E42D92C75C00317B11 /* UnownedNdbNote.swift */,
|
||||
4C47928D2A9939BD00489948 /* flatcc */,
|
||||
4C478E2A2A9935D300489948 /* bindings */,
|
||||
4CE9FBBB2A6B3D9C007E485C /* Test */,
|
||||
@@ -3650,6 +3720,7 @@
|
||||
4CE6DEE527F7A08100C66700 /* damus */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D7DB93082D69478400DA1EE5 /* NIP65 */,
|
||||
D7DB1FDC2D5A77E500CF06DA /* NIP44 */,
|
||||
D755B28B2D3E7D6500BBEEFA /* NIP37 */,
|
||||
D78F08152D7F7F5F00FC6C75 /* NIP04 */,
|
||||
@@ -3734,6 +3805,7 @@
|
||||
4C2D34402BDAF1B300F9FB44 /* NIP10Tests.swift */,
|
||||
D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */,
|
||||
3A96E3FD2D6BCE3800AE1630 /* RepostedTests.swift */,
|
||||
3A92C1012DE17ACA00CEEBAC /* NIP05DomainTimelineHeaderViewTests.swift */,
|
||||
);
|
||||
path = damusTests;
|
||||
sourceTree = "<group>";
|
||||
@@ -3893,7 +3965,6 @@
|
||||
children = (
|
||||
BA3759902ABCCEBA0018D73B /* CameraModel.swift */,
|
||||
BA3759912ABCCEBA0018D73B /* CameraService.swift */,
|
||||
BA37598F2ABCCEBA0018D73B /* CameraService+Extensions.swift */,
|
||||
BA3759892ABCCDE30018D73B /* ImageResizer.swift */,
|
||||
BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */,
|
||||
BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */,
|
||||
@@ -3953,6 +4024,17 @@
|
||||
path = Mocking;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D73BDB122D71212600D69970 /* NostrNetworkManager */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D733F9E02D92C1AA00317B11 /* SubscriptionManager.swift */,
|
||||
D73BDB172D71310C00D69970 /* UserRelayListErrors.swift */,
|
||||
D73BDB132D71215F00D69970 /* UserRelayListManager.swift */,
|
||||
D73BDB0C2D6FF58600D69970 /* NostrNetworkManager.swift */,
|
||||
);
|
||||
path = NostrNetworkManager;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D74EA08C2D2E26E6002290DD /* ErrorHandling */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -3996,6 +4078,7 @@
|
||||
D78F080A2D7F78B000FC6C75 /* WalletConnect */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D74E64112DC95CBE004C7892 /* HumanReadableErrors.swift */,
|
||||
D78F08102D7F78F600FC6C75 /* Response.swift */,
|
||||
D78F080B2D7F78EB00FC6C75 /* Request.swift */,
|
||||
4C7D09612A098D0E00943473 /* WalletConnect.swift */,
|
||||
@@ -4049,6 +4132,14 @@
|
||||
path = NIP44;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D7DB93082D69478400DA1EE5 /* NIP65 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D7DB93092D69485A00DA1EE5 /* NIP65.swift */,
|
||||
);
|
||||
path = NIP65;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E06336A72B7582D600A88E6B /* Assets */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -4116,6 +4207,7 @@
|
||||
D70D90972CDED61800CD0534 /* CodeScanner */,
|
||||
D7C48C0A2D12DE0C00A3BACF /* SwiftyCrop */,
|
||||
D7DB1FE32D5A9AC900CF06DA /* CryptoSwift */,
|
||||
3ACF94372DA9A52F00971A4E /* FaviconFinder */,
|
||||
);
|
||||
productName = damus;
|
||||
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
|
||||
@@ -4183,6 +4275,7 @@
|
||||
D7F360282CEBBE34009D34DA /* CodeScanner */,
|
||||
D7C48C0C2D12E34900A3BACF /* SwiftyCrop */,
|
||||
D7DB1FEB2D5A9F6500CF06DA /* CryptoSwift */,
|
||||
3ACF943F2DA9B11200971A4E /* FaviconFinder */,
|
||||
);
|
||||
productName = "share extension";
|
||||
productReference = 82D6FA972CD9820500C925F4 /* ShareExtension.appex */;
|
||||
@@ -4212,6 +4305,7 @@
|
||||
D70D909B2CDED7B200CD0534 /* CodeScanner */,
|
||||
D7C48C0E2D12E35600A3BACF /* SwiftyCrop */,
|
||||
D7DB1FE72D5A9F5300CF06DA /* CryptoSwift */,
|
||||
3ACF943D2DA9B10800971A4E /* FaviconFinder */,
|
||||
);
|
||||
productName = "highlighter action extension";
|
||||
productReference = D703D7172C66E47100A400EA /* HighlighterActionExtension.appex */;
|
||||
@@ -4324,6 +4418,7 @@
|
||||
D70D90962CDED61800CD0534 /* XCRemoteSwiftPackageReference "CodeScanner" */,
|
||||
D7C48C092D12DE0C00A3BACF /* XCRemoteSwiftPackageReference "SwiftyCrop" */,
|
||||
D7DB1FE22D5A9AC900CF06DA /* XCRemoteSwiftPackageReference "CryptoSwift" */,
|
||||
3ACF94362DA9A52F00971A4E /* XCRemoteSwiftPackageReference "FaviconFinder" */,
|
||||
);
|
||||
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -4438,6 +4533,7 @@
|
||||
4C3DCC762A9FE9EC0091E592 /* NdbTxn.swift in Sources */,
|
||||
4CEF958D2A9CE650000F901B /* verifier.c in Sources */,
|
||||
4C32B9342A9AD01A00DC3548 /* NdbProfile.swift in Sources */,
|
||||
D73BDB0E2D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */,
|
||||
4C32B9332A99845B00DC3548 /* Ndb.swift in Sources */,
|
||||
D7ADD3E22B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift in Sources */,
|
||||
4C4793082A993E8900489948 /* refmap.c in Sources */,
|
||||
@@ -4465,6 +4561,7 @@
|
||||
4C3EA64428FF558100C48A62 /* sha256.c in Sources */,
|
||||
5C8498032D5D150000F74FEB /* ZapExplainer.swift in Sources */,
|
||||
504323A72A34915F006AE6DC /* RelayModel.swift in Sources */,
|
||||
3A2BAC5C2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */,
|
||||
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */,
|
||||
4C32B9542A9AD44700DC3548 /* FlatBuffersUtils.swift in Sources */,
|
||||
D7EDED1C2B1178FE0018B19C /* NoteContent.swift in Sources */,
|
||||
@@ -4485,6 +4582,7 @@
|
||||
F757933A29D7AECD007DEAC1 /* MediaPicker.swift in Sources */,
|
||||
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */,
|
||||
B57B4C662B312C3700A232C0 /* NostrAuth.swift in Sources */,
|
||||
D7DB930B2D69486700DA1EE5 /* NIP65.swift in Sources */,
|
||||
4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */,
|
||||
4C3EA67728FF7A9800C48A62 /* talstr.c in Sources */,
|
||||
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
|
||||
@@ -4502,6 +4600,7 @@
|
||||
4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */,
|
||||
4C363AA228296A7E006E126D /* SearchView.swift in Sources */,
|
||||
D798D2282B085CDA00234419 /* NdbNote+.swift in Sources */,
|
||||
3ACF94422DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */,
|
||||
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */,
|
||||
4C1253662A76D0FF0004F4B8 /* OnlyZapsNotify.swift in Sources */,
|
||||
4CA927652A290F1A0098A105 /* TimeDot.swift in Sources */,
|
||||
@@ -4571,12 +4670,12 @@
|
||||
4C32B9582A9AD44700DC3548 /* VeriferOptions.swift in Sources */,
|
||||
D74AAFC22B153395006CF0F4 /* HeadlessDamusState.swift in Sources */,
|
||||
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
|
||||
D73BDB1A2D71311900D69970 /* UserRelayListErrors.swift in Sources */,
|
||||
4C30AC7629A5770900E2BD5A /* NotificationItemView.swift in Sources */,
|
||||
4C86F7C42A76C44C00EC0817 /* ZappingNotify.swift in Sources */,
|
||||
4C363A8428233689006E126D /* Parser.swift in Sources */,
|
||||
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */,
|
||||
4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */,
|
||||
BA3759922ABCCEBA0018D73B /* CameraService+Extensions.swift in Sources */,
|
||||
D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */,
|
||||
4C363A9A28283854006E126D /* Reply.swift in Sources */,
|
||||
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */,
|
||||
@@ -4608,6 +4707,7 @@
|
||||
D78DB85B2C20FE5000F0AB12 /* VectorMath.swift in Sources */,
|
||||
D7CB5D3E2B116DAD00AD4105 /* NotificationsManager.swift in Sources */,
|
||||
50A16FFF2AA76A0900DFEC1F /* DamusVideoCoordinator.swift in Sources */,
|
||||
D733F9E32D92C1D900317B11 /* SubscriptionManager.swift in Sources */,
|
||||
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */,
|
||||
4C285C8228385570008A31F1 /* CarouselView.swift in Sources */,
|
||||
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */,
|
||||
@@ -4625,6 +4725,7 @@
|
||||
4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */,
|
||||
4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */,
|
||||
BA3759932ABCCEBA0018D73B /* CameraModel.swift in Sources */,
|
||||
D7FA46E72DBDAA7E002C9BB0 /* ImageCacheMigrations.swift in Sources */,
|
||||
D7100C5A2B76FD5100C59298 /* LogoView.swift in Sources */,
|
||||
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
|
||||
4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */,
|
||||
@@ -4638,6 +4739,7 @@
|
||||
D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */,
|
||||
D7373BA82B68974500F7783D /* DamusPurpleNewUserOnboardingView.swift in Sources */,
|
||||
4C7D09682A0AE9B200943473 /* NWCScannerView.swift in Sources */,
|
||||
D74E64152DC95CC7004C7892 /* HumanReadableErrors.swift in Sources */,
|
||||
D7CB5D452B116FE800AD4105 /* Contacts+.swift in Sources */,
|
||||
4CA352A42A76AFF3003BB08B /* UpdateStatsNotify.swift in Sources */,
|
||||
D798D21E2B0858BB00234419 /* MigratedTypes.swift in Sources */,
|
||||
@@ -4695,6 +4797,7 @@
|
||||
4C12535E2A76CA870004F4B8 /* SwitchedTimelineNotify.swift in Sources */,
|
||||
D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */,
|
||||
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */,
|
||||
3ACF94462DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */,
|
||||
D734B1452CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */,
|
||||
4C4E137D2A76D63600BDD832 /* UnmuteThreadNotify.swift in Sources */,
|
||||
D706C5B72D602A110027C627 /* QueueableNotify.swift in Sources */,
|
||||
@@ -4716,6 +4819,7 @@
|
||||
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */,
|
||||
D73E5F7F2C6AA066007EB227 /* DamusAliases.swift in Sources */,
|
||||
4C1A9A2A29DDF54400516EAC /* DamusVideoPlayerView.swift in Sources */,
|
||||
D73BDB152D71216500D69970 /* UserRelayListManager.swift in Sources */,
|
||||
4CA352A22A76AEC5003BB08B /* LikedNotify.swift in Sources */,
|
||||
5CC8529F2BD744F60039FFC5 /* HighlightView.swift in Sources */,
|
||||
BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */,
|
||||
@@ -4813,6 +4917,7 @@
|
||||
4CF38C882A9442DC00BE01B6 /* UserStatusView.swift in Sources */,
|
||||
4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */,
|
||||
4C1253582A76C9060004F4B8 /* PresentSheetNotify.swift in Sources */,
|
||||
D7D09AB52DADCA5C00AB170D /* CoinosDeterministicAccountClient.swift in Sources */,
|
||||
D773BC5F2C6D538500349F0A /* CommentItem.swift in Sources */,
|
||||
4C363A962827096D006E126D /* PostBlock.swift in Sources */,
|
||||
4CA9275F2A2902B20098A105 /* LongformPreview.swift in Sources */,
|
||||
@@ -4852,6 +4957,7 @@
|
||||
4CE4F0F229D4FCFA005914DB /* DebouncedOnChange.swift in Sources */,
|
||||
4C32B9592A9AD44700DC3548 /* Table.swift in Sources */,
|
||||
4C5D5C9D2A6B2CB40024563C /* AsciiCharacter.swift in Sources */,
|
||||
3A2BAC5E2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */,
|
||||
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
|
||||
4C9146FE2A2A87C200DDEA40 /* nostrscript.c in Sources */,
|
||||
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
|
||||
@@ -4899,6 +5005,7 @@
|
||||
4C32B95A2A9AD44700DC3548 /* Verifiable.swift in Sources */,
|
||||
4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */,
|
||||
501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */,
|
||||
3A92C0FE2DE16E9800CEEBAC /* FaviconCache.swift in Sources */,
|
||||
4C1A9A1D29DDCF9B00516EAC /* NotificationSettingsView.swift in Sources */,
|
||||
5CC868DD2AA29B3200FB22BA /* NeutralButtonStyle.swift in Sources */,
|
||||
4C75EFB528049D790006080F /* Relay.swift in Sources */,
|
||||
@@ -4919,6 +5026,7 @@
|
||||
4C9AA14A2A4587A6003F49FD /* NotificationStatusModel.swift in Sources */,
|
||||
D7100C5C2B77016700C59298 /* IAPProductStateView.swift in Sources */,
|
||||
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
|
||||
D733F9E82D92C76100317B11 /* UnownedNdbNote.swift in Sources */,
|
||||
D74EA0902D2E271E002290DD /* ErrorView.swift in Sources */,
|
||||
4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */,
|
||||
BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */,
|
||||
@@ -4980,6 +5088,7 @@
|
||||
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */,
|
||||
4C684A552A7E91FE005E6031 /* LargeEventTests.swift in Sources */,
|
||||
E02B54182B4DFADA0077FF42 /* Bech32ObjectTests.swift in Sources */,
|
||||
3A92C1022DE17ACA00CEEBAC /* NIP05DomainTimelineHeaderViewTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -5065,6 +5174,7 @@
|
||||
82D6FAE72CD99F7900C925F4 /* LoginNotify.swift in Sources */,
|
||||
82D6FAE82CD99F7900C925F4 /* LogoutNotify.swift in Sources */,
|
||||
D706C5B12D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */,
|
||||
3ACF94482DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */,
|
||||
82D6FAE92CD99F7900C925F4 /* NewMutesNotify.swift in Sources */,
|
||||
82D6FAEA2CD99F7900C925F4 /* NewUnmutesNotify.swift in Sources */,
|
||||
82D6FAEB2CD99F7900C925F4 /* Notify.swift in Sources */,
|
||||
@@ -5072,6 +5182,7 @@
|
||||
82D6FAED2CD99F7900C925F4 /* PostNotify.swift in Sources */,
|
||||
82D6FAEE2CD99F7900C925F4 /* PresentSheetNotify.swift in Sources */,
|
||||
D74EA0932D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */,
|
||||
D74E64142DC95CC7004C7892 /* HumanReadableErrors.swift in Sources */,
|
||||
82D6FAEF2CD99F7900C925F4 /* ProfileUpdatedNotify.swift in Sources */,
|
||||
82D6FAF02CD99F7900C925F4 /* ReportNotify.swift in Sources */,
|
||||
82D6FAF12CD99F7900C925F4 /* ScrollToTopNotify.swift in Sources */,
|
||||
@@ -5082,9 +5193,11 @@
|
||||
82D6FAF62CD99F7900C925F4 /* ZappingNotify.swift in Sources */,
|
||||
82D6FAF72CD99F7900C925F4 /* MuteNotify.swift in Sources */,
|
||||
82D6FAF82CD99F7900C925F4 /* RelaysChangedNotify.swift in Sources */,
|
||||
3A2BAC5B2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */,
|
||||
82D6FAF92CD99F7900C925F4 /* MuteThreadNotify.swift in Sources */,
|
||||
82D6FAFA2CD99F7900C925F4 /* UnmuteThreadNotify.swift in Sources */,
|
||||
82D6FAFB2CD99F7900C925F4 /* ReconnectRelaysNotify.swift in Sources */,
|
||||
3ACF94432DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */,
|
||||
82D6FAFC2CD99F7900C925F4 /* PurpleAccountUpdateNotify.swift in Sources */,
|
||||
82D6FAFD2CD99F7900C925F4 /* IdType.swift in Sources */,
|
||||
82D6FAFE2CD99F7900C925F4 /* Pubkey.swift in Sources */,
|
||||
@@ -5127,6 +5240,7 @@
|
||||
82D6FB212CD99F7900C925F4 /* SelectableText.swift in Sources */,
|
||||
82D6FB222CD99F7900C925F4 /* DamusColors.swift in Sources */,
|
||||
82D6FB232CD99F7900C925F4 /* ThiccDivider.swift in Sources */,
|
||||
D733F9E22D92C1D900317B11 /* SubscriptionManager.swift in Sources */,
|
||||
82D6FB242CD99F7900C925F4 /* IconLabel.swift in Sources */,
|
||||
82D6FB252CD99F7900C925F4 /* TruncatedText.swift in Sources */,
|
||||
82D6FB262CD99F7900C925F4 /* SupporterBadge.swift in Sources */,
|
||||
@@ -5141,6 +5255,7 @@
|
||||
82D6FB2F2CD99F7900C925F4 /* BlurHashDecode.swift in Sources */,
|
||||
82D6FB302CD99F7900C925F4 /* PostBox.swift in Sources */,
|
||||
82D6FB312CD99F7900C925F4 /* KFOptionSetter+.swift in Sources */,
|
||||
D73BDB162D71216500D69970 /* UserRelayListManager.swift in Sources */,
|
||||
82D6FB322CD99F7900C925F4 /* FillAndStroke.swift in Sources */,
|
||||
82D6FB332CD99F7900C925F4 /* Array.swift in Sources */,
|
||||
82D6FB342CD99F7900C925F4 /* VectorMath.swift in Sources */,
|
||||
@@ -5162,6 +5277,7 @@
|
||||
82D6FB432CD99F7900C925F4 /* KeychainStorage.swift in Sources */,
|
||||
82D6FB442CD99F7900C925F4 /* Bech32.swift in Sources */,
|
||||
82D6FB452CD99F7900C925F4 /* InputDismissKeyboard.swift in Sources */,
|
||||
D7FA46E62DBDAA7E002C9BB0 /* ImageCacheMigrations.swift in Sources */,
|
||||
82D6FB462CD99F7900C925F4 /* Constants.swift in Sources */,
|
||||
82D6FB472CD99F7900C925F4 /* LinkView.swift in Sources */,
|
||||
D7DB1FDF2D5A78CE00CF06DA /* NIP44.swift in Sources */,
|
||||
@@ -5198,6 +5314,7 @@
|
||||
82D6FB652CD99F7900C925F4 /* CollectionExtension.swift in Sources */,
|
||||
82D6FB662CD99F7900C925F4 /* ZapDataModel.swift in Sources */,
|
||||
82D6FB672CD99F7900C925F4 /* Zaps+.swift in Sources */,
|
||||
D7D09AB72DADCA5C00AB170D /* CoinosDeterministicAccountClient.swift in Sources */,
|
||||
82D6FB682CD99F7900C925F4 /* WalletConnect+.swift in Sources */,
|
||||
82D6FB692CD99F7900C925F4 /* DamusPurpleNotificationManagement.swift in Sources */,
|
||||
82D6FB6A2CD99F7900C925F4 /* DamusPurple.swift in Sources */,
|
||||
@@ -5205,7 +5322,6 @@
|
||||
82D6FB6C2CD99F7900C925F4 /* DamusPurpleURL.swift in Sources */,
|
||||
82D6FB6D2CD99F7900C925F4 /* DamusPurpleEnvironment.swift in Sources */,
|
||||
82D6FB6E2CD99F7900C925F4 /* PurpleStoreKitManager.swift in Sources */,
|
||||
82D6FB6F2CD99F7900C925F4 /* CameraService+Extensions.swift in Sources */,
|
||||
82D6FB702CD99F7900C925F4 /* ImageResizer.swift in Sources */,
|
||||
82D6FB712CD99F7900C925F4 /* PhotoCaptureProcessor.swift in Sources */,
|
||||
82D6FB722CD99F7900C925F4 /* VideoCaptureProcessor.swift in Sources */,
|
||||
@@ -5285,6 +5401,7 @@
|
||||
82D6FBBA2CD99F7900C925F4 /* NostrRequest.swift in Sources */,
|
||||
82D6FBBB2CD99F7900C925F4 /* Profiles.swift in Sources */,
|
||||
82D6FBBC2CD99F7900C925F4 /* NostrKind.swift in Sources */,
|
||||
D733F9E62D92C76100317B11 /* UnownedNdbNote.swift in Sources */,
|
||||
82D6FBBD2CD99F7900C925F4 /* NostrLink.swift in Sources */,
|
||||
82D6FBBE2CD99F7900C925F4 /* WebSocket.swift in Sources */,
|
||||
82D6FBBF2CD99F7900C925F4 /* ReferencedId.swift in Sources */,
|
||||
@@ -5318,6 +5435,7 @@
|
||||
82D6FBE02CD99F7900C925F4 /* ReactionsSettingsView.swift in Sources */,
|
||||
82D6FBE12CD99F7900C925F4 /* NotificationSettingsView.swift in Sources */,
|
||||
82D6FBE22CD99F7900C925F4 /* AppearanceSettingsView.swift in Sources */,
|
||||
D7DB930A2D69486700DA1EE5 /* NIP65.swift in Sources */,
|
||||
82D6FBE32CD99F7900C925F4 /* KeySettingsView.swift in Sources */,
|
||||
82D6FBE42CD99F7900C925F4 /* ZapSettingsView.swift in Sources */,
|
||||
82D6FBE52CD99F7900C925F4 /* TranslationSettingsView.swift in Sources */,
|
||||
@@ -5332,6 +5450,7 @@
|
||||
82D6FBED2CD99F7900C925F4 /* MediaView.swift in Sources */,
|
||||
82D6FBEE2CD99F7900C925F4 /* PurpleViewPrimitives.swift in Sources */,
|
||||
82D6FBEF2CD99F7900C925F4 /* MarketingContentView.swift in Sources */,
|
||||
3A2BAC602DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */,
|
||||
82D6FBF02CD99F7900C925F4 /* LogoView.swift in Sources */,
|
||||
82D6FBF12CD99F7900C925F4 /* IAPProductStateView.swift in Sources */,
|
||||
82D6FBF22CD99F7900C925F4 /* PurpleBackdrop.swift in Sources */,
|
||||
@@ -5350,6 +5469,7 @@
|
||||
82D6FBFF2CD99F7900C925F4 /* NotificationItemView.swift in Sources */,
|
||||
82D6FC002CD99F7900C925F4 /* ProfilePicturesView.swift in Sources */,
|
||||
82D6FC012CD99F7900C925F4 /* DamusAppNotificationView.swift in Sources */,
|
||||
3A92C1002DE16E9800CEEBAC /* FaviconCache.swift in Sources */,
|
||||
82D6FC022CD99F7900C925F4 /* InnerTimelineView.swift in Sources */,
|
||||
82D6FC032CD99F7900C925F4 /* PostingTimelineView.swift in Sources */,
|
||||
82D6FC042CD99F7900C925F4 /* ZapsView.swift in Sources */,
|
||||
@@ -5372,6 +5492,7 @@
|
||||
82D6FC132CD99F7900C925F4 /* FriendIcon.swift in Sources */,
|
||||
82D6FC142CD99F7900C925F4 /* CondensedProfilePicturesView.swift in Sources */,
|
||||
82D6FC152CD99F7900C925F4 /* ProfileEditButton.swift in Sources */,
|
||||
D73BDB102D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */,
|
||||
82D6FC162CD99F7900C925F4 /* RelayPaidDetail.swift in Sources */,
|
||||
82D6FC172CD99F7900C925F4 /* RelayAuthenticationDetail.swift in Sources */,
|
||||
82D6FC182CD99F7900C925F4 /* RelaySoftwareDetail.swift in Sources */,
|
||||
@@ -5425,6 +5546,7 @@
|
||||
82D6FC472CD99F7900C925F4 /* RepostAction.swift in Sources */,
|
||||
82D6FC482CD99F7900C925F4 /* ShareActionButton.swift in Sources */,
|
||||
82D6FC492CD99F7900C925F4 /* BigButton.swift in Sources */,
|
||||
D73BDB182D71311900D69970 /* UserRelayListErrors.swift in Sources */,
|
||||
82D6FC4A2CD99F7900C925F4 /* AddRelayView.swift in Sources */,
|
||||
82D6FC4B2CD99F7900C925F4 /* BlocksView.swift in Sources */,
|
||||
D74EA0912D2E3464002290DD /* URLHandler.swift in Sources */,
|
||||
@@ -5563,11 +5685,13 @@
|
||||
D73E5E682C6A97F4007EB227 /* VectorMath.swift in Sources */,
|
||||
D73E5E692C6A97F4007EB227 /* RelayBootstrap.swift in Sources */,
|
||||
D73E5E6A2C6A97F4007EB227 /* RelayModel.swift in Sources */,
|
||||
3A2BAC5A2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */,
|
||||
D73E5E6B2C6A97F4007EB227 /* AnyCodable.swift in Sources */,
|
||||
D73E5E6C2C6A97F4007EB227 /* AnyDecodable.swift in Sources */,
|
||||
D73E5E6D2C6A97F4007EB227 /* AnyEncodable.swift in Sources */,
|
||||
D73E5F782C6A9A5C007EB227 /* NdbNote+.swift in Sources */,
|
||||
D73E5E6E2C6A97F4007EB227 /* NIPURLBuilder.swift in Sources */,
|
||||
3ACF94472DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */,
|
||||
D73E5E6F2C6A97F4007EB227 /* TimeAgo.swift in Sources */,
|
||||
D73E5E702C6A97F4007EB227 /* Parser.swift in Sources */,
|
||||
D73E5E722C6A97F4007EB227 /* LinkView.swift in Sources */,
|
||||
@@ -5596,7 +5720,7 @@
|
||||
D73E5E882C6A97F4007EB227 /* StoreObserver.swift in Sources */,
|
||||
D73E5E892C6A97F4007EB227 /* DamusPurpleURL.swift in Sources */,
|
||||
D73E5E8A2C6A97F4007EB227 /* PurpleStoreKitManager.swift in Sources */,
|
||||
D73E5E8D2C6A97F4007EB227 /* CameraService+Extensions.swift in Sources */,
|
||||
D733F9E72D92C76100317B11 /* UnownedNdbNote.swift in Sources */,
|
||||
D73E5E8E2C6A97F4007EB227 /* ImageResizer.swift in Sources */,
|
||||
D78F080E2D7F78EF00FC6C75 /* Request.swift in Sources */,
|
||||
D73E5E8F2C6A97F4007EB227 /* PhotoCaptureProcessor.swift in Sources */,
|
||||
@@ -5622,6 +5746,7 @@
|
||||
D73E5E9F2C6A97F4007EB227 /* CreateAccountModel.swift in Sources */,
|
||||
D73E5EA12C6A97F4007EB227 /* SignalModel.swift in Sources */,
|
||||
5CB017272D42C5C400A9ED05 /* TransactionsView.swift in Sources */,
|
||||
D7FA46E52DBDAA7E002C9BB0 /* ImageCacheMigrations.swift in Sources */,
|
||||
D73E5EA22C6A97F4007EB227 /* FollowTarget.swift in Sources */,
|
||||
D73E5EA32C6A97F4007EB227 /* BookmarksManager.swift in Sources */,
|
||||
D73E5EA42C6A97F4007EB227 /* EventsModel.swift in Sources */,
|
||||
@@ -5653,6 +5778,7 @@
|
||||
D73E5EB92C6A97F4007EB227 /* RelayLog.swift in Sources */,
|
||||
D73E5EBA2C6A97F4007EB227 /* NostrFilter.swift in Sources */,
|
||||
D73E5EBB2C6A97F4007EB227 /* Nip98HTTPAuth.swift in Sources */,
|
||||
3A92C0FF2DE16E9800CEEBAC /* FaviconCache.swift in Sources */,
|
||||
D73E5EBC2C6A97F4007EB227 /* Relay.swift in Sources */,
|
||||
D73E5EBD2C6A97F4007EB227 /* NostrRequest.swift in Sources */,
|
||||
5CB017222D2D985E00A9ED05 /* CoinosButton.swift in Sources */,
|
||||
@@ -5672,6 +5798,7 @@
|
||||
D73E5ECC2C6A97F4007EB227 /* SuggestedUsersViewModel.swift in Sources */,
|
||||
D73E5ED22C6A97F4007EB227 /* WalletView.swift in Sources */,
|
||||
D73E5ED32C6A97F4007EB227 /* NWCScannerView.swift in Sources */,
|
||||
D74E64132DC95CC7004C7892 /* HumanReadableErrors.swift in Sources */,
|
||||
D73E5ED42C6A97F4007EB227 /* FriendsButton.swift in Sources */,
|
||||
D73E5ED52C6A97F4007EB227 /* GradientFollowButton.swift in Sources */,
|
||||
D73E5ED62C6A97F4007EB227 /* AlbyButton.swift in Sources */,
|
||||
@@ -5723,6 +5850,7 @@
|
||||
D73E5F012C6A97F4007EB227 /* ZapTypePicker.swift in Sources */,
|
||||
D73E5F022C6A97F4007EB227 /* ZapUserView.swift in Sources */,
|
||||
D73E5F032C6A97F4007EB227 /* ProfileZapLinkView.swift in Sources */,
|
||||
D7DB930C2D69486700DA1EE5 /* NIP65.swift in Sources */,
|
||||
D73E5F042C6A97F4007EB227 /* AboutView.swift in Sources */,
|
||||
D73E5F052C6A97F4007EB227 /* ProfileName.swift in Sources */,
|
||||
D73E5F062C6A97F4007EB227 /* ProfilePictureSelector.swift in Sources */,
|
||||
@@ -5749,6 +5877,7 @@
|
||||
D73E5F192C6A97F4007EB227 /* RelayToggle.swift in Sources */,
|
||||
D73E5F1A2C6A97F4007EB227 /* RelayStatusView.swift in Sources */,
|
||||
D73E5F1B2C6A97F4007EB227 /* RelayType.swift in Sources */,
|
||||
3A2BAC5F2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */,
|
||||
D73E5F1C2C6A97F4007EB227 /* SignalView.swift in Sources */,
|
||||
D73E5F1D2C6A97F4007EB227 /* RelayPicView.swift in Sources */,
|
||||
D73E5F1E2C6A97F4007EB227 /* UserSearch.swift in Sources */,
|
||||
@@ -5763,6 +5892,7 @@
|
||||
D73E5F272C6A97F4007EB227 /* TimeDot.swift in Sources */,
|
||||
D73E5F282C6A97F4007EB227 /* EventTop.swift in Sources */,
|
||||
D73E5F292C6A97F4007EB227 /* ReplyDescription.swift in Sources */,
|
||||
D73BDB0D2D6FF5F600D69970 /* NostrNetworkManager.swift in Sources */,
|
||||
D73E5F2A2C6A97F4007EB227 /* RelativeTime.swift in Sources */,
|
||||
D73E5F732C6A9885007EB227 /* TestData.swift in Sources */,
|
||||
D78F08192D7F7F7500FC6C75 /* NIP04.swift in Sources */,
|
||||
@@ -5801,6 +5931,7 @@
|
||||
D73E5F472C6A97F5007EB227 /* BookmarksView.swift in Sources */,
|
||||
D73E5F482C6A97F5007EB227 /* CarouselView.swift in Sources */,
|
||||
D73E5F492C6A97F5007EB227 /* ConfigView.swift in Sources */,
|
||||
D733F9E12D92C1D900317B11 /* SubscriptionManager.swift in Sources */,
|
||||
D73E5F4A2C6A97F5007EB227 /* CreateAccountView.swift in Sources */,
|
||||
D73E5F7A2C6A9C55007EB227 /* NotificationFormatter.swift in Sources */,
|
||||
D73E5F4B2C6A97F5007EB227 /* DirectMessagesView.swift in Sources */,
|
||||
@@ -5836,6 +5967,7 @@
|
||||
D73E5F6A2C6A97F5007EB227 /* ReportView.swift in Sources */,
|
||||
D73E5F6C2C6A97F5007EB227 /* RepostsView.swift in Sources */,
|
||||
D734B1462CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */,
|
||||
D73BDB142D71216500D69970 /* UserRelayListManager.swift in Sources */,
|
||||
D73E5F6D2C6A97F5007EB227 /* Launch.storyboard in Sources */,
|
||||
D73E5F6F2C6A97F5007EB227 /* RelayFilterView.swift in Sources */,
|
||||
D703D78A2C670C8A00A400EA /* LibreTranslateServer.swift in Sources */,
|
||||
@@ -5878,6 +6010,7 @@
|
||||
D73E5E1B2C6A9672007EB227 /* LikeCounter.swift in Sources */,
|
||||
D703D7A92C670E5A00A400EA /* refmap.c in Sources */,
|
||||
D703D77B2C670BF000A400EA /* TableVerifier.swift in Sources */,
|
||||
3ACF94442DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */,
|
||||
D703D76D2C670B4500A400EA /* ZapDataModel.swift in Sources */,
|
||||
D703D79D2C670E0700A400EA /* node_id.c in Sources */,
|
||||
D703D79B2C670E0000A400EA /* bech32_util.c in Sources */,
|
||||
@@ -5887,6 +6020,7 @@
|
||||
D703D7A52C670E3E00A400EA /* mdb.c in Sources */,
|
||||
D703D76B2C670B3100A400EA /* Referenced.swift in Sources */,
|
||||
D703D7952C670DE600A400EA /* hash_u5.c in Sources */,
|
||||
D73BDB192D71311900D69970 /* UserRelayListErrors.swift in Sources */,
|
||||
D703D7582C670A6000A400EA /* Id.swift in Sources */,
|
||||
5C05675A2C8FBDE70073F23A /* NDBSearchView.swift in Sources */,
|
||||
D703D76E2C670B4900A400EA /* NdbTagsIterator.swift in Sources */,
|
||||
@@ -5939,6 +6073,7 @@
|
||||
D73E5E182C6A963D007EB227 /* AttachMediaUtility.swift in Sources */,
|
||||
D73E5F852C6AA628007EB227 /* LoadScript.swift in Sources */,
|
||||
D703D74E2C6709DA00A400EA /* Pubkey.swift in Sources */,
|
||||
D7D09AB62DADCA5C00AB170D /* CoinosDeterministicAccountClient.swift in Sources */,
|
||||
D703D7802C670C2500A400EA /* NIP05.swift in Sources */,
|
||||
D703D7AA2C670E5D00A400EA /* verifier.c in Sources */,
|
||||
D73E5E1D2C6A9680007EB227 /* PreviewCache.swift in Sources */,
|
||||
@@ -5981,6 +6116,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4C8FA7242BED58A900798A6A /* ThreadReply.swift in Sources */,
|
||||
D733F9E52D92C76100317B11 /* UnownedNdbNote.swift in Sources */,
|
||||
D798D21F2B0858D600234419 /* MigratedTypes.swift in Sources */,
|
||||
D7CE1B472B0BE719002EDAD4 /* NativeObject.swift in Sources */,
|
||||
D71AD9002CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */,
|
||||
@@ -6337,7 +6473,7 @@
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.3;
|
||||
MARKETING_VERSION = 1.14;
|
||||
MARKETING_VERSION = 1.15;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
@@ -6402,7 +6538,7 @@
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.3;
|
||||
MARKETING_VERSION = 1.14;
|
||||
MARKETING_VERSION = 1.15;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -6854,7 +6990,15 @@
|
||||
repositoryURL = "https://github.com/tyiu/EmojiPicker.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 0.1.1;
|
||||
minimumVersion = 0.2.0;
|
||||
};
|
||||
};
|
||||
3ACF94362DA9A52F00971A4E /* XCRemoteSwiftPackageReference "FaviconFinder" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/will-lumley/FaviconFinder.git";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 5.1.4;
|
||||
};
|
||||
};
|
||||
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
|
||||
@@ -6862,7 +7006,7 @@
|
||||
repositoryURL = "https://github.com/onevcat/Kingfisher";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 7.0.0;
|
||||
minimumVersion = 8.3.1;
|
||||
};
|
||||
};
|
||||
4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */ = {
|
||||
@@ -6937,6 +7081,21 @@
|
||||
package = 3A0A30B92C21397A00F8C9BC /* XCRemoteSwiftPackageReference "EmojiPicker" */;
|
||||
productName = EmojiPicker;
|
||||
};
|
||||
3ACF94372DA9A52F00971A4E /* FaviconFinder */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 3ACF94362DA9A52F00971A4E /* XCRemoteSwiftPackageReference "FaviconFinder" */;
|
||||
productName = FaviconFinder;
|
||||
};
|
||||
3ACF943D2DA9B10800971A4E /* FaviconFinder */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 3ACF94362DA9A52F00971A4E /* XCRemoteSwiftPackageReference "FaviconFinder" */;
|
||||
productName = FaviconFinder;
|
||||
};
|
||||
3ACF943F2DA9B11200971A4E /* FaviconFinder */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 3ACF94362DA9A52F00971A4E /* XCRemoteSwiftPackageReference "FaviconFinder" */;
|
||||
productName = FaviconFinder;
|
||||
};
|
||||
4C06670328FC7EC500038D2A /* Kingfisher */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "085cf0f645323bf77edb52886489bf77b309a0a2d2b78a54beaf8520b540d596",
|
||||
"originHash" : "1fc7e0b44329ba72cd285eeb022b5b92582cd01586b920d243cb0485c2e69dcc",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "codescanner",
|
||||
@@ -22,8 +22,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/tyiu/EmojiKit",
|
||||
"state" : {
|
||||
"revision" : "05805f72d63a6d6a2d7dc7fe14abd37c1317b11a",
|
||||
"version" : "0.1.2"
|
||||
"revision" : "47a4b1402de26be0299dcb4d667c1faaf21a7874",
|
||||
"version" : "0.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -31,8 +31,17 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/tyiu/EmojiPicker.git",
|
||||
"state" : {
|
||||
"revision" : "0c28b4a1a6b8840cf2580bda59517f6d0a733dc8",
|
||||
"version" : "0.1.1"
|
||||
"revision" : "3f48903721eae223238ff0af17c22d6373d33813",
|
||||
"version" : "0.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "faviconfinder",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/will-lumley/FaviconFinder.git",
|
||||
"state" : {
|
||||
"revision" : "9279f4371f4877ca302ba3bf1015f3f58ae4a56c",
|
||||
"version" : "5.1.4"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -49,8 +58,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/onevcat/Kingfisher",
|
||||
"state" : {
|
||||
"revision" : "415b1d97fb38bda1e5a6b2dde63354720832110b",
|
||||
"version" : "7.6.1"
|
||||
"revision" : "4c6b067f96953ee19526e49e4189403a2be21fb3",
|
||||
"version" : "8.3.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -105,6 +114,15 @@
|
||||
"version" : "0.1.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftsoup",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/scinfu/SwiftSoup.git",
|
||||
"state" : {
|
||||
"revision" : "bba848db50462894e7fc0891d018dfecad4ef11e",
|
||||
"version" : "2.8.7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swiftycrop",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
|
Before Width: | Height: | Size: 216 B After Width: | Height: | Size: 216 B |
@@ -94,26 +94,30 @@ enum OpenWalletError: Error {
|
||||
}
|
||||
|
||||
func open_with_wallet(wallet: Wallet.Model, invoice: String) throws {
|
||||
let url = try getUrlToOpen(invoice: invoice, with: wallet)
|
||||
this_app.open(url)
|
||||
}
|
||||
|
||||
func getUrlToOpen(invoice: String, with wallet: Wallet.Model) throws(OpenWalletError) -> URL {
|
||||
if let url = URL(string: "\(wallet.link)\(invoice)"), this_app.canOpenURL(url) {
|
||||
this_app.open(url)
|
||||
return url
|
||||
} else {
|
||||
guard let store_link = wallet.appStoreLink else {
|
||||
throw OpenWalletError.no_wallet_to_open
|
||||
throw .no_wallet_to_open
|
||||
}
|
||||
|
||||
guard let url = URL(string: store_link) else {
|
||||
throw OpenWalletError.store_link_invalid
|
||||
throw .store_link_invalid
|
||||
}
|
||||
|
||||
guard this_app.canOpenURL(url) else {
|
||||
throw OpenWalletError.system_cannot_open_store_link
|
||||
throw .system_cannot_open_store_link
|
||||
}
|
||||
|
||||
this_app.open(url)
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let test_invoice = Invoice(description: .description("this is a description"), amount: .specific(10000), string: "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r", expiry: 604800, payment_hash: Data(), created_at: 1666139119)
|
||||
|
||||
struct InvoiceView_Previews: PreviewProvider {
|
||||
|
||||
@@ -5,27 +5,27 @@
|
||||
// Created by William Casarin on 2023-01-11.
|
||||
//
|
||||
|
||||
import FaviconFinder
|
||||
import Kingfisher
|
||||
import SwiftUI
|
||||
|
||||
struct NIP05Badge: View {
|
||||
let nip05: NIP05
|
||||
let pubkey: Pubkey
|
||||
let contacts: Contacts
|
||||
let damus_state: DamusState
|
||||
let show_domain: Bool
|
||||
let profiles: Profiles
|
||||
let nip05_domain_favicon: FaviconURL?
|
||||
|
||||
@Environment(\.openURL) var openURL
|
||||
|
||||
init(nip05: NIP05, pubkey: Pubkey, contacts: Contacts, show_domain: Bool, profiles: Profiles) {
|
||||
init(nip05: NIP05, pubkey: Pubkey, damus_state: DamusState, show_domain: Bool, nip05_domain_favicon: FaviconURL?) {
|
||||
self.nip05 = nip05
|
||||
self.pubkey = pubkey
|
||||
self.contacts = contacts
|
||||
self.damus_state = damus_state
|
||||
self.show_domain = show_domain
|
||||
self.profiles = profiles
|
||||
self.nip05_domain_favicon = nip05_domain_favicon
|
||||
}
|
||||
|
||||
var nip05_color: Bool {
|
||||
return use_nip05_color(pubkey: pubkey, contacts: contacts)
|
||||
return use_nip05_color(pubkey: pubkey, contacts: damus_state.contacts)
|
||||
}
|
||||
|
||||
var Seal: some View {
|
||||
@@ -44,8 +44,23 @@ struct NIP05Badge: View {
|
||||
}
|
||||
}
|
||||
|
||||
var domainBadge: some View {
|
||||
Group {
|
||||
if let nip05_domain_favicon {
|
||||
KFImage(nip05_domain_favicon.source)
|
||||
.imageContext(.favicon, disable_animation: true)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 18, height: 18)
|
||||
.clipped()
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var username_matches_nip05: Bool {
|
||||
guard let name = profiles.lookup(id: pubkey)?.map({ p in p?.name }).value
|
||||
guard let name = damus_state.profiles.lookup(id: pubkey)?.map({ p in p?.name }).value
|
||||
else {
|
||||
return false
|
||||
}
|
||||
@@ -65,14 +80,18 @@ struct NIP05Badge: View {
|
||||
HStack(spacing: 2) {
|
||||
Seal
|
||||
|
||||
if show_domain {
|
||||
Text(nip05_string)
|
||||
.nip05_colorized(gradient: nip05_color)
|
||||
.onTapGesture {
|
||||
if let nip5url = nip05.siteUrl {
|
||||
openURL(nip5url)
|
||||
}
|
||||
}
|
||||
Group {
|
||||
if show_domain {
|
||||
Text(nip05_string)
|
||||
.nip05_colorized(gradient: nip05_color)
|
||||
}
|
||||
|
||||
if nip05_domain_favicon != nil {
|
||||
domainBadge
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
damus_state.nav.push(route: Route.NIP05DomainEvents(events: NIP05DomainEventsModel(state: damus_state, domain: nip05.host), nip05_domain_favicon: nip05_domain_favicon))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,13 +117,9 @@ struct NIP05Badge_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let test_state = test_damus_state
|
||||
VStack {
|
||||
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
|
||||
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, damus_state: test_state, show_domain: true, nip05_domain_favicon: nil)
|
||||
|
||||
NIP05Badge(nip05: NIP05(username: "_", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
|
||||
|
||||
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
|
||||
|
||||
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: Contacts(our_pubkey: test_pubkey), show_domain: true, profiles: test_state.profiles)
|
||||
NIP05Badge(nip05: NIP05(username: "_", host: "jb55.com"), pubkey: test_state.pubkey, damus_state: test_state, show_domain: true, nip05_domain_favicon: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ struct NoteZapButton: View {
|
||||
print("cancel_zap: we already have a real zap, can't cancel")
|
||||
break
|
||||
case .pending(let pzap):
|
||||
guard let res = cancel_zap(zap: pzap, box: damus_state.postbox, zapcache: damus_state.zaps, evcache: damus_state.events) else {
|
||||
guard let res = cancel_zap(zap: pzap, box: damus_state.nostrNetwork.postbox, zapcache: damus_state.zaps, evcache: damus_state.events) else {
|
||||
|
||||
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
|
||||
return
|
||||
@@ -179,7 +179,7 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
|
||||
}
|
||||
|
||||
// Only take the first 10 because reasons
|
||||
let relays = Array(damus_state.pool.our_descriptors.prefix(10))
|
||||
let relays = Array(damus_state.nostrNetwork.pool.our_descriptors.prefix(10))
|
||||
let content = comment ?? ""
|
||||
|
||||
guard let mzapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type) else {
|
||||
@@ -232,7 +232,7 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
|
||||
flusher = .once({ pe in
|
||||
// send donation zap when the pending zap is flushed, this allows user to cancel and not send a donation
|
||||
Task { @MainActor in
|
||||
await WalletConnect.send_donation_zap(pool: damus_state.pool, postbox: damus_state.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat)
|
||||
await WalletConnect.send_donation_zap(pool: damus_state.nostrNetwork.pool, postbox: damus_state.nostrNetwork.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -240,7 +240,7 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
|
||||
// we don't have a delay on one-tap nozaps (since this will be from customize zap view)
|
||||
let delay = damus_state.settings.nozaps ? nil : 5.0
|
||||
|
||||
let nwc_req = WalletConnect.pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, delay: delay, on_flush: flusher)
|
||||
let nwc_req = WalletConnect.pay(url: nwc_state.url, pool: damus_state.nostrNetwork.pool, post: damus_state.nostrNetwork.postbox, invoice: inv, zap_request: zapreq, 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)")
|
||||
|
||||
@@ -94,12 +94,12 @@ struct SelectableText: View {
|
||||
case show_mute_word_view(highlighted_text: String)
|
||||
|
||||
func should_show_highlight_post_view() -> Bool {
|
||||
guard case .show_highlight_post_view(let highlighted_text) = self else { return false }
|
||||
guard case .show_highlight_post_view = self else { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
func should_show_mute_word_view() -> Bool {
|
||||
guard case .show_mute_word_view(let highlighted_text) = self else { return false }
|
||||
guard case .show_mute_word_view = self else { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -119,16 +119,23 @@ struct SelectableText: View {
|
||||
fileprivate class TextView: UITextView {
|
||||
var postHighlight: (String) -> Void
|
||||
var muteWord: (String) -> Void
|
||||
private let enableHighlighting: Bool
|
||||
|
||||
init(frame: CGRect, textContainer: NSTextContainer?, postHighlight: @escaping (String) -> Void, muteWord: @escaping (String) -> Void) {
|
||||
init(frame: CGRect, textContainer: NSTextContainer?, postHighlight: @escaping (String) -> Void, muteWord: @escaping (String) -> Void, enableHighlighting: Bool) {
|
||||
self.postHighlight = postHighlight
|
||||
self.muteWord = muteWord
|
||||
self.enableHighlighting = enableHighlighting
|
||||
|
||||
super.init(frame: frame, textContainer: textContainer)
|
||||
|
||||
if enableHighlighting {
|
||||
self.delegate = self
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
||||
if action == #selector(highlightText(_:)) {
|
||||
@@ -142,23 +149,44 @@ fileprivate class TextView: UITextView {
|
||||
return super.canPerformAction(action, withSender: sender)
|
||||
}
|
||||
|
||||
func getSelectedText() -> String? {
|
||||
private func getSelectedText() -> String? {
|
||||
guard let selectedRange = self.selectedTextRange else { return nil }
|
||||
return self.text(in: selectedRange)
|
||||
}
|
||||
|
||||
@objc public func highlightText(_ sender: Any?) {
|
||||
@objc private func highlightText(_ sender: Any?) {
|
||||
guard let selectedText = self.getSelectedText() else { return }
|
||||
self.postHighlight(selectedText)
|
||||
}
|
||||
|
||||
@objc public func muteText(_ sender: Any?) {
|
||||
@objc private func muteText(_ sender: Any?) {
|
||||
guard let selectedText = self.getSelectedText() else { return }
|
||||
self.muteWord(selectedText)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension TextView: UITextViewDelegate {
|
||||
func textView(_ textView: UITextView, editMenuForTextIn range: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu? {
|
||||
guard enableHighlighting,
|
||||
let selectedTextRange = self.selectedTextRange,
|
||||
let selectedText = self.text(in: selectedTextRange),
|
||||
!selectedText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let highlightAction = UIAction(title: NSLocalizedString("Highlight", comment: "Context menu action to highlight the selected text as context to draft a new note."), image: UIImage(systemName: "highlighter")) { [weak self] _ in
|
||||
self?.postHighlight(selectedText)
|
||||
}
|
||||
|
||||
let muteAction = UIAction(title: NSLocalizedString("Mute", comment: "Context menu action to mute the selected word."), image: UIImage(systemName: "speaker.slash")) { [weak self] _ in
|
||||
self?.muteWord(selectedText)
|
||||
}
|
||||
|
||||
return UIMenu(children: suggestedActions + [highlightAction, muteAction])
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate struct TextViewRepresentable: UIViewRepresentable {
|
||||
|
||||
let attributedString: AttributedString
|
||||
@@ -172,7 +200,7 @@ fileprivate struct TextViewRepresentable: UIViewRepresentable {
|
||||
@Binding var height: CGFloat
|
||||
|
||||
func makeUIView(context: UIViewRepresentableContext<Self>) -> TextView {
|
||||
let view = TextView(frame: .zero, textContainer: nil, postHighlight: postHighlight, muteWord: muteWord)
|
||||
let view = TextView(frame: .zero, textContainer: nil, postHighlight: postHighlight, muteWord: muteWord, enableHighlighting: enableHighlighting)
|
||||
view.isEditable = false
|
||||
view.dataDetectorTypes = .all
|
||||
view.isSelectable = true
|
||||
@@ -183,11 +211,6 @@ fileprivate struct TextViewRepresentable: UIViewRepresentable {
|
||||
view.textContainerInset.right = 1.0
|
||||
view.textAlignment = textAlignment
|
||||
|
||||
let menuController = UIMenuController.shared
|
||||
let highlightItem = UIMenuItem(title: "Highlight", action: #selector(view.highlightText(_:)))
|
||||
let muteItem = UIMenuItem(title: "Mute", action: #selector(view.muteText(_:)))
|
||||
menuController.menuItems = self.enableHighlighting ? [highlightItem, muteItem] : []
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
|
||||
@@ -213,6 +213,6 @@ struct UserStatusSheet: View {
|
||||
|
||||
struct UserStatusSheet_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
UserStatusSheet(damus_state: test_damus_state, postbox: test_damus_state.postbox, keypair: test_keypair, status: .init())
|
||||
UserStatusSheet(damus_state: test_damus_state, postbox: test_damus_state.nostrNetwork.postbox, keypair: test_keypair, status: .init())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ func translate_note(profiles: Profiles, keypair: Keypair, event: NostrEvent, set
|
||||
|
||||
// Render translated note
|
||||
let translated_blocks = parse_note_content(content: .content(translated_note, event.tags))
|
||||
let artifacts = render_blocks(blocks: translated_blocks, profiles: profiles)
|
||||
let artifacts = render_blocks(blocks: translated_blocks, profiles: profiles, can_hide_last_previewable_refs: true)
|
||||
|
||||
// and cache it
|
||||
return .translated(Translated(artifacts: artifacts, language: note_lang))
|
||||
|
||||
+34
-48
@@ -199,7 +199,7 @@ struct ContentView: View {
|
||||
func MaybeReportView(target: ReportTarget) -> some View {
|
||||
Group {
|
||||
if let keypair = damus_state.keypair.to_full() {
|
||||
ReportView(postbox: damus_state.postbox, target: target, keypair: keypair)
|
||||
ReportView(postbox: damus_state.nostrNetwork.postbox, target: target, keypair: keypair)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
@@ -317,7 +317,7 @@ struct ContentView: View {
|
||||
case .post(let action):
|
||||
PostView(action: action, damus_state: damus_state!)
|
||||
case .user_status:
|
||||
UserStatusSheet(damus_state: damus_state!, postbox: damus_state!.postbox, keypair: damus_state!.keypair, status: damus_state!.profiles.profile_data(damus_state!.pubkey).status)
|
||||
UserStatusSheet(damus_state: damus_state!, postbox: damus_state!.nostrNetwork.postbox, keypair: damus_state!.keypair, status: damus_state!.profiles.profile_data(damus_state!.pubkey).status)
|
||||
.presentationDragIndicator(.visible)
|
||||
case .event:
|
||||
EventDetailView()
|
||||
@@ -356,7 +356,7 @@ struct ContentView: View {
|
||||
self.hide_bar = !show
|
||||
}
|
||||
.onReceive(timer) { n in
|
||||
self.damus_state?.postbox.try_flushing_events()
|
||||
self.damus_state?.nostrNetwork.postbox.try_flushing_events()
|
||||
self.damus_state!.profiles.profile_data(self.damus_state!.pubkey).status.try_expire()
|
||||
}
|
||||
.onReceive(handle_notify(.report)) { target in
|
||||
@@ -367,10 +367,6 @@ struct ContentView: View {
|
||||
self.confirm_mute = true
|
||||
}
|
||||
.onReceive(handle_notify(.attached_wallet)) { nwc in
|
||||
// Ensure to add NWC relay to the pool and connect it.
|
||||
try? damus_state.pool.add_relay(.nwc(url: nwc.relay))
|
||||
damus_state.pool.connect(to: [nwc.relay])
|
||||
|
||||
// update the lightning address on our profile when we attach a
|
||||
// wallet with an associated
|
||||
guard let ds = self.damus_state,
|
||||
@@ -391,12 +387,12 @@ struct ContentView: View {
|
||||
let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: lud16, nip05: profile.nip05, damus_donation: profile.damus_donation, reactions: profile.reactions)
|
||||
|
||||
guard let ev = make_metadata_event(keypair: keypair, metadata: prof) else { return }
|
||||
ds.postbox.send(ev)
|
||||
ds.nostrNetwork.postbox.send(ev)
|
||||
}
|
||||
.onReceive(handle_notify(.broadcast)) { ev in
|
||||
guard let ds = self.damus_state else { return }
|
||||
|
||||
ds.postbox.send(ev)
|
||||
ds.nostrNetwork.postbox.send(ev)
|
||||
}
|
||||
.onReceive(handle_notify(.unfollow)) { target in
|
||||
guard let state = self.damus_state else { return }
|
||||
@@ -418,7 +414,7 @@ struct ContentView: View {
|
||||
return
|
||||
}
|
||||
|
||||
if !handle_post_notification(keypair: keypair, postbox: state.postbox, events: state.events, post: post) {
|
||||
if !handle_post_notification(keypair: keypair, postbox: state.nostrNetwork.postbox, events: state.events, post: post) {
|
||||
self.active_sheet = nil
|
||||
}
|
||||
}
|
||||
@@ -462,7 +458,7 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.disconnect_relays)) { () in
|
||||
damus_state.pool.disconnect()
|
||||
damus_state.nostrNetwork.pool.disconnect()
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { obj in
|
||||
print("txn: 📙 DAMUS ACTIVE NOTIFY")
|
||||
@@ -508,7 +504,7 @@ struct ContentView: View {
|
||||
break
|
||||
case .active:
|
||||
print("txn: 📙 DAMUS ACTIVE")
|
||||
damus_state.pool.ping()
|
||||
damus_state.nostrNetwork.pool.ping()
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
@@ -527,7 +523,7 @@ struct ContentView: View {
|
||||
let prof = Profile(name: profile.name, display_name: profile.display_name, about: profile.about, picture: profile.picture, banner: profile.banner, website: profile.website, lud06: profile.lud06, lud16: profile.lud16, nip05: profile.nip05, damus_donation: profile.damus_donation, reactions: !hide)
|
||||
|
||||
guard let profile_ev = make_metadata_event(keypair: keypair, metadata: prof) else { return }
|
||||
ds.postbox.send(profile_ev)
|
||||
ds.nostrNetwork.postbox.send(profile_ev)
|
||||
}
|
||||
.alert(NSLocalizedString("User muted", comment: "Alert message to indicate the user has been muted"), isPresented: $user_muted_confirm, actions: {
|
||||
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to muted a user was successful.")) {
|
||||
@@ -559,7 +555,7 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
ds.mutelist_manager.set_mutelist(mutelist)
|
||||
ds.postbox.send(mutelist)
|
||||
ds.nostrNetwork.postbox.send(mutelist)
|
||||
|
||||
confirm_overwrite_mutelist = false
|
||||
confirm_mute = false
|
||||
@@ -591,7 +587,7 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
ds.mutelist_manager.set_mutelist(ev)
|
||||
ds.postbox.send(ev)
|
||||
ds.nostrNetwork.postbox.send(ev)
|
||||
}
|
||||
}
|
||||
}, message: {
|
||||
@@ -632,7 +628,7 @@ struct ContentView: View {
|
||||
|
||||
func handleNotification(notification: LossyLocalNotification) {
|
||||
Log.info("ContentView is handling a notification", for: .push_notifications)
|
||||
guard let damus_state else {
|
||||
guard damus_state != nil else {
|
||||
// This should never happen because `listenAndHandleLocalNotifications` is called after damus state is initialized in `onAppear`
|
||||
assertionFailure("DamusState not loaded when ContentView (new handler) was handling a notification")
|
||||
Log.error("DamusState not loaded when ContentView (new handler) was handling a notification", for: .push_notifications)
|
||||
@@ -660,28 +656,14 @@ struct ContentView: View {
|
||||
|
||||
guard let ndb = mndb else { return }
|
||||
|
||||
let pool = RelayPool(ndb: ndb, keypair: keypair)
|
||||
let model_cache = RelayModelCache()
|
||||
let relay_filters = RelayFilters(our_pubkey: pubkey)
|
||||
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
|
||||
|
||||
let settings = UserSettingsStore.globally_load_for(pubkey: pubkey)
|
||||
|
||||
let new_relay_filters = load_relay_filters(pubkey) == nil
|
||||
for relay in bootstrap_relays {
|
||||
let descriptor = RelayDescriptor(url: relay, info: .rw)
|
||||
add_new_relay(model_cache: model_cache, relay_filters: relay_filters, pool: pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: settings.developer_mode)
|
||||
}
|
||||
|
||||
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
|
||||
|
||||
if let nwc_str = settings.nostr_wallet_connect,
|
||||
let nwc = WalletConnectURL(str: nwc_str) {
|
||||
try? pool.add_relay(.nwc(url: nwc.relay))
|
||||
}
|
||||
|
||||
self.damus_state = DamusState(pool: pool,
|
||||
keypair: keypair,
|
||||
self.damus_state = DamusState(keypair: keypair,
|
||||
likes: EventCounter(our_pubkey: pubkey),
|
||||
boosts: EventCounter(our_pubkey: pubkey),
|
||||
contacts: Contacts(our_pubkey: pubkey),
|
||||
@@ -697,8 +679,6 @@ struct ContentView: View {
|
||||
drafts: Drafts(),
|
||||
events: EventCache(ndb: ndb),
|
||||
bookmarks: BookmarksManager(pubkey: pubkey),
|
||||
postbox: PostBox(pool: pool),
|
||||
bootstrap_relays: bootstrap_relays,
|
||||
replies: ReplyCounter(our_pubkey: pubkey),
|
||||
wallet: WalletModel(settings: settings),
|
||||
nav: self.navigationCoordinator,
|
||||
@@ -706,7 +686,8 @@ struct ContentView: View {
|
||||
video: DamusVideoCoordinator(),
|
||||
ndb: ndb,
|
||||
quote_reposts: .init(our_pubkey: pubkey),
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true),
|
||||
favicon_cache: FaviconCache()
|
||||
)
|
||||
|
||||
home.damus_state = self.damus_state!
|
||||
@@ -722,7 +703,8 @@ struct ContentView: View {
|
||||
// Purple API is an experimental feature. If not enabled, do not connect `StoreObserver` with Purple API to avoid leaking receipts
|
||||
}
|
||||
|
||||
pool.connect()
|
||||
damus_state.nostrNetwork.pool.register_handler(sub_id: sub_id, handler: home.handle_event)
|
||||
damus_state.nostrNetwork.connect()
|
||||
}
|
||||
|
||||
func music_changed(_ state: MusicState) {
|
||||
@@ -745,7 +727,7 @@ struct ContentView: View {
|
||||
pdata.status.music = music
|
||||
|
||||
guard let ev = music.to_note(keypair: kp) else { return }
|
||||
damus_state.postbox.send(ev)
|
||||
damus_state.nostrNetwork.postbox.send(ev)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -761,6 +743,8 @@ struct ContentView: View {
|
||||
case route(Route)
|
||||
/// Open a sheet
|
||||
case sheet(Sheets)
|
||||
/// Open an external URL
|
||||
case external_url(URL)
|
||||
/// Do nothing.
|
||||
///
|
||||
/// ## Implementation notes
|
||||
@@ -777,6 +761,8 @@ struct ContentView: View {
|
||||
navigationCoordinator.push(route: route)
|
||||
case .sheet(let sheet):
|
||||
self.active_sheet = sheet
|
||||
case .external_url(let url):
|
||||
this_app.open(url)
|
||||
case .no_action:
|
||||
return
|
||||
}
|
||||
@@ -994,7 +980,7 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
|
||||
var has_event = false
|
||||
guard let filter else { return }
|
||||
|
||||
state.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in
|
||||
state.nostrNetwork.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in
|
||||
guard case .nostr_event(let ev) = res else {
|
||||
return
|
||||
}
|
||||
@@ -1008,7 +994,7 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
|
||||
break
|
||||
case .event(_, let ev):
|
||||
has_event = true
|
||||
state.pool.unsubscribe(sub_id: subid)
|
||||
state.nostrNetwork.pool.unsubscribe(sub_id: subid)
|
||||
|
||||
switch query {
|
||||
case .profile:
|
||||
@@ -1021,11 +1007,11 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
|
||||
case .eose:
|
||||
if !has_event {
|
||||
attempts += 1
|
||||
if attempts >= state.pool.our_descriptors.count {
|
||||
if attempts >= state.nostrNetwork.pool.our_descriptors.count {
|
||||
callback(nil) // If we could not find any events in any of the relays we are connected to, send back nil
|
||||
}
|
||||
}
|
||||
state.pool.unsubscribe(sub_id: subid, to: [relay_id]) // We are only finding an event once, so close subscription on eose
|
||||
state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id]) // We are only finding an event once, so close subscription on eose
|
||||
case .notice:
|
||||
break
|
||||
case .auth:
|
||||
@@ -1044,15 +1030,15 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
|
||||
/// - naddr: the `naddr` address
|
||||
/// - callback: A function to handle the found event
|
||||
func naddrLookup(damus_state: DamusState, naddr: NAddr, callback: @escaping (NostrEvent?) -> ()) {
|
||||
var nostrKinds: [NostrKind]? = NostrKind(rawValue: naddr.kind).map { [$0] }
|
||||
let nostrKinds: [NostrKind]? = NostrKind(rawValue: naddr.kind).map { [$0] }
|
||||
|
||||
let filter = NostrFilter(kinds: nostrKinds, authors: [naddr.author])
|
||||
|
||||
let subid = UUID().description
|
||||
|
||||
damus_state.pool.subscribe_to(sub_id: subid, filters: [filter], to: nil) { relay_id, res in
|
||||
damus_state.nostrNetwork.pool.subscribe_to(sub_id: subid, filters: [filter], to: nil) { relay_id, res in
|
||||
guard case .nostr_event(let ev) = res else {
|
||||
damus_state.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
||||
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1060,14 +1046,14 @@ func naddrLookup(damus_state: DamusState, naddr: NAddr, callback: @escaping (Nos
|
||||
for tag in ev.tags {
|
||||
if(tag.count >= 2 && tag[0].string() == "d"){
|
||||
if (tag[1].string() == naddr.identifier){
|
||||
damus_state.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
||||
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
||||
callback(ev)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
damus_state.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
||||
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1115,7 +1101,7 @@ func handle_unfollow(state: DamusState, unfollow: FollowRef) -> Bool {
|
||||
|
||||
let old_contacts = state.contacts.event
|
||||
|
||||
guard let ev = unfollow_reference(postbox: state.postbox, our_contacts: old_contacts, keypair: keypair, unfollow: unfollow)
|
||||
guard let ev = unfollow_reference(postbox: state.nostrNetwork.postbox, our_contacts: old_contacts, keypair: keypair, unfollow: unfollow)
|
||||
else {
|
||||
return false
|
||||
}
|
||||
@@ -1141,7 +1127,7 @@ func handle_follow(state: DamusState, follow: FollowRef) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
guard let ev = follow_reference(box: state.postbox, our_contacts: state.contacts.event, keypair: keypair, follow: follow)
|
||||
guard let ev = follow_reference(box: state.nostrNetwork.postbox, our_contacts: state.contacts.event, keypair: keypair, follow: follow)
|
||||
else {
|
||||
return false
|
||||
}
|
||||
@@ -1216,7 +1202,7 @@ extension LossyLocalNotification {
|
||||
case .nprofile(let nProfile):
|
||||
// TODO: Improve this by implementing a profile route that handles nprofiles with their relay hints.
|
||||
return .route(.ProfileByKey(pubkey: nProfile.author))
|
||||
case .nrelay(let string):
|
||||
case .nrelay:
|
||||
// We do not need to implement `nrelay` support, it has been deprecated.
|
||||
// See https://github.com/nostr-protocol/nips/blob/6e7a618e7f873bb91e743caacc3b09edab7796a0/BREAKING.md?plain=1#L21
|
||||
return .sheet(.error(ErrorView.UserPresentableError(
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
//
|
||||
// CameraService+Extensions.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Suhail Saqan on 8/5/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AVFoundation
|
||||
|
||||
extension AVCaptureVideoOrientation {
|
||||
init?(deviceOrientation: UIDeviceOrientation) {
|
||||
switch deviceOrientation {
|
||||
case .portrait: self = .portrait
|
||||
case .portraitUpsideDown: self = .portraitUpsideDown
|
||||
case .landscapeLeft: self = .landscapeRight
|
||||
case .landscapeRight: self = .landscapeLeft
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
init?(interfaceOrientation: UIInterfaceOrientation) {
|
||||
switch interfaceOrientation {
|
||||
case .portrait: self = .portrait
|
||||
case .portraitUpsideDown: self = .portraitUpsideDown
|
||||
case .landscapeLeft: self = .landscapeLeft
|
||||
case .landscapeRight: self = .landscapeRight
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,44 +63,10 @@ func follow_user_event(our_contacts: NostrEvent?, keypair: FullKeypair, follow:
|
||||
}
|
||||
|
||||
|
||||
func decode_json_relays(_ content: String) -> [RelayURL: RelayInfo]? {
|
||||
func decode_json_relays(_ content: String) -> [RelayURL: LegacyKind3RelayRWConfiguration]? {
|
||||
return decode_json(content)
|
||||
}
|
||||
|
||||
func remove_relay(ev: NostrEvent, current_relays: [RelayDescriptor], keypair: FullKeypair, relay: RelayURL) -> NostrEvent?{
|
||||
var relays = ensure_relay_info(relays: current_relays, content: ev.content)
|
||||
|
||||
relays.removeValue(forKey: relay)
|
||||
|
||||
guard let content = encode_json(relays) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings())
|
||||
}
|
||||
|
||||
func add_relay(ev: NostrEvent, keypair: FullKeypair, current_relays: [RelayDescriptor], relay: RelayURL, info: RelayInfo) -> NostrEvent? {
|
||||
var relays = ensure_relay_info(relays: current_relays, content: ev.content)
|
||||
|
||||
// If kind:3 content is empty, or if the relay doesn't exist in the list,
|
||||
// we want to create a kind:3 event with the new relay
|
||||
guard ev.content.isEmpty || relays.index(forKey: relay) == nil else {
|
||||
return nil
|
||||
}
|
||||
|
||||
relays[relay] = info
|
||||
|
||||
guard let content = encode_json(relays) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings())
|
||||
}
|
||||
|
||||
func ensure_relay_info(relays: [RelayDescriptor], content: String) -> [RelayURL: RelayInfo] {
|
||||
return decode_json_relays(content) ?? make_contact_relays(relays)
|
||||
}
|
||||
|
||||
func is_already_following(contacts: NostrEvent, follow: FollowRef) -> Bool {
|
||||
return contacts.references.contains { ref in
|
||||
switch (ref, follow) {
|
||||
@@ -128,22 +94,3 @@ func follow_with_existing_contacts(keypair: FullKeypair, our_contacts: NostrEven
|
||||
return NostrEvent(content: our_contacts.content, keypair: keypair.to_keypair(), kind: kind, tags: tags)
|
||||
}
|
||||
|
||||
func make_contact_relays(_ relays: [RelayDescriptor]) -> [RelayURL: RelayInfo] {
|
||||
return relays.reduce(into: [:]) { acc, relay in
|
||||
acc[relay.url] = relay.info
|
||||
}
|
||||
}
|
||||
|
||||
func make_relay_metadata(relays: [RelayDescriptor], keypair: FullKeypair) -> NostrEvent? {
|
||||
let tags = relays.compactMap { r -> [String]? in
|
||||
var tag = ["r", r.url.absoluteString]
|
||||
if (r.info.read ?? true) != (r.info.write ?? true) {
|
||||
tag += r.info.read == true ? ["read"] : ["write"]
|
||||
}
|
||||
if ((r.info.read ?? true) || (r.info.write ?? true)) && r.variant == .regular {
|
||||
return tag;
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return NostrEvent(content: "", keypair: keypair.to_keypair(), kind: 10_002, tags: tags)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,10 @@ class Contacts {
|
||||
return friends
|
||||
}
|
||||
|
||||
func get_friend_of_friends_list() -> Set<Pubkey> {
|
||||
return friend_of_friends
|
||||
}
|
||||
|
||||
func get_followed_hashtags() -> Set<String> {
|
||||
guard let ev = self.event else { return Set() }
|
||||
return Set(ev.referenced_hashtags.map({ $0.hashtag }))
|
||||
|
||||
@@ -40,6 +40,12 @@ func get_repost_of_muted_user_filter(damus_state: DamusState) -> ((_ ev: NostrEv
|
||||
}
|
||||
}
|
||||
|
||||
func timestamp_filter(ev: NostrEvent) -> Bool {
|
||||
// Allow notes that are created no more than 3 seconds in the future
|
||||
// to account for natural clock skew between sender and receiver.
|
||||
ev.age >= -3
|
||||
}
|
||||
|
||||
/// Generic filter with various tweakable settings
|
||||
struct ContentFilters {
|
||||
var filters: [(NostrEvent) -> Bool]
|
||||
@@ -66,6 +72,7 @@ extension ContentFilters {
|
||||
filters.append(nsfw_tag_filter)
|
||||
}
|
||||
filters.append(get_repost_of_muted_user_filter(damus_state: damus_state))
|
||||
filters.append(timestamp_filter)
|
||||
return filters
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ class CreateAccountModel: ObservableObject {
|
||||
return Keypair(pubkey: self.pubkey, privkey: self.privkey)
|
||||
}
|
||||
|
||||
var full_keypair: FullKeypair {
|
||||
return FullKeypair(pubkey: self.pubkey, privkey: self.privkey)
|
||||
}
|
||||
|
||||
init(display_name: String = "", name: String = "", about: String = "") {
|
||||
let keypair = generate_new_keypair()
|
||||
self.pubkey = keypair.pubkey
|
||||
|
||||
@@ -10,7 +10,6 @@ import LinkPresentation
|
||||
import EmojiPicker
|
||||
|
||||
class DamusState: HeadlessDamusState {
|
||||
let pool: RelayPool
|
||||
let keypair: Keypair
|
||||
let likes: EventCounter
|
||||
let boosts: EventCounter
|
||||
@@ -28,8 +27,6 @@ class DamusState: HeadlessDamusState {
|
||||
let drafts: Drafts
|
||||
let events: EventCache
|
||||
let bookmarks: BookmarksManager
|
||||
let postbox: PostBox
|
||||
let bootstrap_relays: [RelayURL]
|
||||
let replies: ReplyCounter
|
||||
let wallet: WalletModel
|
||||
let nav: NavigationCoordinator
|
||||
@@ -39,9 +36,10 @@ class DamusState: HeadlessDamusState {
|
||||
var purple: DamusPurple
|
||||
var push_notification_client: PushNotificationClient
|
||||
let emoji_provider: EmojiProvider
|
||||
let favicon_cache: FaviconCache
|
||||
private(set) var nostrNetwork: NostrNetworkManager
|
||||
|
||||
init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [RelayURL], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider) {
|
||||
self.pool = pool
|
||||
init(keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider, favicon_cache: FaviconCache) {
|
||||
self.keypair = keypair
|
||||
self.likes = likes
|
||||
self.boosts = boosts
|
||||
@@ -58,8 +56,6 @@ class DamusState: HeadlessDamusState {
|
||||
self.drafts = drafts
|
||||
self.events = events
|
||||
self.bookmarks = bookmarks
|
||||
self.postbox = postbox
|
||||
self.bootstrap_relays = bootstrap_relays
|
||||
self.replies = replies
|
||||
self.wallet = wallet
|
||||
self.nav = nav
|
||||
@@ -73,6 +69,10 @@ class DamusState: HeadlessDamusState {
|
||||
self.quote_reposts = quote_reposts
|
||||
self.push_notification_client = PushNotificationClient(keypair: keypair, settings: settings)
|
||||
self.emoji_provider = emoji_provider
|
||||
self.favicon_cache = FaviconCache()
|
||||
|
||||
let networkManagerDelegate = NostrNetworkManagerDelegate(settings: settings, contacts: contacts, ndb: ndb, keypair: keypair, relayModelCache: relay_model_cache, relayFilters: relay_filters)
|
||||
self.nostrNetwork = NostrNetworkManager(delegate: networkManagerDelegate)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
@@ -98,27 +98,13 @@ class DamusState: HeadlessDamusState {
|
||||
guard let ndb = mndb else { return nil }
|
||||
let pubkey = keypair.pubkey
|
||||
|
||||
let pool = RelayPool(ndb: ndb, keypair: keypair)
|
||||
let model_cache = RelayModelCache()
|
||||
let relay_filters = RelayFilters(our_pubkey: pubkey)
|
||||
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
|
||||
|
||||
let settings = UserSettingsStore.globally_load_for(pubkey: pubkey)
|
||||
|
||||
let new_relay_filters = load_relay_filters(pubkey) == nil
|
||||
for relay in bootstrap_relays {
|
||||
let descriptor = RelayDescriptor(url: relay, info: .rw)
|
||||
add_new_relay(model_cache: model_cache, relay_filters: relay_filters, pool: pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: settings.developer_mode)
|
||||
}
|
||||
|
||||
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
|
||||
|
||||
if let nwc_str = settings.nostr_wallet_connect,
|
||||
let nwc = WalletConnectURL(str: nwc_str) {
|
||||
try? pool.add_relay(.nwc(url: nwc.relay))
|
||||
}
|
||||
self.init(
|
||||
pool: pool,
|
||||
keypair: keypair,
|
||||
likes: EventCounter(our_pubkey: pubkey),
|
||||
boosts: EventCounter(our_pubkey: pubkey),
|
||||
@@ -135,8 +121,6 @@ class DamusState: HeadlessDamusState {
|
||||
drafts: Drafts(),
|
||||
events: EventCache(ndb: ndb),
|
||||
bookmarks: BookmarksManager(pubkey: pubkey),
|
||||
postbox: PostBox(pool: pool),
|
||||
bootstrap_relays: bootstrap_relays,
|
||||
replies: ReplyCounter(our_pubkey: pubkey),
|
||||
wallet: WalletModel(settings: settings),
|
||||
nav: navigationCoordinator,
|
||||
@@ -144,7 +128,8 @@ class DamusState: HeadlessDamusState {
|
||||
video: DamusVideoCoordinator(),
|
||||
ndb: ndb,
|
||||
quote_reposts: .init(our_pubkey: pubkey),
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true),
|
||||
favicon_cache: FaviconCache()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -179,7 +164,7 @@ class DamusState: HeadlessDamusState {
|
||||
try await self.push_notification_client.revoke_token()
|
||||
}
|
||||
wallet.disconnect()
|
||||
pool.close()
|
||||
nostrNetwork.pool.close()
|
||||
ndb.close()
|
||||
}
|
||||
|
||||
@@ -189,7 +174,6 @@ class DamusState: HeadlessDamusState {
|
||||
let kp = Keypair(pubkey: empty_pub, privkey: nil)
|
||||
|
||||
return DamusState.init(
|
||||
pool: RelayPool(ndb: .empty),
|
||||
keypair: Keypair(pubkey: empty_pub, privkey: empty_sec),
|
||||
likes: EventCounter(our_pubkey: empty_pub),
|
||||
boosts: EventCounter(our_pubkey: empty_pub),
|
||||
@@ -206,8 +190,6 @@ class DamusState: HeadlessDamusState {
|
||||
drafts: Drafts(),
|
||||
events: EventCache(ndb: .empty),
|
||||
bookmarks: BookmarksManager(pubkey: empty_pub),
|
||||
postbox: PostBox(pool: RelayPool(ndb: .empty)),
|
||||
bootstrap_relays: [],
|
||||
replies: ReplyCounter(our_pubkey: empty_pub),
|
||||
wallet: WalletModel(settings: UserSettingsStore()),
|
||||
nav: NavigationCoordinator(),
|
||||
@@ -215,7 +197,34 @@ class DamusState: HeadlessDamusState {
|
||||
video: DamusVideoCoordinator(),
|
||||
ndb: .empty,
|
||||
quote_reposts: .init(our_pubkey: empty_pub),
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true),
|
||||
favicon_cache: FaviconCache()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ class Drafts: ObservableObject {
|
||||
// TODO: Once it is time to implement draft syncing with relays, please consider the following:
|
||||
// - Privacy: Sending drafts to the network leaks metadata about app activity, and may break user expectations
|
||||
// - Down-sync conflict resolution: Consider how to solve conflicts for different draft versions holding the same ID (e.g. edited in Damus, then another client, then Damus again)
|
||||
damus_state.pool.send_raw_to_local_ndb(.typical(.event(draft_event)))
|
||||
damus_state.nostrNetwork.pool.send_raw_to_local_ndb(.typical(.event(draft_event)))
|
||||
}
|
||||
|
||||
damus_state.settings.draft_event_ids = draft_events.map({ $0.id.hex() })
|
||||
|
||||
@@ -68,13 +68,13 @@ class EventsModel: ObservableObject {
|
||||
}
|
||||
|
||||
func subscribe() {
|
||||
state.pool.subscribe(sub_id: sub_id,
|
||||
state.nostrNetwork.pool.subscribe(sub_id: sub_id,
|
||||
filters: [get_filter()],
|
||||
handler: handle_nostr_event)
|
||||
}
|
||||
|
||||
func unsubscribe() {
|
||||
state.pool.unsubscribe(sub_id: sub_id)
|
||||
state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
|
||||
}
|
||||
|
||||
private func handle_event(relay_id: RelayURL, ev: NostrEvent) {
|
||||
|
||||
@@ -37,11 +37,11 @@ class FollowersModel: ObservableObject {
|
||||
let filter = get_filter()
|
||||
let filters = [filter]
|
||||
//print_filters(relay_id: "following", filters: [filters])
|
||||
self.damus_state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
|
||||
self.damus_state.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
|
||||
}
|
||||
|
||||
func unsubscribe() {
|
||||
self.damus_state.pool.unsubscribe(sub_id: sub_id)
|
||||
self.damus_state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
|
||||
}
|
||||
|
||||
func handle_contact_event(_ ev: NostrEvent) {
|
||||
@@ -61,7 +61,7 @@ class FollowersModel: ObservableObject {
|
||||
|
||||
let filter = NostrFilter(kinds: [.metadata],
|
||||
authors: authors)
|
||||
damus_state.pool.subscribe_to(sub_id: profiles_id, filters: [filter], to: [relay_id], handler: handle_event)
|
||||
damus_state.nostrNetwork.pool.subscribe_to(sub_id: profiles_id, filters: [filter], to: [relay_id], handler: handle_event)
|
||||
}
|
||||
|
||||
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
||||
@@ -86,7 +86,7 @@ class FollowersModel: ObservableObject {
|
||||
guard let txn = NdbTxn(ndb: self.damus_state.ndb) else { return }
|
||||
load_profiles(relay_id: relay_id, txn: txn)
|
||||
} else if sub_id == self.profiles_id {
|
||||
damus_state.pool.unsubscribe(sub_id: profiles_id, to: [relay_id])
|
||||
damus_state.nostrNetwork.pool.unsubscribe(sub_id: profiles_id, to: [relay_id])
|
||||
}
|
||||
|
||||
case .ok:
|
||||
|
||||
@@ -42,7 +42,7 @@ class FollowingModel {
|
||||
}
|
||||
let filters = [filter]
|
||||
//print_filters(relay_id: "following", filters: [filters])
|
||||
self.damus_state.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
|
||||
self.damus_state.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: filters, handler: handle_event)
|
||||
}
|
||||
|
||||
func unsubscribe() {
|
||||
@@ -50,7 +50,7 @@ class FollowingModel {
|
||||
return
|
||||
}
|
||||
print("unsubscribing from following \(sub_id)")
|
||||
self.damus_state.pool.unsubscribe(sub_id: sub_id)
|
||||
self.damus_state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
|
||||
}
|
||||
|
||||
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
||||
|
||||
@@ -95,13 +95,13 @@ class HomeModel: ContactsDelegate {
|
||||
}
|
||||
|
||||
var pool: RelayPool {
|
||||
return damus_state.pool
|
||||
self.damus_state.nostrNetwork.pool
|
||||
}
|
||||
|
||||
var dms: DirectMessagesModel {
|
||||
return damus_state.dms
|
||||
}
|
||||
|
||||
|
||||
func has_sub_id_event(sub_id: String, ev_id: NoteId) -> Bool {
|
||||
if !has_event.keys.contains(sub_id) {
|
||||
has_event[sub_id] = Set()
|
||||
@@ -225,6 +225,8 @@ class HomeModel: ContactsDelegate {
|
||||
// TODO: Implement draft syncing with relays. We intentionally do not support that as of writing. See `DraftsModel.swift` for other details
|
||||
// try? damus_state.drafts.load(wrapped_draft_note: ev, with: damus_state)
|
||||
break
|
||||
case .relay_list:
|
||||
break // This will be handled by `UserRelayListManager`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,22 +261,41 @@ class HomeModel: ContactsDelegate {
|
||||
Task { @MainActor in
|
||||
// TODO: Adapt KeychainStorage to StringCodable and instead of parsing to WalletConnectURL every time
|
||||
guard let nwc_str = damus_state.settings.nostr_wallet_connect,
|
||||
let nwc = WalletConnectURL(str: nwc_str),
|
||||
let resp = await WalletConnect.FullWalletResponse(from: ev, nwc: nwc) else {
|
||||
let nwc = WalletConnectURL(str: nwc_str) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard nwc.relay == relay else { return } // Don't process NWC responses coming from relays other than our designated one
|
||||
guard ev.referenced_pubkeys.first == nwc.keypair.pubkey else {
|
||||
return // This message is not for us. Ignore it.
|
||||
}
|
||||
|
||||
var resp: WalletConnect.FullWalletResponse? = nil
|
||||
do {
|
||||
resp = try await WalletConnect.FullWalletResponse(from: ev, nwc: nwc)
|
||||
} catch {
|
||||
Log.error("HomeModel: Error on NWC wallet response handling: %s", for: .nwc, error.localizedDescription)
|
||||
if let initError = error as? WalletConnect.FullWalletResponse.InitializationError,
|
||||
let humanReadableError = initError.humanReadableError {
|
||||
present_sheet(.error(humanReadableError))
|
||||
}
|
||||
}
|
||||
guard let resp else { return }
|
||||
|
||||
// since command results are not returned for ephemeral events,
|
||||
// remove the request from the postbox which is likely failing over and over
|
||||
if damus_state.postbox.remove_relayer(relay_id: nwc.relay, event_id: resp.req_id) {
|
||||
print("nwc: got response, removed \(resp.req_id) from the postbox [\(relay)]")
|
||||
if damus_state.nostrNetwork.postbox.remove_relayer(relay_id: nwc.relay, event_id: resp.req_id) {
|
||||
Log.debug("HomeModel: got NWC response, removed %s from the postbox [%s]", for: .nwc, resp.req_id.hex(), relay.absoluteString)
|
||||
} else {
|
||||
print("nwc: \(resp.req_id) not found in the postbox, nothing to remove [\(relay)]")
|
||||
Log.debug("HomeModel: got NWC response, %s not found in the postbox, nothing to remove [%s]", for: .nwc, resp.req_id.hex(), relay.absoluteString)
|
||||
}
|
||||
|
||||
guard resp.response.error == nil else {
|
||||
print("nwc error: \(resp.response)")
|
||||
Log.error("HomeModel: NWC wallet raised an error: %s", for: .nwc, String(describing: resp.response))
|
||||
WalletConnect.handle_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
|
||||
if let humanReadableError = resp.response.error?.humanReadableError {
|
||||
present_sheet(.error(humanReadableError))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -478,7 +499,7 @@ class HomeModel: ContactsDelegate {
|
||||
break
|
||||
}
|
||||
|
||||
update_signal_from_pool(signal: self.signal, pool: damus_state.pool)
|
||||
update_signal_from_pool(signal: self.signal, pool: damus_state.nostrNetwork.pool)
|
||||
case .nostr_event(let ev):
|
||||
switch ev {
|
||||
case .event(let sub_id, let ev):
|
||||
@@ -948,7 +969,6 @@ func load_our_stuff(state: DamusState, ev: NostrEvent) {
|
||||
state.contacts.event = ev
|
||||
|
||||
load_our_contacts(state: state, m_old_ev: m_old_ev, ev: ev)
|
||||
load_our_relays(state: state, m_old_ev: m_old_ev, ev: ev)
|
||||
}
|
||||
|
||||
func process_contact_event(state: DamusState, ev: NostrEvent) {
|
||||
@@ -956,78 +976,6 @@ func process_contact_event(state: DamusState, ev: NostrEvent) {
|
||||
add_contact_if_friend(contacts: state.contacts, ev: ev)
|
||||
}
|
||||
|
||||
func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
||||
let bootstrap_dict: [RelayURL: RelayInfo] = [:]
|
||||
let old_decoded = m_old_ev.flatMap { decode_json_relays($0.content) } ?? state.bootstrap_relays.reduce(into: bootstrap_dict) { (d, r) in
|
||||
d[r] = .rw
|
||||
}
|
||||
|
||||
guard let decoded: [RelayURL: RelayInfo] = decode_json_relays(ev.content) else {
|
||||
return
|
||||
}
|
||||
|
||||
var changed = false
|
||||
|
||||
var new = Set<RelayURL>()
|
||||
for key in decoded.keys {
|
||||
new.insert(key)
|
||||
}
|
||||
|
||||
var old = Set<RelayURL>()
|
||||
for key in old_decoded.keys {
|
||||
old.insert(key)
|
||||
}
|
||||
|
||||
let diff = old.symmetricDifference(new)
|
||||
|
||||
let new_relay_filters = load_relay_filters(state.pubkey) == nil
|
||||
for d in diff {
|
||||
changed = true
|
||||
if new.contains(d) {
|
||||
let descriptor = RelayDescriptor(url: d, info: decoded[d] ?? .rw)
|
||||
add_new_relay(model_cache: state.relay_model_cache, relay_filters: state.relay_filters, pool: state.pool, descriptor: descriptor, new_relay_filters: new_relay_filters, logging_enabled: state.settings.developer_mode)
|
||||
} else {
|
||||
state.pool.remove_relay(d)
|
||||
}
|
||||
}
|
||||
|
||||
if changed {
|
||||
save_bootstrap_relays(pubkey: state.pubkey, relays: Array(new))
|
||||
state.pool.connect()
|
||||
notify(.relays_changed)
|
||||
}
|
||||
}
|
||||
|
||||
func add_new_relay(model_cache: RelayModelCache, relay_filters: RelayFilters, pool: RelayPool, descriptor: RelayDescriptor, new_relay_filters: Bool, logging_enabled: Bool) {
|
||||
try? pool.add_relay(descriptor)
|
||||
let url = descriptor.url
|
||||
|
||||
let relay_id = url
|
||||
guard model_cache.model(withURL: url) == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
Task.detached(priority: .background) {
|
||||
guard let meta = try? await fetch_relay_metadata(relay_id: relay_id) else {
|
||||
return
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
let model = RelayModel(url, metadata: meta)
|
||||
model_cache.insert(model: model)
|
||||
|
||||
if logging_enabled {
|
||||
pool.setLog(model.log, for: relay_id)
|
||||
}
|
||||
|
||||
// if this is the first time adding filters, we should filter non-paid relays
|
||||
if new_relay_filters && !meta.is_paid {
|
||||
relay_filters.insert(timeline: .search, relay_id: relay_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetch_relay_metadata(relay_id: RelayURL) async throws -> RelayMetadata? {
|
||||
var urlString = relay_id.absoluteString.replacingOccurrences(of: "wss://", with: "https://")
|
||||
urlString = urlString.replacingOccurrences(of: "ws://", with: "http://")
|
||||
@@ -1250,3 +1198,4 @@ func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-10
@@ -47,16 +47,16 @@ enum MuteItem: Hashable, Equatable {
|
||||
// rhs is the item we want to check against (ie. the item in the mute list)
|
||||
|
||||
switch (lhs, rhs) {
|
||||
case (.user(let lhs_pubkey, _), .user(let rhs_pubkey, let rhs_expiration_date)):
|
||||
return lhs_pubkey == rhs_pubkey && !rhs.is_expired()
|
||||
case (.hashtag(let lhs_hashtag, _), .hashtag(let rhs_hashtag, let rhs_expiration_date)):
|
||||
return lhs_hashtag == rhs_hashtag && !rhs.is_expired()
|
||||
case (.word(let lhs_word, _), .word(let rhs_word, let rhs_expiration_date)):
|
||||
return lhs_word == rhs_word && !rhs.is_expired()
|
||||
case (.thread(let lhs_thread, _), .thread(let rhs_thread, let rhs_expiration_date)):
|
||||
return lhs_thread == rhs_thread && !rhs.is_expired()
|
||||
default:
|
||||
return false
|
||||
case (.user(let lhs_pubkey, _), .user(let rhs_pubkey, _)):
|
||||
return lhs_pubkey == rhs_pubkey && !rhs.is_expired()
|
||||
case (.hashtag(let lhs_hashtag, _), .hashtag(let rhs_hashtag, _)):
|
||||
return lhs_hashtag == rhs_hashtag && !rhs.is_expired()
|
||||
case (.word(let lhs_word, _), .word(let rhs_word, _)):
|
||||
return lhs_word == rhs_word && !rhs.is_expired()
|
||||
case (.thread(let lhs_thread, _), .thread(let rhs_thread, _)):
|
||||
return lhs_thread == rhs_thread && !rhs.is_expired()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ func migrate_old_muted_threads_to_new_mutelist(keypair: Keypair, damus_state: Da
|
||||
let previous_mute_list_event = damus_state.mutelist_manager.event
|
||||
guard let new_mutelist_event = create_or_update_mutelist(keypair: fullKeypair, mprev: previous_mute_list_event, to_add: Set(mutedThreads.map { MuteItem.thread($0, nil) })) else { return }
|
||||
damus_state.mutelist_manager.set_mutelist(new_mutelist_event)
|
||||
damus_state.postbox.send(new_mutelist_event)
|
||||
damus_state.nostrNetwork.postbox.send(new_mutelist_event)
|
||||
// Set existing muted threads to an empty array
|
||||
UserDefaults.standard.set([], forKey: getMutedThreadsKey(pubkey: keypair.pubkey))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// NIP05DomainEventsModel.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 4/11/25.
|
||||
//
|
||||
|
||||
import FaviconFinder
|
||||
import Foundation
|
||||
|
||||
class NIP05DomainEventsModel: ObservableObject {
|
||||
let state: DamusState
|
||||
var events: EventHolder
|
||||
@Published var loading: Bool = false
|
||||
|
||||
let domain: String
|
||||
var filter: NostrFilter
|
||||
let sub_id = UUID().description
|
||||
let profiles_subid = UUID().description
|
||||
let limit: UInt32 = 500
|
||||
|
||||
init(state: DamusState, domain: String) {
|
||||
self.state = state
|
||||
self.domain = domain
|
||||
self.events = EventHolder(on_queue: { ev in
|
||||
preload_events(state: state, events: [ev])
|
||||
})
|
||||
self.filter = NostrFilter()
|
||||
}
|
||||
|
||||
@MainActor func subscribe() {
|
||||
filter.limit = self.limit
|
||||
filter.kinds = [.text, .longform, .highlight]
|
||||
|
||||
var authors = Set<Pubkey>()
|
||||
for pubkey in state.contacts.get_friend_of_friends_list() {
|
||||
let profile_txn = state.profiles.lookup(id: pubkey)
|
||||
|
||||
guard let profile = profile_txn?.unsafeUnownedValue,
|
||||
let nip05_str = profile.nip05,
|
||||
let nip05 = NIP05.parse(nip05_str),
|
||||
nip05.host.caseInsensitiveCompare(domain) == .orderedSame else {
|
||||
continue
|
||||
}
|
||||
|
||||
authors.insert(pubkey)
|
||||
}
|
||||
if authors.isEmpty {
|
||||
return
|
||||
}
|
||||
filter.authors = Array(authors)
|
||||
|
||||
print("subscribing to notes from friends of friends with '\(domain)' NIP-05 domain with sub_id \(sub_id)")
|
||||
state.nostrNetwork.pool.register_handler(sub_id: sub_id, handler: handle_event)
|
||||
loading = true
|
||||
state.nostrNetwork.pool.send(.subscribe(.init(filters: [filter], sub_id: sub_id)))
|
||||
}
|
||||
|
||||
func unsubscribe() {
|
||||
state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
|
||||
loading = false
|
||||
print("unsubscribing from notes from friends of friends with '\(domain)' NIP-05 domain with sub_id \(sub_id)")
|
||||
}
|
||||
|
||||
func add_event(_ ev: NostrEvent) {
|
||||
if !event_matches_filter(ev, filter: filter) {
|
||||
return
|
||||
}
|
||||
|
||||
guard should_show_event(state: state, ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
if self.events.insert(ev) {
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
||||
let (sub_id, done) = handle_subid_event(pool: state.nostrNetwork.pool, relay_id: relay_id, ev: ev) { sub_id, ev in
|
||||
if sub_id == self.sub_id && ev.is_textlike && ev.should_show_event {
|
||||
self.add_event(ev)
|
||||
}
|
||||
}
|
||||
|
||||
guard done else {
|
||||
return
|
||||
}
|
||||
|
||||
self.loading = false
|
||||
|
||||
if sub_id == self.sub_id {
|
||||
guard let txn = NdbTxn(ndb: state.ndb) else { return }
|
||||
load_profiles(context: "search", profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(self.events.all_events), damus_state: state, txn: txn)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
//
|
||||
// NostrNetworkManager.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-02-26.
|
||||
//
|
||||
import Foundation
|
||||
|
||||
/// Manages interactions with the Nostr Network.
|
||||
///
|
||||
/// This delineates a layer that is responsible for doing mid-level management of interactions with the Nostr network, controlling lower-level classes that perform more network/DB specific code, and providing an easier to use and more semantic interfaces for the rest of the app.
|
||||
///
|
||||
/// This is responsible for:
|
||||
/// - Managing the user's relay list
|
||||
/// - Establishing a `RelayPool` and maintaining it in sync with the user's relay list as it changes
|
||||
/// - Abstracting away complexities of interacting with the nostr network, providing an easier-to-use interface to fetch and send content related to the Nostr network
|
||||
///
|
||||
/// This is **NOT** responsible for:
|
||||
/// - Doing actual storage of relay list (delegated via the delegate
|
||||
/// - Handling low-level relay logic (this will be delegated to lower level classes used in RelayPool/RelayConnection)
|
||||
class NostrNetworkManager {
|
||||
/// The relay pool that we manage
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - This will be marked `private` in the future to prevent other code from accessing the relay pool directly. Code outside this layer should use a higher level interface
|
||||
let pool: RelayPool // TODO: Make this private and make higher level interface for classes outside the NostrNetworkManager
|
||||
/// A delegate that allows us to interact with the rest of app without introducing hard or circular dependencies
|
||||
private var delegate: Delegate
|
||||
/// Manages the user's relay list, controls RelayPool's connected relays
|
||||
let userRelayList: UserRelayListManager
|
||||
/// Handles sending out notes to the network
|
||||
let postbox: PostBox
|
||||
/// Handles subscriptions and functions to read or consume data from the Nostr network
|
||||
let reader: SubscriptionManager
|
||||
|
||||
init(delegate: Delegate) {
|
||||
self.delegate = delegate
|
||||
let pool = RelayPool(ndb: delegate.ndb, keypair: delegate.keypair)
|
||||
self.pool = pool
|
||||
let reader = SubscriptionManager(pool: pool, ndb: delegate.ndb)
|
||||
let userRelayList = UserRelayListManager(delegate: delegate, pool: pool, reader: reader)
|
||||
self.reader = reader
|
||||
self.userRelayList = userRelayList
|
||||
self.postbox = PostBox(pool: pool)
|
||||
}
|
||||
|
||||
// MARK: - Control functions
|
||||
|
||||
/// Connects the app to the Nostr network
|
||||
func connect() {
|
||||
self.userRelayList.connect()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Helper types
|
||||
|
||||
extension NostrNetworkManager {
|
||||
/// The delegate that provides information and structure for the `NostrNetworkManager` to function.
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// This is needed to prevent a circular reference between `DamusState` and `NostrNetworkManager`, and reduce coupling.
|
||||
protocol Delegate: Sendable {
|
||||
/// NostrDB instance, used with `RelayPool` to send events for ingestion.
|
||||
var ndb: Ndb { get }
|
||||
|
||||
/// The keypair to use for relay authentication and updating relay lists
|
||||
var keypair: Keypair { get }
|
||||
|
||||
/// The latest relay list event id hex
|
||||
var latestRelayListEventIdHex: String? { get set } // TODO: Update this once we have full NostrDB query support
|
||||
|
||||
/// The latest contact list `NostrEvent`
|
||||
///
|
||||
/// Note: Read-only access, because `NostrNetworkManager` does not manage contact lists.
|
||||
var latestContactListEvent: NostrEvent? { get }
|
||||
|
||||
/// Default bootstrap relays to start with when a user relay list is not present
|
||||
var bootstrapRelays: [RelayURL] { get }
|
||||
|
||||
/// Whether the app is in developer mode
|
||||
var developerMode: Bool { get }
|
||||
|
||||
/// The cache of relay model information
|
||||
var relayModelCache: RelayModelCache { get }
|
||||
|
||||
/// Relay filters
|
||||
var relayFilters: RelayFilters { get }
|
||||
|
||||
/// The user's connected NWC wallet
|
||||
var nwcWallet: WalletConnectURL? { get }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// SubscriptionManager.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-03-25.
|
||||
//
|
||||
|
||||
extension NostrNetworkManager {
|
||||
/// Reads or fetches information from RelayPool and NostrDB, and provides an easier and unified higher-level interface.
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - This class will be a key part of the local relay model migration. Most higher-level code should fetch content from this class, which will properly setup the correct relay pool subscriptions, and provide a stream from NostrDB for higher performance and reliability.
|
||||
class SubscriptionManager {
|
||||
private let pool: RelayPool
|
||||
private var ndb: Ndb
|
||||
|
||||
init(pool: RelayPool, ndb: Ndb) {
|
||||
self.pool = pool
|
||||
self.ndb = ndb
|
||||
}
|
||||
|
||||
// MARK: - Reading data from Nostr
|
||||
|
||||
/// Subscribes to data from the user's relays
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - When we migrate to the local relay model, we should modify this function to stream directly from NostrDB
|
||||
///
|
||||
/// - Parameter filters: The nostr filters to specify what kind of data to subscribe to
|
||||
/// - Returns: An async stream of nostr data
|
||||
func subscribe(filters: [NostrFilter]) -> AsyncStream<StreamItem> {
|
||||
return AsyncStream<StreamItem> { continuation in
|
||||
let streamTask = Task {
|
||||
for await item in self.pool.subscribe(filters: filters) {
|
||||
switch item {
|
||||
case .eose: continuation.yield(.eose)
|
||||
case .event(let nostrEvent):
|
||||
// At this point of the pipeline, if the note is valid it should have been processed and verified by NostrDB,
|
||||
// in which case we should pull the note from NostrDB to ensure validity.
|
||||
// However, NdbNotes are unowned, so we return a function where our callers can temporarily borrow the NostrDB note
|
||||
let noteId = nostrEvent.id
|
||||
let lender: NdbNoteLender = { lend in
|
||||
guard let ndbNoteTxn = self.ndb.lookup_note(noteId) else {
|
||||
throw NdbNoteLenderError.errorLoadingNote
|
||||
}
|
||||
guard let unownedNote = UnownedNdbNote(ndbNoteTxn) else {
|
||||
throw NdbNoteLenderError.errorLoadingNote
|
||||
}
|
||||
lend(unownedNote)
|
||||
}
|
||||
continuation.yield(.event(borrow: lender))
|
||||
}
|
||||
}
|
||||
}
|
||||
continuation.onTermination = { @Sendable _ in
|
||||
streamTask.cancel() // Close the RelayPool stream when caller stops streaming
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum StreamItem {
|
||||
/// An event which can be borrowed from NostrDB
|
||||
case event(borrow: NdbNoteLender)
|
||||
/// The end of stored events
|
||||
case eose
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// UserRelayListErrors.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-02-27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension NostrNetworkManager.UserRelayListManager {
|
||||
/// Models an error that may occur when performing operations that change the user's relay list.
|
||||
///
|
||||
/// Callers to functions that throw this error SHOULD handle them in order to provide a better user experience.
|
||||
enum UpdateError: Error {
|
||||
/// The user is not authorized to change relay list, usually because the private key is missing.
|
||||
case notAuthorizedToChangeRelayList
|
||||
/// An error occurred when forming the relay list Nostr event.
|
||||
case cannotFormRelayListEvent
|
||||
/// Cannot add item to the relay list because the relay is already present in the list.
|
||||
case relayAlreadyExists
|
||||
/// Cannot update the relay list because we do not have the user's previous relay list.
|
||||
///
|
||||
/// Implementers must be careful not to overwrite the user's existing relay list if it exists somewhere else.
|
||||
case noInitialRelayList
|
||||
/// Cannot remove or update a specific relay because it is not on the relay list
|
||||
case noSuchRelay
|
||||
|
||||
/// Convert `RelayPool.RelayError` into `UserRelayListUpdateError`
|
||||
static func from(_ relayPoolError: RelayPool.RelayError) -> Self {
|
||||
switch relayPoolError {
|
||||
case .RelayAlreadyExists: return .relayAlreadyExists
|
||||
}
|
||||
}
|
||||
|
||||
var humanReadableError: ErrorView.UserPresentableError {
|
||||
switch self {
|
||||
case .notAuthorizedToChangeRelayList:
|
||||
ErrorView.UserPresentableError(
|
||||
user_visible_description: NSLocalizedString("You do not have permission to alter this relay list.", comment: "Human readable error description"),
|
||||
tip: NSLocalizedString("Please make sure you have logged-in with your private key.", comment: "Human readable tip for error"),
|
||||
technical_info: nil
|
||||
)
|
||||
case .cannotFormRelayListEvent:
|
||||
ErrorView.UserPresentableError(
|
||||
user_visible_description: NSLocalizedString("There was a problem creating the relay list event.", comment: "Human readable error description"),
|
||||
tip: NSLocalizedString("Please try again later or contact support if the issue persists.", comment: "Human readable tip for error"),
|
||||
technical_info: "Failed forming Nostr event for the relay list update."
|
||||
)
|
||||
case .relayAlreadyExists:
|
||||
ErrorView.UserPresentableError(
|
||||
user_visible_description: NSLocalizedString("This relay is already in your list.", comment: "Human readable tip for error"),
|
||||
tip: NSLocalizedString("Check the address and/or the relay list.", comment: "Human readable tip for error"),
|
||||
technical_info: nil
|
||||
)
|
||||
case .noInitialRelayList:
|
||||
ErrorView.UserPresentableError(
|
||||
user_visible_description: NSLocalizedString("No initial relay list available to update.", comment: "Human readable error description"),
|
||||
tip: NSLocalizedString("Please go to Settings > First Aid > Repair relay list, or contact support.", comment: "Human readable tip for error"),
|
||||
technical_info: "Missing initial relay list data for reference during update."
|
||||
)
|
||||
case .noSuchRelay:
|
||||
ErrorView.UserPresentableError(
|
||||
user_visible_description: NSLocalizedString("The specified relay that you are trying to udpate was not found in your relay list.", comment: "Human readable error description"),
|
||||
tip: NSLocalizedString("This is an unexpected error, please contact support.", comment: "Human readable tip for error"),
|
||||
technical_info: nil
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum LoadingError: Error {
|
||||
case relayListParseError
|
||||
|
||||
var humanReadableError: ErrorView.UserPresentableError {
|
||||
switch self {
|
||||
case .relayListParseError:
|
||||
return ErrorView.UserPresentableError(
|
||||
user_visible_description: NSLocalizedString("Your relay list appears to be broken, so we cannot connect you to your Nostr network.", comment: "Human readable error description for a failure to parse the relay list due to a bad relay list"),
|
||||
tip: NSLocalizedString("Please contact support for further help.", comment: "Human readable tips for what to do for a failure to find the relay list"),
|
||||
technical_info: "Relay list could not be parsed."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
//
|
||||
// UserRelayListManager.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+102
-58
@@ -73,85 +73,130 @@ func render_note_content(ev: NostrEvent, profiles: Profiles, keypair: Keypair) -
|
||||
return .longform(LongformContent(ev.content))
|
||||
}
|
||||
|
||||
return .separated(render_blocks(blocks: blocks, profiles: profiles))
|
||||
return .separated(render_blocks(blocks: blocks, profiles: profiles, can_hide_last_previewable_refs: true))
|
||||
}
|
||||
|
||||
func render_blocks(blocks bs: Blocks, profiles: Profiles) -> NoteArtifactsSeparated {
|
||||
func render_blocks(blocks bs: Blocks, profiles: Profiles, can_hide_last_previewable_refs: Bool = false) -> NoteArtifactsSeparated {
|
||||
var invoices: [Invoice] = []
|
||||
var urls: [UrlType] = []
|
||||
let blocks = bs.blocks
|
||||
|
||||
let one_note_ref = blocks
|
||||
.filter({
|
||||
if case .mention(let mention) = $0,
|
||||
case .note = mention.ref {
|
||||
return true
|
||||
|
||||
var end_mention_count = 0
|
||||
var end_url_count = 0
|
||||
|
||||
// Search backwards until we find the beginning index of the chain of previewables that reach the end of the content.
|
||||
var hide_text_index = blocks.endIndex
|
||||
if can_hide_last_previewable_refs {
|
||||
outerLoop: for (i, block) in blocks.enumerated().reversed() {
|
||||
if block.is_previewable {
|
||||
switch block {
|
||||
case .mention:
|
||||
end_mention_count += 1
|
||||
|
||||
// If there is more than one previewable mention,
|
||||
// do not hide anything because we allow rich rendering of only one mention currently.
|
||||
// This should be fixed in the future to show events inline instead.
|
||||
if end_mention_count > 1 {
|
||||
hide_text_index = blocks.endIndex
|
||||
break outerLoop
|
||||
}
|
||||
case .url(let url):
|
||||
let url_type = classify_url(url)
|
||||
if case .link = url_type {
|
||||
end_url_count += 1
|
||||
|
||||
// If there is more than one link, do not hide anything because we allow rich rendering of only
|
||||
// one link.
|
||||
if end_url_count > 1 {
|
||||
hide_text_index = blocks.endIndex
|
||||
break outerLoop
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
hide_text_index = i
|
||||
} else if case .text(let txt) = block, txt.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
hide_text_index = i
|
||||
} else {
|
||||
break
|
||||
}
|
||||
else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
.count == 1
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var ind: Int = -1
|
||||
let txt: CompatibleText = blocks.reduce(CompatibleText()) { str, block in
|
||||
ind = ind + 1
|
||||
|
||||
|
||||
// Add the rendered previewable blocks to their type-specific lists.
|
||||
switch block {
|
||||
case .mention(let m):
|
||||
if case .note = m.ref, one_note_ref {
|
||||
case .invoice(let invoice):
|
||||
invoices.append(invoice)
|
||||
case .url(let url):
|
||||
let url_type = classify_url(url)
|
||||
urls.append(url_type)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if can_hide_last_previewable_refs {
|
||||
// If there are previewable blocks that occur before the consecutive sequence of them at the end of the content,
|
||||
// we should not hide the text representation of any previewable block to avoid altering the format of the note.
|
||||
if ind < hide_text_index && block.is_previewable {
|
||||
hide_text_index = blocks.endIndex
|
||||
}
|
||||
|
||||
// No need to show the text representation of the block if the only previewables are the sequence of them
|
||||
// found at the end of the content.
|
||||
// This is to save unnecessary use of screen space.
|
||||
if ind >= hide_text_index {
|
||||
return str
|
||||
}
|
||||
}
|
||||
|
||||
switch block {
|
||||
case .mention(let m):
|
||||
return str + mention_str(m, profiles: profiles)
|
||||
case .text(let txt):
|
||||
return str + CompatibleText(stringLiteral: reduce_text_block(blocks: blocks, ind: ind, txt: txt, one_note_ref: one_note_ref))
|
||||
|
||||
return str + CompatibleText(stringLiteral: reduce_text_block(ind: ind, hide_text_index: hide_text_index, txt: txt))
|
||||
case .relay(let relay):
|
||||
return str + CompatibleText(stringLiteral: relay)
|
||||
|
||||
case .hashtag(let htag):
|
||||
return str + hashtag_str(htag)
|
||||
case .invoice(let invoice):
|
||||
invoices.append(invoice)
|
||||
return str
|
||||
return str + invoice_str(invoice)
|
||||
case .url(let url):
|
||||
let url_type = classify_url(url)
|
||||
switch url_type {
|
||||
case .media:
|
||||
urls.append(url_type)
|
||||
return str
|
||||
case .link(let url):
|
||||
urls.append(url_type)
|
||||
return str + url_str(url)
|
||||
}
|
||||
return str + url_str(url)
|
||||
}
|
||||
}
|
||||
|
||||
return NoteArtifactsSeparated(content: txt, words: bs.words, urls: urls, invoices: invoices)
|
||||
}
|
||||
|
||||
func reduce_text_block(blocks: [Block], ind: Int, txt: String, one_note_ref: Bool) -> String {
|
||||
func reduce_text_block(ind: Int, hide_text_index: Int, txt: String) -> String {
|
||||
var trimmed = txt
|
||||
|
||||
if let prev = blocks[safe: ind-1],
|
||||
case .url(let u) = prev,
|
||||
classify_url(u).is_media != nil {
|
||||
trimmed = " " + trim_prefix(trimmed)
|
||||
|
||||
// Trim leading whitespaces.
|
||||
if ind == 0 {
|
||||
trimmed = trim_prefix(trimmed)
|
||||
}
|
||||
|
||||
if let next = blocks[safe: ind+1] {
|
||||
if case .url(let u) = next, classify_url(u).is_media != nil {
|
||||
trimmed = trim_suffix(trimmed)
|
||||
} else if case .mention(let m) = next,
|
||||
case .note = m.ref,
|
||||
one_note_ref {
|
||||
trimmed = trim_suffix(trimmed)
|
||||
}
|
||||
|
||||
// Trim trailing whitespaces if the following blocks will be hidden or if this is the last block.
|
||||
if ind == hide_text_index - 1 {
|
||||
trimmed = trim_suffix(trimmed)
|
||||
}
|
||||
|
||||
|
||||
return trimmed
|
||||
}
|
||||
|
||||
func invoice_str(_ invoice: Invoice) -> CompatibleText {
|
||||
var attributedString = AttributedString(stringLiteral: abbrev_identifier(invoice.string))
|
||||
attributedString.link = URL(string: "damus:lightning:\(invoice.string)")
|
||||
attributedString.foregroundColor = DamusColors.purple
|
||||
|
||||
return CompatibleText(attributed: attributedString)
|
||||
}
|
||||
|
||||
func url_str(_ url: URL) -> CompatibleText {
|
||||
var attributedString = AttributedString(stringLiteral: url.absoluteString)
|
||||
attributedString.link = url
|
||||
@@ -161,17 +206,16 @@ func url_str(_ url: URL) -> CompatibleText {
|
||||
}
|
||||
|
||||
func classify_url(_ url: URL) -> UrlType {
|
||||
let str = url.lastPathComponent.lowercased()
|
||||
|
||||
if str.hasSuffix(".png") || str.hasSuffix(".jpg") || str.hasSuffix(".jpeg") || str.hasSuffix(".gif") || str.hasSuffix(".webp") {
|
||||
let fileExtension = url.lastPathComponent.lowercased().components(separatedBy: ".").last
|
||||
|
||||
switch fileExtension {
|
||||
case "png", "jpg", "jpeg", "gif", "webp":
|
||||
return .media(.image(url))
|
||||
}
|
||||
|
||||
if str.hasSuffix(".mp4") || str.hasSuffix(".mov") || str.hasSuffix(".m3u8") {
|
||||
case "mp4", "mov", "m3u8":
|
||||
return .media(.video(url))
|
||||
default:
|
||||
return .link(url)
|
||||
}
|
||||
|
||||
return .link(url)
|
||||
}
|
||||
|
||||
func attributed_string_attach_icon(_ astr: inout AttributedString, img: UIImage) {
|
||||
@@ -194,11 +238,11 @@ func mention_str(_ m: Mention<MentionRef>, profiles: Profiles) -> CompatibleText
|
||||
let display_str: String = {
|
||||
switch m.ref {
|
||||
case .pubkey(let pk): return getDisplayName(pk: pk, profiles: profiles)
|
||||
case .note: return abbrev_pubkey(bech32String)
|
||||
case .nevent: return abbrev_pubkey(bech32String)
|
||||
case .note: return abbrev_identifier(bech32String)
|
||||
case .nevent: return abbrev_identifier(bech32String)
|
||||
case .nprofile(let nprofile): return getDisplayName(pk: nprofile.author, profiles: profiles)
|
||||
case .nrelay(let url): return url
|
||||
case .naddr: return abbrev_pubkey(bech32String)
|
||||
case .naddr: return abbrev_identifier(bech32String)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -54,7 +54,14 @@ func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent
|
||||
guard ev.age < EVENT_MAX_AGE_FOR_NOTIFICATION else {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
// Don't show notifications for future events.
|
||||
// Allow notes that are created no more than 3 seconds in the future
|
||||
// to account for natural clock skew between sender and receiver.
|
||||
guard ev.age >= -3 else {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,18 @@ import Foundation
|
||||
class ProfileModel: ObservableObject, Equatable {
|
||||
@Published var contacts: NostrEvent? = nil
|
||||
@Published var following: Int = 0
|
||||
@Published var relays: [RelayURL: RelayInfo]? = nil
|
||||
@Published var relay_list: NIP65.RelayList? = nil
|
||||
@Published var legacy_relay_list: [RelayURL: LegacyKind3RelayRWConfiguration]? = nil
|
||||
@Published var progress: Int = 0
|
||||
var relay_urls: [RelayURL]? {
|
||||
if let relay_list {
|
||||
return relay_list.relays.values.map({ $0.url })
|
||||
}
|
||||
if let legacy_relay_list {
|
||||
return Array(legacy_relay_list.keys)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private let MAX_SHARE_RELAYS = 4
|
||||
|
||||
@@ -59,16 +69,17 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
|
||||
func unsubscribe() {
|
||||
print("unsubscribing from profile \(pubkey) with sub_id \(sub_id)")
|
||||
damus.pool.unsubscribe(sub_id: sub_id)
|
||||
damus.pool.unsubscribe(sub_id: prof_subid)
|
||||
damus.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
|
||||
damus.nostrNetwork.pool.unsubscribe(sub_id: prof_subid)
|
||||
if pubkey != damus.pubkey {
|
||||
damus.pool.unsubscribe(sub_id: conversations_subid)
|
||||
damus.nostrNetwork.pool.unsubscribe(sub_id: conversations_subid)
|
||||
}
|
||||
}
|
||||
|
||||
func subscribe() {
|
||||
var text_filter = NostrFilter(kinds: [.text, .longform, .highlight])
|
||||
var profile_filter = NostrFilter(kinds: [.contacts, .metadata, .boost])
|
||||
var relay_list_filter = NostrFilter(kinds: [.relay_list], authors: [pubkey])
|
||||
|
||||
profile_filter.authors = [pubkey]
|
||||
|
||||
@@ -77,8 +88,8 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
|
||||
print("subscribing to textlike events from profile \(pubkey) with sub_id \(sub_id)")
|
||||
//print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]])
|
||||
damus.pool.subscribe(sub_id: sub_id, filters: [text_filter], handler: handle_event)
|
||||
damus.pool.subscribe(sub_id: prof_subid, filters: [profile_filter], handler: handle_event)
|
||||
damus.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: [text_filter], handler: handle_event)
|
||||
damus.nostrNetwork.pool.subscribe(sub_id: prof_subid, filters: [profile_filter, relay_list_filter], handler: handle_event)
|
||||
|
||||
subscribe_to_conversations()
|
||||
}
|
||||
@@ -94,7 +105,7 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
let conversations_filter_them = NostrFilter(kinds: conversation_kinds, pubkeys: [damus.pubkey], limit: limit, authors: [pubkey])
|
||||
let conversations_filter_us = NostrFilter(kinds: conversation_kinds, pubkeys: [pubkey], limit: limit, authors: [damus.pubkey])
|
||||
print("subscribing to conversation events from and to profile \(pubkey) with sub_id \(conversations_subid)")
|
||||
damus.pool.subscribe(sub_id: conversations_subid, filters: [conversations_filter_them, conversations_filter_us], handler: handle_event)
|
||||
damus.nostrNetwork.pool.subscribe(sub_id: conversations_subid, filters: [conversations_filter_them, conversations_filter_us], handler: handle_event)
|
||||
}
|
||||
|
||||
func handle_profile_contact_event(_ ev: NostrEvent) {
|
||||
@@ -109,7 +120,7 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
|
||||
self.contacts = ev
|
||||
self.following = count_pubkeys(ev.tags)
|
||||
self.relays = decode_json_relays(ev.content)
|
||||
self.legacy_relay_list = decode_json_relays(ev.content)
|
||||
}
|
||||
|
||||
private func add_event(_ ev: NostrEvent) {
|
||||
@@ -120,6 +131,9 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
} else if ev.known_kind == .contacts {
|
||||
handle_profile_contact_event(ev)
|
||||
}
|
||||
else if ev.known_kind == .relay_list {
|
||||
self.relay_list = try? NIP65.RelayList(event: ev) // Whether another user's list is malformatted is something beyond our control. Probably best to suppress errors
|
||||
}
|
||||
seen_event.insert(ev.id)
|
||||
}
|
||||
|
||||
@@ -192,7 +206,7 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
|
||||
private func findRelaysHandler(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
||||
if case .nostr_event(let resp) = ev, case .event(_, let event) = resp, case .contacts = event.known_kind {
|
||||
self.relays = decode_json_relays(event.content)
|
||||
self.legacy_relay_list = decode_json_relays(event.content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,15 +214,15 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
var profile_filter = NostrFilter(kinds: [.contacts])
|
||||
profile_filter.authors = [pubkey]
|
||||
|
||||
damus.pool.subscribe(sub_id: findRelay_subid, filters: [profile_filter], handler: findRelaysHandler)
|
||||
damus.nostrNetwork.pool.subscribe(sub_id: findRelay_subid, filters: [profile_filter], handler: findRelaysHandler)
|
||||
}
|
||||
|
||||
func unsubscribeFindRelays() {
|
||||
damus.pool.unsubscribe(sub_id: findRelay_subid)
|
||||
damus.nostrNetwork.pool.unsubscribe(sub_id: findRelay_subid)
|
||||
}
|
||||
|
||||
func getCappedRelayStrings() -> [String] {
|
||||
return relays?.keys.prefix(MAX_SHARE_RELAYS).map { $0.absoluteString } ?? []
|
||||
return self.relay_urls?.prefix(MAX_SHARE_RELAYS).map { $0.absoluteString } ?? []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,13 +41,13 @@ class SearchHomeModel: ObservableObject {
|
||||
|
||||
func subscribe() {
|
||||
loading = true
|
||||
let to_relays = determine_to_relays(pool: damus_state.pool, filters: damus_state.relay_filters)
|
||||
damus_state.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event, to: to_relays)
|
||||
let to_relays = determine_to_relays(pool: damus_state.nostrNetwork.pool, filters: damus_state.relay_filters)
|
||||
damus_state.nostrNetwork.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event, to: to_relays)
|
||||
}
|
||||
|
||||
func unsubscribe(to: RelayURL? = nil) {
|
||||
loading = false
|
||||
damus_state.pool.unsubscribe(sub_id: base_subid, to: to.map { [$0] })
|
||||
damus_state.nostrNetwork.pool.unsubscribe(sub_id: base_subid, to: to.map { [$0] })
|
||||
}
|
||||
|
||||
func handle_event(relay_id: RelayURL, conn_ev: NostrConnectionEvent) {
|
||||
@@ -140,7 +140,7 @@ func load_profiles<Y>(context: String, profiles_subid: String, relay_id: RelayUR
|
||||
|
||||
let filter = NostrFilter(kinds: [.metadata], authors: authors)
|
||||
|
||||
damus_state.pool.subscribe_to(sub_id: profiles_subid, filters: [filter], to: [relay_id]) { rid, conn_ev in
|
||||
damus_state.nostrNetwork.pool.subscribe_to(sub_id: profiles_subid, filters: [filter], to: [relay_id]) { rid, conn_ev in
|
||||
|
||||
let now = UInt64(Date.now.timeIntervalSince1970)
|
||||
switch conn_ev {
|
||||
@@ -156,7 +156,7 @@ func load_profiles<Y>(context: String, profiles_subid: String, relay_id: RelayUR
|
||||
}
|
||||
case .eose:
|
||||
print("load_profiles[\(context)]: done loading \(authors.count) profiles from \(relay_id)")
|
||||
damus_state.pool.unsubscribe(sub_id: profiles_subid, to: [relay_id])
|
||||
damus_state.nostrNetwork.pool.unsubscribe(sub_id: profiles_subid, to: [relay_id])
|
||||
case .ok:
|
||||
break
|
||||
case .notice:
|
||||
|
||||
@@ -41,13 +41,13 @@ class SearchModel: ObservableObject {
|
||||
//likes_filter.ids = ref_events.referenced_ids!
|
||||
|
||||
print("subscribing to search '\(search)' with sub_id \(sub_id)")
|
||||
state.pool.register_handler(sub_id: sub_id, handler: handle_event)
|
||||
state.nostrNetwork.pool.register_handler(sub_id: sub_id, handler: handle_event)
|
||||
loading = true
|
||||
state.pool.send(.subscribe(.init(filters: [search], sub_id: sub_id)))
|
||||
state.nostrNetwork.pool.send(.subscribe(.init(filters: [search], sub_id: sub_id)))
|
||||
}
|
||||
|
||||
func unsubscribe() {
|
||||
state.pool.unsubscribe(sub_id: sub_id)
|
||||
state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
|
||||
loading = false
|
||||
print("unsubscribing from search '\(search)' with sub_id \(sub_id)")
|
||||
}
|
||||
@@ -67,7 +67,7 @@ class SearchModel: ObservableObject {
|
||||
}
|
||||
|
||||
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
||||
let (sub_id, done) = handle_subid_event(pool: state.pool, relay_id: relay_id, ev: ev) { sub_id, ev in
|
||||
let (sub_id, done) = handle_subid_event(pool: state.nostrNetwork.pool, relay_id: relay_id, ev: ev) { sub_id, ev in
|
||||
if ev.is_textlike && ev.should_show_event {
|
||||
self.add_event(ev)
|
||||
}
|
||||
|
||||
@@ -88,12 +88,12 @@ class ThreadModel: ObservableObject {
|
||||
|
||||
/// Unsubscribe from events in the relay pool. Call this when unloading the view
|
||||
func unsubscribe() {
|
||||
self.damus_state.pool.remove_handler(sub_id: base_subid)
|
||||
self.damus_state.pool.remove_handler(sub_id: meta_subid)
|
||||
self.damus_state.pool.remove_handler(sub_id: profiles_subid)
|
||||
self.damus_state.pool.unsubscribe(sub_id: base_subid)
|
||||
self.damus_state.pool.unsubscribe(sub_id: meta_subid)
|
||||
self.damus_state.pool.unsubscribe(sub_id: profiles_subid)
|
||||
self.damus_state.nostrNetwork.pool.remove_handler(sub_id: base_subid)
|
||||
self.damus_state.nostrNetwork.pool.remove_handler(sub_id: meta_subid)
|
||||
self.damus_state.nostrNetwork.pool.remove_handler(sub_id: profiles_subid)
|
||||
self.damus_state.nostrNetwork.pool.unsubscribe(sub_id: base_subid)
|
||||
self.damus_state.nostrNetwork.pool.unsubscribe(sub_id: meta_subid)
|
||||
self.damus_state.nostrNetwork.pool.unsubscribe(sub_id: profiles_subid)
|
||||
Log.info("unsubscribing to thread %s with sub_id %s", for: .render, original_event.id.hex(), base_subid)
|
||||
}
|
||||
|
||||
@@ -129,8 +129,8 @@ class ThreadModel: ObservableObject {
|
||||
let meta_filters = [meta_events, quote_events]
|
||||
|
||||
Log.info("subscribing to thread %s with sub_id %s", for: .render, original_event.id.hex(), base_subid)
|
||||
damus_state.pool.subscribe(sub_id: base_subid, filters: base_filters, handler: handle_event)
|
||||
damus_state.pool.subscribe(sub_id: meta_subid, filters: meta_filters, handler: handle_event)
|
||||
damus_state.nostrNetwork.pool.subscribe(sub_id: base_subid, filters: base_filters, handler: handle_event)
|
||||
damus_state.nostrNetwork.pool.subscribe(sub_id: meta_subid, filters: meta_filters, handler: handle_event)
|
||||
}
|
||||
|
||||
/// Adds an event to this thread.
|
||||
@@ -176,7 +176,7 @@ class ThreadModel: ObservableObject {
|
||||
/// Marked as private because it is this class' responsibility to load events, not the view's. Simplify the interface
|
||||
@MainActor
|
||||
private func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
||||
let (sub_id, done) = handle_subid_event(pool: damus_state.pool, relay_id: relay_id, ev: ev) { sid, ev in
|
||||
let (sub_id, done) = handle_subid_event(pool: damus_state.nostrNetwork.pool, relay_id: relay_id, ev: ev) { sid, ev in
|
||||
guard subids.contains(sid) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -43,6 +43,15 @@ struct DamusURLHandler {
|
||||
return .route(.Script(script: model))
|
||||
case .purple(let purple_url):
|
||||
return await damus_state.purple.handle(purple_url: purple_url)
|
||||
case .invoice(let invoice):
|
||||
if damus_state.settings.show_wallet_selector {
|
||||
return .sheet(.select_wallet(invoice: invoice.string))
|
||||
} else {
|
||||
guard let url = try? getUrlToOpen(invoice: invoice.string, with: damus_state.settings.default_wallet.model) else {
|
||||
return .sheet(.select_wallet(invoice: invoice.string))
|
||||
}
|
||||
return .external_url(url)
|
||||
}
|
||||
case nil:
|
||||
break
|
||||
}
|
||||
@@ -91,6 +100,11 @@ struct DamusURLHandler {
|
||||
return .filter(filt)
|
||||
case .script(let script):
|
||||
return .script(script)
|
||||
case .invoice(let bolt11):
|
||||
if let invoice = decode_bolt11(bolt11) {
|
||||
return .invoice(invoice)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -103,5 +117,6 @@ struct DamusURLHandler {
|
||||
case wallet_connect(WalletConnectURL)
|
||||
case script([UInt8])
|
||||
case purple(DamusPurpleURL)
|
||||
case invoice(Invoice)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,9 @@ class UserSettingsStore: ObservableObject {
|
||||
@Setting(key: "show_wallet_selector", default_value: false)
|
||||
var show_wallet_selector: Bool
|
||||
|
||||
@Setting(key: "dismiss_wallet_high_balance_warning", default_value: false)
|
||||
var dismiss_wallet_high_balance_warning: Bool
|
||||
|
||||
@Setting(key: "left_handed", default_value: false)
|
||||
var left_handed: Bool
|
||||
|
||||
@@ -174,8 +177,12 @@ class UserSettingsStore: ObservableObject {
|
||||
var truncate_timeline_text: Bool
|
||||
|
||||
/// Nozaps mode gimps note zapping to fit into apple's content-tipping guidelines. It can not be configurable to end-users on the app store
|
||||
@Setting(key: "nozaps", default_value: true)
|
||||
var nozaps: Bool
|
||||
///
|
||||
/// Update 2025-05-12: This can be re-enabled 🥳. See https://github.com/damus-io/damus/issues/3016
|
||||
// @Setting(key: "nozaps", default_value: true)
|
||||
var nozaps: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@Setting(key: "truncate_mention_text", default_value: true)
|
||||
var truncate_mention_text: Bool
|
||||
@@ -342,6 +349,10 @@ class UserSettingsStore: ObservableObject {
|
||||
@Setting(key: "draft_event_ids", default_value: nil)
|
||||
var draft_event_ids: [String]?
|
||||
|
||||
// TODO: Get rid of this once we have NostrDB query capabilities integrated
|
||||
@Setting(key: "latest_relay_list_event_id", default_value: nil)
|
||||
var latestRelayListEventIdHex: String?
|
||||
|
||||
// MARK: Helper types
|
||||
|
||||
enum NotificationsMode: String, CaseIterable, Identifiable, StringCodable, Equatable {
|
||||
|
||||
@@ -31,11 +31,11 @@ class ZapsModel: ObservableObject {
|
||||
case .note(let note_target):
|
||||
filter.referenced_ids = [note_target.note_id]
|
||||
}
|
||||
state.pool.subscribe(sub_id: zaps_subid, filters: [filter], handler: handle_event)
|
||||
state.nostrNetwork.pool.subscribe(sub_id: zaps_subid, filters: [filter], handler: handle_event)
|
||||
}
|
||||
|
||||
func unsubscribe() {
|
||||
state.pool.unsubscribe(sub_id: zaps_subid)
|
||||
state.nostrNetwork.pool.unsubscribe(sub_id: zaps_subid)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
||||
@@ -52,4 +52,28 @@ extension NIP04 {
|
||||
|
||||
return create_encrypted_event(message, to_pk: to_pk, tags: tags, keypair: keypair, created_at: created, kind: 4)
|
||||
}
|
||||
|
||||
/// Decrypts string content
|
||||
static func decryptContent(recipientPrivateKey: Privkey, senderPubkey: Pubkey, content: String, encoding: EncEncoding) throws(NIP04DecryptionError) -> String {
|
||||
guard let shared_sec = get_shared_secret(privkey: recipientPrivateKey, pubkey: senderPubkey) else {
|
||||
throw .failedToComputeSharedSecret
|
||||
}
|
||||
guard let dat = (encoding == .base64 ? decode_dm_base64(content) : decode_dm_bech32(content)) else {
|
||||
throw .failedToDecodeEncryptedContent
|
||||
}
|
||||
guard let dat = aes_decrypt(data: dat.content, iv: dat.iv, shared_sec: shared_sec) else {
|
||||
throw .failedToDecryptAES
|
||||
}
|
||||
guard let decryptedString = String(data: dat, encoding: .utf8) else {
|
||||
throw .utf8DecodingFailedOnDecryptedPayload
|
||||
}
|
||||
return decryptedString
|
||||
}
|
||||
|
||||
enum NIP04DecryptionError: Error {
|
||||
case failedToComputeSharedSecret
|
||||
case failedToDecodeEncryptedContent
|
||||
case failedToDecryptAES
|
||||
case utf8DecodingFailedOnDecryptedPayload
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
//
|
||||
// NIP65.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino 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
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
-1
@@ -34,6 +34,19 @@ protocol TagConvertible {
|
||||
static func from_tag(tag: TagSequence) -> Self?
|
||||
}
|
||||
|
||||
/// Protocol for types that can be converted from/to a tag sequence with the possibilty of an error
|
||||
protocol ThrowingTagConvertible {
|
||||
associatedtype E: Error
|
||||
var tag: [String] { get }
|
||||
static func fromTag(tag: TagSequence) throws(E) -> Self?
|
||||
}
|
||||
|
||||
/// Protocol for types that can be converted from/to a tag item
|
||||
protocol TagItemConvertible {
|
||||
var tagItem: String? { get }
|
||||
static func fromTagItem(_ item: String?) -> Self?
|
||||
}
|
||||
|
||||
struct QuoteId: IdType, TagKey, TagConvertible {
|
||||
let id: Data
|
||||
|
||||
@@ -130,8 +143,16 @@ struct ReplaceableParam: TagConvertible {
|
||||
var keychar: AsciiCharacter { "d" }
|
||||
}
|
||||
|
||||
struct Signature: Hashable, Equatable {
|
||||
struct Signature: Codable, Hashable, Equatable {
|
||||
let data: Data
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
self.init(try hex_decoder(decoder, expected_len: 64))
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
try hex_encoder(to: encoder, data: self.data)
|
||||
}
|
||||
|
||||
init(_ p: Data) {
|
||||
self.data = p
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
func make_auth_request(keypair: FullKeypair, challenge_string: String, relay: Relay) -> NostrEvent? {
|
||||
func make_auth_request(keypair: FullKeypair, challenge_string: String, relay: RelayPool.Relay) -> NostrEvent? {
|
||||
let tags: [[String]] = [["relay", relay.descriptor.url.absoluteString],["challenge", challenge_string]]
|
||||
let event = NostrEvent(content: "", keypair: keypair.to_keypair(), kind: 22242, tags: tags)
|
||||
return event
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
func make_zap_request_event(keypair: FullKeypair, content: String, relays: [RelayDescriptor], target: ZapTarget, zap_type: ZapType) -> MakeZapRequest? {
|
||||
func make_zap_request_event(keypair: FullKeypair, content: String, relays: [RelayPool.RelayDescriptor], target: ZapTarget, zap_type: ZapType) -> MakeZapRequest? {
|
||||
var tags = zap_target_to_tags(target)
|
||||
var relay_tag = ["relays"]
|
||||
relay_tag.append(contentsOf: relays.map { $0.url.absoluteString })
|
||||
@@ -78,8 +78,8 @@ func make_private_zap_request_event(identity: FullKeypair, enc_key: FullKeypair,
|
||||
|
||||
func make_first_contact_event(keypair: Keypair) -> NostrEvent? {
|
||||
let bootstrap_relays = load_bootstrap_relays(pubkey: keypair.pubkey)
|
||||
let rw_relay_info = RelayInfo(read: true, write: true)
|
||||
var relays: [RelayURL: RelayInfo] = [:]
|
||||
let rw_relay_info = LegacyKind3RelayRWConfiguration(read: true, write: true)
|
||||
var relays: [RelayURL: LegacyKind3RelayRWConfiguration] = [:]
|
||||
|
||||
for relay in bootstrap_relays {
|
||||
relays[relay] = rw_relay_info
|
||||
|
||||
@@ -13,6 +13,18 @@ import CryptoKit
|
||||
import NaturalLanguage
|
||||
|
||||
|
||||
/// A protocol for structs and classes that can convert themselves from/to a NostrEvent
|
||||
protocol NostrEventConvertible {
|
||||
associatedtype E: Error
|
||||
|
||||
/// Iniitialize this type from a NostrEvent
|
||||
init(event: NostrEvent) throws(E)
|
||||
|
||||
/// Convert this type into a Nostr Event, using a keypair for signing and a specific timestamp
|
||||
func toNostrEvent(keypair: FullKeypair, timestamp: UInt32?) -> NostrEvent?
|
||||
}
|
||||
|
||||
|
||||
enum ValidationResult: Decodable {
|
||||
case unknown
|
||||
case ok
|
||||
@@ -367,6 +379,10 @@ func decode_json<T: Decodable>(_ val: String) -> T? {
|
||||
return try? JSONDecoder().decode(T.self, from: Data(val.utf8))
|
||||
}
|
||||
|
||||
func decode_json_throwing<T: Decodable>(_ val: String) throws -> T {
|
||||
return try JSONDecoder().decode(T.self, from: Data(val.utf8))
|
||||
}
|
||||
|
||||
func decode_data<T: Decodable>(_ data: Data) -> T? {
|
||||
let decoder = JSONDecoder()
|
||||
do {
|
||||
@@ -527,6 +543,7 @@ func event_to_json(ev: NostrEvent) -> String {
|
||||
return str
|
||||
}
|
||||
|
||||
@available(*, deprecated, renamed: "NIP04.decryptContent", message: "Deprecated, please use NIP04.decryptContent instead")
|
||||
func decrypt_dm(_ privkey: Privkey?, pubkey: Pubkey, content: String, encoding: EncEncoding) -> String? {
|
||||
guard let privkey = privkey else {
|
||||
return nil
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import Foundation
|
||||
|
||||
|
||||
/// A known Nostr event kind, addressable by name, with the actual number assigned by the protocol as the value
|
||||
enum NostrKind: UInt32, Codable {
|
||||
case metadata = 0
|
||||
case text = 1
|
||||
@@ -18,6 +19,7 @@ enum NostrKind: UInt32, Codable {
|
||||
case like = 7
|
||||
case chat = 42
|
||||
case mute_list = 10000
|
||||
case relay_list = 10002
|
||||
case list_deprecated = 30000
|
||||
case draft = 31234
|
||||
case longform = 30023
|
||||
|
||||
@@ -12,6 +12,7 @@ enum NostrLink: Equatable {
|
||||
case ref(RefId)
|
||||
case filter(NostrFilter)
|
||||
case script([UInt8])
|
||||
case invoice(String)
|
||||
}
|
||||
|
||||
func encode_pubkey_uri(_ pubkey: Pubkey) -> String {
|
||||
@@ -93,8 +94,15 @@ func decode_nostr_uri(_ s: String) -> NostrLink? {
|
||||
return
|
||||
}
|
||||
|
||||
if parts.count >= 2 && parts[0] == "t" {
|
||||
return .filter(NostrFilter(hashtag: [parts[1].lowercased()]))
|
||||
if parts.count >= 2 {
|
||||
switch parts[0] {
|
||||
case "t":
|
||||
return .filter(NostrFilter(hashtag: [parts[1].lowercased()]))
|
||||
case "lightning":
|
||||
return .invoice(parts[1])
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
guard parts.count == 1 else {
|
||||
|
||||
@@ -12,11 +12,14 @@ struct NostrSubscribe {
|
||||
let sub_id: String
|
||||
}
|
||||
|
||||
|
||||
/// Models a request/message that is sent to a Nostr relay
|
||||
enum NostrRequestType {
|
||||
/// A standard nostr request
|
||||
case typical(NostrRequest)
|
||||
/// A customized nostr request. Generally used in the context of a nostrscript.
|
||||
case custom(String)
|
||||
|
||||
/// Whether this request is meant to write data to a relay
|
||||
var is_write: Bool {
|
||||
guard case .typical(let req) = self else {
|
||||
return true
|
||||
@@ -25,6 +28,7 @@ enum NostrRequestType {
|
||||
return req.is_write
|
||||
}
|
||||
|
||||
/// Whether this request is meant to read data from a relay
|
||||
var is_read: Bool {
|
||||
guard case .typical(let req) = self else {
|
||||
return true
|
||||
@@ -34,12 +38,18 @@ enum NostrRequestType {
|
||||
}
|
||||
}
|
||||
|
||||
/// Models a standard request/message that is sent to a Nostr relay.
|
||||
enum NostrRequest {
|
||||
/// Subscribes to receive information from the relay
|
||||
case subscribe(NostrSubscribe)
|
||||
/// Unsubscribes from an existing subscription, addressed by its id
|
||||
case unsubscribe(String)
|
||||
/// Posts an event
|
||||
case event(NostrEvent)
|
||||
/// Authenticate with the relay
|
||||
case auth(NostrEvent)
|
||||
|
||||
/// Whether this request is meant to write data to a relay
|
||||
var is_write: Bool {
|
||||
switch self {
|
||||
case .subscribe:
|
||||
@@ -53,6 +63,7 @@ enum NostrRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether this request is meant to read data from a relay
|
||||
var is_read: Bool {
|
||||
return !is_write
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ class Profiles {
|
||||
@MainActor
|
||||
private var profiles: [Pubkey: ProfileData] = [:]
|
||||
|
||||
// Map of validated NIP-05 address to pubkey.
|
||||
@MainActor
|
||||
var nip05_pubkey: [String: Pubkey] = [:]
|
||||
|
||||
|
||||
@@ -115,6 +115,19 @@ enum FollowRef: TagKeys, Hashable, TagConvertible, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Models common tag references defined by the Nostr protocol, and their associated values.
|
||||
///
|
||||
/// For example, this raw JSON tag sequence:
|
||||
/// ```json
|
||||
/// ["p", "8b2be0a0ad34805d76679272c28a77dbede9adcbfdca48c681ec8b624a1208a6"]
|
||||
/// ```
|
||||
///
|
||||
/// would be parsed into something equivalent to `.pubkey(Pubkey(hex: "8b2be0a0ad34805d76679272c28a77dbede9adcbfdca48c681ec8b624a1208a6"))`
|
||||
///
|
||||
/// ## Notes
|
||||
///
|
||||
/// - Not all tag information from all NIPs can be modelled using this alone, as some NIPs may define extra associated values for specific event types. You may need to use a specialized type for some event kinds
|
||||
///
|
||||
enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
||||
case event(NoteId)
|
||||
case pubkey(Pubkey)
|
||||
@@ -124,6 +137,7 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
||||
case naddr(NAddr)
|
||||
case reference(String)
|
||||
|
||||
/// The key that defines the type of reference being made
|
||||
var key: RefKey {
|
||||
switch self {
|
||||
case .event: return .e
|
||||
@@ -136,6 +150,14 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the type of reference being made on a Nostr event tag
|
||||
///
|
||||
/// Example:
|
||||
/// ```json
|
||||
/// ["p", "8b2be0a0ad34805d76679272c28a77dbede9adcbfdca48c681ec8b624a1208a6"]
|
||||
/// ```
|
||||
///
|
||||
/// The `RefKey` is "p"
|
||||
enum RefKey: AsciiCharacter, TagKey, CustomStringConvertible {
|
||||
case e, p, t, d, q, a, r
|
||||
|
||||
@@ -148,10 +170,12 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
/// A raw nostr-style tag sequence representation of this object
|
||||
var tag: [String] {
|
||||
[self.key.description, self.description]
|
||||
}
|
||||
|
||||
|
||||
/// Describes what is being referenced, as a `String`
|
||||
var description: String {
|
||||
switch self {
|
||||
case .event(let noteId): return noteId.hex()
|
||||
@@ -166,6 +190,7 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a raw tag sequence
|
||||
static func from_tag(tag: TagSequence) -> RefId? {
|
||||
var i = tag.makeIterator()
|
||||
|
||||
|
||||
+88
-50
@@ -7,16 +7,25 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct RelayInfo: Codable {
|
||||
let read: Bool?
|
||||
let write: Bool?
|
||||
public struct LegacyKind3RelayRWConfiguration: Codable, Sendable {
|
||||
public let read: Bool?
|
||||
public let write: Bool?
|
||||
|
||||
init(read: Bool, write: Bool) {
|
||||
self.read = read
|
||||
self.write = write
|
||||
}
|
||||
|
||||
static let rw = RelayInfo(read: true, write: true)
|
||||
static let rw = LegacyKind3RelayRWConfiguration(read: true, write: true)
|
||||
|
||||
func toNIP65RWConfiguration() -> NIP65.RelayList.RelayItem.RWConfiguration? {
|
||||
switch (self.read, self.write) {
|
||||
case (false, true): return .write
|
||||
case (true, false): return .read
|
||||
case (true, true): return .readWrite
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum RelayVariant {
|
||||
@@ -25,30 +34,33 @@ enum RelayVariant {
|
||||
case nwc
|
||||
}
|
||||
|
||||
public struct RelayDescriptor {
|
||||
let url: RelayURL
|
||||
let info: RelayInfo
|
||||
let variant: RelayVariant
|
||||
|
||||
init(url: RelayURL, info: RelayInfo, variant: RelayVariant = .regular) {
|
||||
self.url = url
|
||||
self.info = info
|
||||
self.variant = variant
|
||||
}
|
||||
|
||||
var ephemeral: Bool {
|
||||
switch variant {
|
||||
case .regular:
|
||||
return false
|
||||
case .ephemeral:
|
||||
return true
|
||||
case .nwc:
|
||||
return true
|
||||
extension RelayPool {
|
||||
/// Describes a relay for use in `RelayPool`
|
||||
public struct RelayDescriptor {
|
||||
let url: RelayURL
|
||||
var info: NIP65.RelayList.RelayItem.RWConfiguration
|
||||
let variant: RelayVariant
|
||||
|
||||
init(url: RelayURL, info: NIP65.RelayList.RelayItem.RWConfiguration, variant: RelayVariant = .regular) {
|
||||
self.url = url
|
||||
self.info = info
|
||||
self.variant = variant
|
||||
}
|
||||
|
||||
var ephemeral: Bool {
|
||||
switch variant {
|
||||
case .regular:
|
||||
return false
|
||||
case .ephemeral:
|
||||
return true
|
||||
case .nwc:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
static func nwc(url: RelayURL) -> RelayDescriptor {
|
||||
return RelayDescriptor(url: url, info: .readWrite, variant: .nwc)
|
||||
}
|
||||
}
|
||||
|
||||
static func nwc(url: RelayURL) -> RelayDescriptor {
|
||||
return RelayDescriptor(url: url, info: .rw, variant: .nwc)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,30 +141,56 @@ struct RelayMetadata: Codable {
|
||||
}
|
||||
}
|
||||
|
||||
class Relay: Identifiable {
|
||||
let descriptor: RelayDescriptor
|
||||
let connection: RelayConnection
|
||||
var authentication_state: RelayAuthenticationState
|
||||
|
||||
var flags: Int
|
||||
|
||||
init(descriptor: RelayDescriptor, connection: RelayConnection) {
|
||||
self.flags = 0
|
||||
self.descriptor = descriptor
|
||||
self.connection = connection
|
||||
self.authentication_state = RelayAuthenticationState.none
|
||||
extension RelayPool {
|
||||
class Relay: Identifiable {
|
||||
var descriptor: RelayDescriptor
|
||||
let connection: RelayConnection
|
||||
var authentication_state: RelayAuthenticationState
|
||||
|
||||
var flags: Int
|
||||
|
||||
init(descriptor: RelayDescriptor, connection: RelayConnection) {
|
||||
self.flags = 0
|
||||
self.descriptor = descriptor
|
||||
self.connection = connection
|
||||
self.authentication_state = RelayAuthenticationState.none
|
||||
}
|
||||
|
||||
var is_broken: Bool {
|
||||
return (flags & RelayFlags.broken.rawValue) == RelayFlags.broken.rawValue
|
||||
}
|
||||
|
||||
var id: RelayURL {
|
||||
return descriptor.url
|
||||
}
|
||||
}
|
||||
|
||||
var is_broken: Bool {
|
||||
return (flags & RelayFlags.broken.rawValue) == RelayFlags.broken.rawValue
|
||||
}
|
||||
|
||||
var id: RelayURL {
|
||||
return descriptor.url
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum RelayError: Error {
|
||||
case RelayAlreadyExists
|
||||
extension RelayPool {
|
||||
enum RelayError: Error {
|
||||
case RelayAlreadyExists
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Extension to bridge NIP-65 relay list structs with app-native objects
|
||||
|
||||
extension NIP65.RelayList {
|
||||
static func fromLegacyContactList(_ contactList: NdbNote) throws(BridgeError) -> Self {
|
||||
guard let relayListInfo = decode_json_relays(contactList.content) else { throw .couldNotDecodeRelayListInfo }
|
||||
let relayItems = relayListInfo.map({ url, rwConfiguration in
|
||||
return RelayItem(url: url, rwConfiguration: rwConfiguration.toNIP65RWConfiguration() ?? .readWrite)
|
||||
})
|
||||
return NIP65.RelayList(relays: relayItems)
|
||||
}
|
||||
|
||||
static func fromLegacyContactList(_ contactList: NdbNote?) throws(BridgeError) -> Self? {
|
||||
guard let contactList = contactList else { return nil }
|
||||
return try fromLegacyContactList(contactList)
|
||||
}
|
||||
|
||||
enum BridgeError: Error {
|
||||
case couldNotDecodeRelayListInfo
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,13 +24,15 @@ struct SeenEvent: Hashable {
|
||||
let evid: NoteId
|
||||
}
|
||||
|
||||
/// Establishes and manages connections and subscriptions to a list of relays.
|
||||
class RelayPool {
|
||||
var relays: [Relay] = []
|
||||
private(set) var relays: [Relay] = []
|
||||
var handlers: [RelayHandler] = []
|
||||
var request_queue: [QueuedRequest] = []
|
||||
var seen: Set<SeenEvent> = Set()
|
||||
var counts: [RelayURL: UInt64] = [:]
|
||||
var ndb: Ndb
|
||||
/// The keypair used to authenticate with relays
|
||||
var keypair: Keypair?
|
||||
var message_received_function: (((String, RelayDescriptor)) -> Void)?
|
||||
var message_sent_function: (((String, Relay)) -> Void)?
|
||||
@@ -122,7 +124,7 @@ class RelayPool {
|
||||
}
|
||||
}
|
||||
|
||||
func add_relay(_ desc: RelayDescriptor) throws {
|
||||
func add_relay(_ desc: RelayDescriptor) throws(RelayError) {
|
||||
let relay_id = desc.url
|
||||
if get_relay(relay_id) != nil {
|
||||
throw RelayError.RelayAlreadyExists
|
||||
@@ -200,6 +202,64 @@ class RelayPool {
|
||||
register_handler(sub_id: sub_id, handler: handler)
|
||||
send(.subscribe(.init(filters: filters, sub_id: sub_id)), to: to)
|
||||
}
|
||||
|
||||
/// Subscribes to data from the `RelayPool` based on a filter and a list of desired relays.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - filters: The filters specifying the desired content.
|
||||
/// - desiredRelays: The desired relays which to subsctibe to. If `nil`, it defaults to the `RelayPool`'s default list
|
||||
/// - eoseTimeout: The maximum timeout which to give up waiting for the eoseSignal, in seconds
|
||||
/// - Returns: Returns an async stream that callers can easily consume via a for-loop
|
||||
func subscribe(filters: [NostrFilter], to desiredRelays: [RelayURL]? = nil, eoseTimeout: TimeInterval = 10) -> AsyncStream<StreamItem> {
|
||||
let desiredRelays = desiredRelays ?? self.relays.map({ $0.descriptor.url })
|
||||
return AsyncStream<StreamItem> { continuation in
|
||||
let sub_id = UUID().uuidString
|
||||
var seenEvents: Set<NoteId> = []
|
||||
var relaysWhoFinishedInitialResults: Set<RelayURL> = []
|
||||
var eoseSent = false
|
||||
self.subscribe(sub_id: sub_id, filters: filters, handler: { (relayUrl, connectionEvent) in
|
||||
switch connectionEvent {
|
||||
case .ws_event(let ev):
|
||||
// Websocket events such as connect/disconnect/error are already handled in `RelayConnection`. Do not perform any handling here.
|
||||
// For the future, perhaps we should abstract away `.ws_event` in `RelayPool`? Seems like something to be handled on the `RelayConnection` layer.
|
||||
break
|
||||
case .nostr_event(let nostrResponse):
|
||||
guard nostrResponse.subid == sub_id else { return } // Do not stream items that do not belong in this subscription
|
||||
switch nostrResponse {
|
||||
case .event(_, let nostrEvent):
|
||||
if seenEvents.contains(nostrEvent.id) { break } // Don't send two of the same events.
|
||||
continuation.yield(with: .success(.event(nostrEvent)))
|
||||
seenEvents.insert(nostrEvent.id)
|
||||
case .notice(let note):
|
||||
break // We do not support handling these yet
|
||||
case .eose(_):
|
||||
relaysWhoFinishedInitialResults.insert(relayUrl)
|
||||
if relaysWhoFinishedInitialResults == Set(desiredRelays) {
|
||||
continuation.yield(with: .success(.eose))
|
||||
eoseSent = true
|
||||
}
|
||||
case .ok(_): break // No need to handle this, we are not sending an event to the relay
|
||||
case .auth(_): break // Handled in a separate function in RelayPool
|
||||
}
|
||||
}
|
||||
}, to: desiredRelays)
|
||||
Task {
|
||||
try? await Task.sleep(nanoseconds: 1_000_000_000 * UInt64(eoseTimeout))
|
||||
if !eoseSent { continuation.yield(with: .success(.eose)) }
|
||||
}
|
||||
continuation.onTermination = { @Sendable _ in
|
||||
self.unsubscribe(sub_id: sub_id, to: desiredRelays)
|
||||
self.remove_handler(sub_id: sub_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum StreamItem {
|
||||
/// A Nostr event
|
||||
case event(NostrEvent)
|
||||
/// The "end of stored events" signal
|
||||
case eose
|
||||
}
|
||||
|
||||
func subscribe_to(sub_id: String, filters: [NostrFilter], to: [RelayURL]?, handler: @escaping (RelayURL, NostrConnectionEvent) -> ()) {
|
||||
register_handler(sub_id: sub_id, handler: handler)
|
||||
@@ -243,19 +303,19 @@ class RelayPool {
|
||||
func send_raw(_ req: NostrRequestType, to: [RelayURL]? = nil, skip_ephemeral: Bool = true) {
|
||||
let relays = to.map{ get_relays($0) } ?? self.relays
|
||||
|
||||
self.send_raw_to_local_ndb(req)
|
||||
self.send_raw_to_local_ndb(req) // Always send Nostr events and data to NostrDB for a local copy
|
||||
|
||||
for relay in relays {
|
||||
if req.is_read && !(relay.descriptor.info.read ?? true) {
|
||||
continue
|
||||
if req.is_read && !(relay.descriptor.info.canRead) {
|
||||
continue // Do not send read requests to relays that are not READ relays
|
||||
}
|
||||
|
||||
if req.is_write && !(relay.descriptor.info.write ?? true) {
|
||||
continue
|
||||
if req.is_write && !(relay.descriptor.info.canWrite) {
|
||||
continue // Do not send write requests to relays that are not WRITE relays
|
||||
}
|
||||
|
||||
if relay.descriptor.ephemeral && skip_ephemeral {
|
||||
continue
|
||||
continue // Do not send requests to ephemeral relays if we want to skip them
|
||||
}
|
||||
|
||||
guard relay.connection.isConnected else {
|
||||
@@ -354,7 +414,7 @@ class RelayPool {
|
||||
}
|
||||
|
||||
func add_rw_relay(_ pool: RelayPool, _ url: RelayURL) {
|
||||
try? pool.add_relay(RelayDescriptor(url: url, info: .rw))
|
||||
try? pool.add_relay(RelayPool.RelayDescriptor(url: url, info: .readWrite))
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -83,8 +83,7 @@ var test_damus_state: DamusState = ({
|
||||
let our_pubkey = test_pubkey
|
||||
let pool = RelayPool(ndb: ndb)
|
||||
let settings = UserSettingsStore()
|
||||
let damus = DamusState(pool: pool,
|
||||
keypair: test_keypair,
|
||||
let damus = DamusState(keypair: test_keypair,
|
||||
likes: .init(our_pubkey: our_pubkey),
|
||||
boosts: .init(our_pubkey: our_pubkey),
|
||||
contacts: .init(our_pubkey: our_pubkey),
|
||||
@@ -100,8 +99,6 @@ var test_damus_state: DamusState = ({
|
||||
drafts: .init(),
|
||||
events: .init(ndb: ndb),
|
||||
bookmarks: .init(pubkey: our_pubkey),
|
||||
postbox: .init(pool: pool),
|
||||
bootstrap_relays: .init(),
|
||||
replies: .init(our_pubkey: our_pubkey),
|
||||
wallet: .init(settings: settings),
|
||||
nav: .init(),
|
||||
@@ -109,7 +106,8 @@ var test_damus_state: DamusState = ({
|
||||
video: .init(),
|
||||
ndb: ndb,
|
||||
quote_reposts: .init(our_pubkey: our_pubkey),
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true),
|
||||
favicon_cache: .init()
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
+17
-1
@@ -37,7 +37,23 @@ enum Block: Equatable {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var is_previewable: Bool {
|
||||
switch self {
|
||||
case .mention(let m):
|
||||
switch m.ref {
|
||||
case .note, .nevent: return true
|
||||
default: return false
|
||||
}
|
||||
case .invoice:
|
||||
return true
|
||||
case .url:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
case text(String)
|
||||
case mention(Mention<MentionRef>)
|
||||
case hashtag(String)
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
//
|
||||
// CoinosDeterministicClient.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-04-14.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Implements a client that can talk to the Coinos API server with a deterministic account derived from the user's private key.
|
||||
///
|
||||
/// This is NOT a general-purpose Coinos client, and only works with the user's own deterministic "one-click setup" Coinos wallet account.
|
||||
class CoinosDeterministicAccountClient {
|
||||
// MARK: - State
|
||||
|
||||
/// The user's normal keypair for using Nostr
|
||||
private let userKeypair: FullKeypair
|
||||
/// The JWT authentication token with Coinos
|
||||
private var jwtAuthToken: String? = nil
|
||||
|
||||
|
||||
// MARK: - Computed properties for a deterministic wallet
|
||||
|
||||
/// A deterministic keypair for the NWC connection derived from the user's private key
|
||||
private var nwcKeypair: FullKeypair? {
|
||||
let nwcPrivateKey: Privkey = Privkey(sha256(self.userKeypair.privkey.id)) // SHA256 is an irreversible operation, user's nsec should not be deriveable from this new private key
|
||||
return FullKeypair(privkey: nwcPrivateKey)
|
||||
}
|
||||
|
||||
/// A deterministic username for a Coinos account
|
||||
private var username: String? {
|
||||
// Derive from private key because deriving from a pubkey would mean that anyone could compute the username and take that username before our user
|
||||
// Add some prefix so that we can ensure this will NOT match the password nor the NWC keypair
|
||||
guard let fullText = sha256Hex(text: "coinos_username:" + self.userKeypair.privkey.hex()) else { return nil }
|
||||
// There is very little risk of a birthday attack on getting only the first 16 characters, because:
|
||||
// 1. before this user creates an account, no one else knows the private key in order to know the expected username and create an account before them
|
||||
// 2. after the account is created and username is revealed, finding collisions is pointless as duplicate usernames will be rejected by Coinos
|
||||
//
|
||||
// In terms of the risk of an accidental collision due to the birthday problem, 16 characters should be enough to pragmatically avoid any collision.
|
||||
// According to `https://en.wikipedia.org/wiki/Birthday_problem#Probability_table`,
|
||||
// even if we have 610 million Damus users connected to Coinos, the probability of even a single collision is still as low as 1%.
|
||||
return String(fullText.prefix(16))
|
||||
}
|
||||
|
||||
/// A deterministic password for a Coinos account
|
||||
private var password: String? {
|
||||
// Add some prefix so that we can ensure this will NOT match the user nor the NWC private key
|
||||
return sha256Hex(text: "coinos_password:" + self.userKeypair.privkey.hex())
|
||||
}
|
||||
|
||||
/// A deterministic NWC app connection name
|
||||
private var nwcConnectionName: String { return "Damus" }
|
||||
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
/// Initializes the client with the user's keypair
|
||||
init(userKeypair: FullKeypair) {
|
||||
self.userKeypair = userKeypair
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Authentication and registration
|
||||
|
||||
/// Tries to login to the user's deterministic account. If it cannot be found, it will register for one and log into that.
|
||||
func loginOrRegister() async throws {
|
||||
do {
|
||||
// Check if client has an account
|
||||
try await self.login()
|
||||
}
|
||||
catch {
|
||||
guard let error = error as? CoinosDeterministicAccountClient.ClientError, error == .unauthorized else { throw error }
|
||||
// Client does not seem to have an account, create one
|
||||
try await self.register()
|
||||
try await self.login()
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers for a Coinos account using deterministic account details.
|
||||
///
|
||||
/// It succeeds if it returns without throwing errors.
|
||||
func register() async throws {
|
||||
guard let username, let password else { throw ClientError.errorFormingRequest }
|
||||
let registerPayload = RegisterRequest(user: UserCredentials(username: username, password: password))
|
||||
let jsonData = try JSONEncoder().encode(registerPayload)
|
||||
|
||||
let url = URL(string: "https://coinos.io/api/register")!
|
||||
let (data, response) = try await makeRequest(method: .post, url: url, payload: jsonData, payload_type: .json)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
|
||||
return
|
||||
} else {
|
||||
throw ClientError.unexpectedHTTPResponse(status_code: (response as? HTTPURLResponse)?.statusCode ?? -1, response: data)
|
||||
}
|
||||
}
|
||||
|
||||
/// Logs into the deterministic account, if an auth token is not present
|
||||
func loginIfNeeded() async throws {
|
||||
if self.jwtAuthToken == nil { try await self.login() }
|
||||
}
|
||||
|
||||
/// Logs into to our deterministic account.
|
||||
///
|
||||
/// Succeeds if it returns without returning errors.
|
||||
///
|
||||
/// Mutating function, will update the client's internal state.
|
||||
func login() async throws {
|
||||
self.jwtAuthToken = try await sendLoginRequest().token
|
||||
}
|
||||
|
||||
/// Sends the login request and return the response
|
||||
///
|
||||
/// Does NOT update the internal login state.
|
||||
private func sendLoginRequest() async throws -> AuthResponse {
|
||||
guard let url = URL(string: "https://coinos.io/api/login") else { throw ClientError.errorFormingRequest }
|
||||
guard let username, let password else { throw ClientError.errorFormingRequest }
|
||||
let credentials = UserCredentials(username: username, password: password)
|
||||
let jsonData = try JSONEncoder().encode(credentials)
|
||||
|
||||
let (data, response) = try await makeRequest(method: .post, url: url, payload: jsonData, payload_type: .json)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
switch httpResponse.statusCode {
|
||||
case 200: return try JSONDecoder().decode(AuthResponse.self, from: data)
|
||||
case 401: throw ClientError.unauthorized
|
||||
default: throw ClientError.unexpectedHTTPResponse(status_code: httpResponse.statusCode, response: data)
|
||||
}
|
||||
}
|
||||
throw ClientError.errorProcessingResponse
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Managing NWC connections
|
||||
|
||||
/// Creates a new NWC connection
|
||||
///
|
||||
/// Note: Account must exist before calling this endpoint
|
||||
func createNWCConnection() async throws -> WalletConnectURL {
|
||||
guard let nwcKeypair else { throw ClientError.errorFormingRequest }
|
||||
guard let urlEndpoint = URL(string: "https://coinos.io/api/app") else { throw ClientError.errorFormingRequest }
|
||||
|
||||
try await self.loginIfNeeded()
|
||||
|
||||
let config = try defaultWalletConnectionConfig()
|
||||
let configData = try encode_json_data(config)
|
||||
|
||||
let (data, response) = try await self.makeAuthenticatedRequest(
|
||||
method: .post,
|
||||
url: urlEndpoint,
|
||||
payload: configData,
|
||||
payload_type: .json
|
||||
)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
switch httpResponse.statusCode {
|
||||
case 200:
|
||||
guard let nwc = try await self.getNWCUrl() else { throw ClientError.errorProcessingResponse }
|
||||
return nwc
|
||||
case 401: throw ClientError.unauthorized
|
||||
default: throw ClientError.unexpectedHTTPResponse(status_code: httpResponse.statusCode, response: data)
|
||||
}
|
||||
}
|
||||
throw ClientError.errorProcessingResponse
|
||||
}
|
||||
|
||||
/// Returns the default wallet connection config
|
||||
private func defaultWalletConnectionConfig() throws -> NewWalletConnectionConfig {
|
||||
guard let nwcKeypair else { throw ClientError.errorFormingRequest }
|
||||
return NewWalletConnectionConfig(
|
||||
name: self.nwcConnectionName,
|
||||
secret: nwcKeypair.privkey.hex(),
|
||||
pubkey: nwcKeypair.pubkey.hex(),
|
||||
max_amount: 30000, // 30K sats per week maximum
|
||||
budget_renewal: .weekly
|
||||
)
|
||||
}
|
||||
|
||||
/// Gets the NWC URL for the deterministic NWC app connection
|
||||
///
|
||||
/// Account must already exist before calling this
|
||||
///
|
||||
/// Returns `nil` if no NWC url is found, (e.g. if app connection has not been configured yet)
|
||||
func getNWCUrl() async throws -> WalletConnectURL? {
|
||||
guard let connectionConfig = try await self.getNWCAppConnectionConfig(), let nwc = connectionConfig.nwc else { return nil }
|
||||
return WalletConnectURL(str: nwc)
|
||||
}
|
||||
|
||||
/// Gets the deterministic NWC app connection configuration details, if it exists
|
||||
///
|
||||
/// Account must already exist before calling this
|
||||
///
|
||||
/// Returns `nil` if no connection is found, (e.g. if app connection has not been configured yet)
|
||||
func getNWCAppConnectionConfig() async throws -> WalletConnectionConfig? {
|
||||
guard let nwcKeypair else { throw ClientError.errorFormingRequest }
|
||||
guard let url = URL(string: "https://coinos.io/api/app/" + nwcKeypair.pubkey.hex()) else { throw ClientError.errorFormingRequest }
|
||||
|
||||
try await self.loginIfNeeded()
|
||||
|
||||
let (data, response) = try await self.makeAuthenticatedRequest(
|
||||
method: .get,
|
||||
url: url,
|
||||
payload: nil,
|
||||
payload_type: nil
|
||||
)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
switch httpResponse.statusCode {
|
||||
case 200: return try JSONDecoder().decode(WalletConnectionConfig.self, from: data)
|
||||
case 401: throw ClientError.unauthorized
|
||||
case 404: return nil
|
||||
default: throw ClientError.unexpectedHTTPResponse(status_code: httpResponse.statusCode, response: data)
|
||||
}
|
||||
}
|
||||
throw ClientError.errorProcessingResponse
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Lower level request convenience functions
|
||||
|
||||
/// Makes a request without any authorization
|
||||
func makeRequest(method: HTTPMethod, url: URL, payload: Data?, payload_type: HTTPPayloadType?) async throws -> (data: Data, response: URLResponse) {
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method.rawValue
|
||||
request.httpBody = payload
|
||||
|
||||
if let payload_type {
|
||||
request.setValue(payload_type.rawValue, forHTTPHeaderField: "Content-Type")
|
||||
}
|
||||
return try await URLSession.shared.data(for: request)
|
||||
}
|
||||
|
||||
/// Makes an authenticated request with our JWT auth token.
|
||||
///
|
||||
/// Client must be logged-in before calling this, otherwise an error will be thrown.
|
||||
func makeAuthenticatedRequest(method: HTTPMethod, url: URL, payload: Data?, payload_type: HTTPPayloadType?) async throws -> (data: Data, response: URLResponse) {
|
||||
guard let jwtAuthToken else { throw ClientError.errorFormingRequest }
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method.rawValue
|
||||
request.httpBody = payload
|
||||
|
||||
request.setValue("Bearer " + jwtAuthToken, forHTTPHeaderField: "Authorization")
|
||||
if let payload_type {
|
||||
request.setValue(payload_type.rawValue, forHTTPHeaderField: "Content-Type")
|
||||
}
|
||||
return try await URLSession.shared.data(for: request)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Helper structures
|
||||
|
||||
/// Payload for registering for a new Coinos account
|
||||
struct RegisterRequest: Codable {
|
||||
/// New user credentials
|
||||
let user: UserCredentials
|
||||
}
|
||||
|
||||
/// Payload for user credentials (sign-up and login)
|
||||
struct UserCredentials: Codable {
|
||||
/// The username
|
||||
let username: String
|
||||
/// The user password
|
||||
let password: String
|
||||
}
|
||||
|
||||
/// A successful response to a login auth endpoint
|
||||
struct AuthResponse: Codable {
|
||||
/// The JWT token to be applied to any authenticated API calls
|
||||
let token: String
|
||||
}
|
||||
|
||||
/// Used by the client to define new NWC configurations
|
||||
struct NewWalletConnectionConfig: Codable {
|
||||
/// The name of the connection
|
||||
let name: String
|
||||
/// 32 Hex-encoded bytes containing a shared private key secret
|
||||
let secret: String
|
||||
/// 32 Hex-encoded bytes containing the pubkey for the secret
|
||||
let pubkey: String
|
||||
/// Max amount that can be spent in each renewal period (measured in sats)
|
||||
let max_amount: UInt64
|
||||
/// The period of time it takes for the budget limits to reset
|
||||
let budget_renewal: BudgetRenewalPeriod
|
||||
}
|
||||
|
||||
/// The NWC connection configuration details
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - All items defined as optionals because the Coinos API may change in the future, so this may help increase future compatibility.
|
||||
struct WalletConnectionConfig: Codable {
|
||||
/// The name of the connection
|
||||
let name: String?
|
||||
/// 32 Hex-encoded bytes containing a shared private key secret
|
||||
let secret: String?
|
||||
/// 32 Hex-encoded bytes containing the pubkey for the secret
|
||||
let pubkey: String?
|
||||
/// Max amount that can be spent in every renewal period (measured in sats)
|
||||
let max_amount: UInt64?
|
||||
/// The NWC url generated by the server
|
||||
let nwc: String?
|
||||
/// Budget renewal information
|
||||
let budget_renewal: BudgetRenewalPeriod?
|
||||
}
|
||||
|
||||
/// A period of time it takes for budget limits to be reset
|
||||
enum BudgetRenewalPeriod: String, Codable {
|
||||
/// Resets once a week
|
||||
case weekly
|
||||
}
|
||||
|
||||
/// A client error occured
|
||||
enum ClientError: Error, Equatable {
|
||||
/// Received an unexpected HTTP response
|
||||
///
|
||||
/// Could be for a variety of reasons.
|
||||
case unexpectedHTTPResponse(status_code: Int, response: Data)
|
||||
/// Error forming the request, generally due to missing or inconsistent internal data
|
||||
///
|
||||
/// Probably caused by a programming error.
|
||||
case errorFormingRequest
|
||||
/// The client could not process the response from the server
|
||||
///
|
||||
/// Might be a sign of an incompatibility bug
|
||||
case errorProcessingResponse
|
||||
/// The action performed is not authorized
|
||||
/// Generally thrown if user does not exist, credentials do not match what Coinos has on file, or programming error
|
||||
case unauthorized
|
||||
/// Client not logged in on a call that expected login
|
||||
case notLoggedIn
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes a SHA256 hash digest from a piece of UTF-8 text, and returns the result as a "hex" string
|
||||
///
|
||||
/// When working only with strings, this can be more convenient than transforming text to data, and data back to text
|
||||
fileprivate func sha256Hex(text: String) -> String? {
|
||||
guard let data = text.data(using: .utf8) else { return nil }
|
||||
return sha256(data).toHexString()
|
||||
}
|
||||
@@ -80,9 +80,9 @@ func parse_display_name(name: String?, display_name: String?, pubkey: Pubkey) ->
|
||||
}
|
||||
|
||||
func abbrev_bech32_pubkey(pubkey: Pubkey) -> String {
|
||||
return abbrev_pubkey(String(pubkey.npub.dropFirst(4)))
|
||||
return abbrev_identifier(String(pubkey.npub.dropFirst(4)))
|
||||
}
|
||||
|
||||
func abbrev_pubkey(_ pubkey: String, amount: Int = 8) -> String {
|
||||
func abbrev_identifier(_ pubkey: String, amount: Int = 8) -> String {
|
||||
return pubkey.prefix(amount) + ":" + pubkey.suffix(amount)
|
||||
}
|
||||
|
||||
@@ -354,7 +354,7 @@ func preload_image(url: URL) {
|
||||
|
||||
//print("Preloading image \(url.absoluteString)")
|
||||
|
||||
KingfisherManager.shared.retrieveImage(with: Kingfisher.ImageResource(downloadURL: url)) { val in
|
||||
KingfisherManager.shared.retrieveImage(with: Kingfisher.KF.ImageResource(downloadURL: url)) { val in
|
||||
//print("Preloaded image \(url.absoluteString)")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,15 +29,15 @@ extension KFOptionSetter {
|
||||
options.onlyLoadFirstFrame = disable_animation
|
||||
|
||||
switch imageContext {
|
||||
case .pfp:
|
||||
options.diskCacheExpiration = .days(60)
|
||||
break
|
||||
case .banner:
|
||||
options.diskCacheExpiration = .days(5)
|
||||
break
|
||||
case .note:
|
||||
options.diskCacheExpiration = .days(1)
|
||||
break
|
||||
case .pfp, .favicon:
|
||||
options.diskCacheExpiration = .days(60)
|
||||
break
|
||||
case .banner:
|
||||
options.diskCacheExpiration = .days(5)
|
||||
break
|
||||
case .note:
|
||||
options.diskCacheExpiration = .days(1)
|
||||
break
|
||||
}
|
||||
|
||||
return self
|
||||
@@ -52,7 +52,7 @@ extension KFOptionSetter {
|
||||
|
||||
func onFailure(fallbackUrl: URL?, cacheKey: String?) -> Self {
|
||||
guard let url = fallbackUrl, let key = cacheKey else { return self }
|
||||
let imageResource = Kingfisher.ImageResource(downloadURL: url, cacheKey: key)
|
||||
let imageResource = Kingfisher.KF.ImageResource(downloadURL: url, cacheKey: key)
|
||||
let source = imageResource.convertToSource()
|
||||
options.alternativeSources = [source]
|
||||
|
||||
@@ -82,11 +82,14 @@ enum ImageContext {
|
||||
case pfp
|
||||
case banner
|
||||
case note
|
||||
|
||||
case favicon
|
||||
|
||||
func maxMebibyteSize() -> Int {
|
||||
switch self {
|
||||
case .favicon:
|
||||
return 512_000 // 500KiB
|
||||
case .pfp:
|
||||
return 5_242_880 // 5Mib
|
||||
return 5_242_880 // 5MiB
|
||||
case .banner, .note:
|
||||
return 20_971_520 // 20MiB
|
||||
}
|
||||
@@ -94,6 +97,8 @@ enum ImageContext {
|
||||
|
||||
func downsampleSize() -> CGSize {
|
||||
switch self {
|
||||
case .favicon:
|
||||
return CGSize(width: 18, height: 18)
|
||||
case .pfp:
|
||||
return CGSize(width: 200, height: 200)
|
||||
case .banner:
|
||||
@@ -159,20 +164,25 @@ struct CustomCacheSerializer: CacheSerializer {
|
||||
}
|
||||
}
|
||||
|
||||
class CustomSessionDelegate: SessionDelegate {
|
||||
override func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
|
||||
class CustomSessionDelegate: SessionDelegate, @unchecked Sendable {
|
||||
override func urlSession(
|
||||
_ session: URLSession,
|
||||
dataTask: URLSessionDataTask,
|
||||
didReceive response: URLResponse
|
||||
) async -> URLSession.ResponseDisposition {
|
||||
let contentLength = response.expectedContentLength
|
||||
|
||||
// Content-Length header is optional (-1 when missing)
|
||||
if (contentLength != -1 && contentLength > MAX_FILE_SIZE) {
|
||||
return super.urlSession(session, dataTask: dataTask, didReceive: URLResponse(), completionHandler: completionHandler)
|
||||
return await super.urlSession(session, dataTask: dataTask, didReceive: URLResponse())
|
||||
}
|
||||
|
||||
super.urlSession(session, dataTask: dataTask, didReceive: response, completionHandler: completionHandler)
|
||||
return await super.urlSession(session, dataTask: dataTask, didReceive: response)
|
||||
}
|
||||
}
|
||||
|
||||
class CustomImageDownloader: ImageDownloader {
|
||||
|
||||
class CustomImageDownloader: ImageDownloader, @unchecked Sendable {
|
||||
|
||||
static let shared = CustomImageDownloader(name: "shared")
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// FaviconCache.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 5/23/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import FaviconFinder
|
||||
|
||||
class FaviconCache {
|
||||
private var nip05DomainFavicons: [String: [FaviconURL]] = [:]
|
||||
|
||||
@MainActor
|
||||
func lookup(_ domain: String) async -> [FaviconURL] {
|
||||
let lowercasedDomain = domain.lowercased()
|
||||
if let faviconURLs = nip05DomainFavicons[lowercasedDomain] {
|
||||
return faviconURLs
|
||||
}
|
||||
|
||||
guard let siteURL = URL(string: "https://\(lowercasedDomain)"),
|
||||
let faviconURLs = try? await FaviconFinder(
|
||||
url: siteURL,
|
||||
configuration: .init(
|
||||
preferredSource: .ico, // Prefer using common favicon .ico filenames at root level to avoid scraping HTML when possible.
|
||||
preferences: [
|
||||
.html: FaviconFormatType.appleTouchIcon.rawValue,
|
||||
.ico: "favicon.ico",
|
||||
.webApplicationManifestFile: FaviconFormatType.launcherIcon4x.rawValue
|
||||
]
|
||||
)
|
||||
).fetchFaviconURLs()
|
||||
else {
|
||||
return []
|
||||
}
|
||||
|
||||
nip05DomainFavicons[lowercasedDomain] = faviconURLs
|
||||
|
||||
return faviconURLs
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
//
|
||||
// ImageCacheMigrations.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-04-26.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Kingfisher
|
||||
|
||||
struct ImageCacheMigrations {
|
||||
static func migrateKingfisherCacheIfNeeded() {
|
||||
let fileManager = FileManager.default
|
||||
let defaults = UserDefaults.standard
|
||||
let migration1Key = "KingfisherCacheMigrated" // Never ever changes
|
||||
let migration2Key = "KingfisherCacheMigratedV2" // Never ever changes
|
||||
|
||||
let migration1Done = defaults.bool(forKey: migration1Key)
|
||||
let migration2Done = defaults.bool(forKey: migration2Key)
|
||||
|
||||
guard !migration1Done || !migration2Done else {
|
||||
// All migrations are already done. Skip.
|
||||
return
|
||||
}
|
||||
|
||||
let oldCachePath = migration1Done ? migration1KingfisherCachePath() : migration0KingfisherCachePath()
|
||||
|
||||
// New shared cache location
|
||||
let newCachePath = kingfisherCachePath().path
|
||||
|
||||
if fileManager.fileExists(atPath: oldCachePath) {
|
||||
do {
|
||||
// Move the old cache to the new location
|
||||
try fileManager.moveItem(atPath: oldCachePath, toPath: newCachePath)
|
||||
Log.info("Successfully migrated Kingfisher cache to %s", for: .storage, newCachePath)
|
||||
} catch {
|
||||
do {
|
||||
// Cache data is not essential, fallback to deleting the cache and starting all over
|
||||
// It's better than leaving significant garbage data stuck indefinitely on the user's phone
|
||||
try fileManager.removeItem(atPath: newCachePath)
|
||||
try fileManager.removeItem(atPath: oldCachePath)
|
||||
}
|
||||
catch {
|
||||
Log.error("Failed to migrate cache: %s", for: .storage, error.localizedDescription)
|
||||
return // Do not mark them as complete, we can try again next time the user reloads the app
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark migrations as complete
|
||||
defaults.set(true, forKey: migration1Key)
|
||||
defaults.set(true, forKey: migration2Key)
|
||||
}
|
||||
|
||||
static private func migration0KingfisherCachePath() -> String {
|
||||
// Implementation note: These are old, so they should not be changed
|
||||
let defaultCache = ImageCache.default
|
||||
return defaultCache.diskStorage.directoryURL.path
|
||||
}
|
||||
|
||||
static private func migration1KingfisherCachePath() -> String {
|
||||
// Implementation note: These are old, so they are hard-coded on purpose, because we can't change these values from the past.
|
||||
let groupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.damus")!
|
||||
return groupURL.appendingPathComponent("ImageCache").path
|
||||
}
|
||||
|
||||
/// The latest path for kingfisher to store cached images on.
|
||||
///
|
||||
/// Documentation references:
|
||||
/// - https://developer.apple.com/documentation/foundation/filemanager/containerurl(forsecurityapplicationgroupidentifier:)#:~:text=The%20system%20creates%20only%20the%20Library/Caches%20subdirectory%20automatically
|
||||
/// - https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html#:~:text=Put%20data%20cache,files%20as%20needed.
|
||||
static func kingfisherCachePath() -> URL {
|
||||
let groupURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: Constants.DAMUS_APP_GROUP_IDENTIFIER)!
|
||||
return groupURL
|
||||
.appendingPathComponent("Library")
|
||||
.appendingPathComponent("Caches")
|
||||
.appendingPathComponent(Constants.IMAGE_CACHE_DIRNAME)
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ enum CancelSendErr {
|
||||
}
|
||||
|
||||
class PostBox {
|
||||
let pool: RelayPool
|
||||
private let pool: RelayPool
|
||||
var events: [NoteId: PostedEvent]
|
||||
|
||||
init(pool: RelayPool) {
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Stores information, metadata, and logs about different relays. Generally used as a singleton.
|
||||
///
|
||||
/// # Discussion
|
||||
///
|
||||
/// This class is primarily used as a shared singleton in `DamusState`, to allow other parts of the app to access information, metadata, and logs about relays without having to fetch it themselves.
|
||||
///
|
||||
/// For example, it is used by `RelayView` to supplement information about the relay without having to fetch those again from the network, as well as to display logs collected throughout the use of the app.
|
||||
final class RelayModelCache: ObservableObject {
|
||||
private var models = [RelayURL: RelayModel]()
|
||||
|
||||
|
||||
+15
-2
@@ -5,6 +5,7 @@
|
||||
// Created by Scott Penrose on 5/7/23.
|
||||
//
|
||||
|
||||
import FaviconFinder
|
||||
import SwiftUI
|
||||
|
||||
enum Route: Hashable {
|
||||
@@ -46,6 +47,8 @@ enum Route: Hashable {
|
||||
case Wallet(wallet: WalletModel)
|
||||
case WalletScanner(result: Binding<WalletScanResult>)
|
||||
case FollowersYouKnow(friendedFollowers: [Pubkey], followers: FollowersModel)
|
||||
case NIP05DomainEvents(events: NIP05DomainEventsModel, nip05_domain_favicon: FaviconURL?)
|
||||
case NIP05DomainPubkeys(domain: String, nip05_domain_favicon: FaviconURL?, pubkeys: [Pubkey])
|
||||
|
||||
@ViewBuilder
|
||||
func view(navigationCoordinator: NavigationCoordinator, damusState: DamusState) -> some View {
|
||||
@@ -126,7 +129,11 @@ enum Route: Hashable {
|
||||
case .FollowersYouKnow(let friendedFollowers, let followers):
|
||||
FollowersYouKnowView(damus_state: damusState, friended_followers: friendedFollowers, followers: followers)
|
||||
case .Script(let load_model):
|
||||
LoadScript(pool: damusState.pool, model: load_model)
|
||||
LoadScript(pool: damusState.nostrNetwork.pool, model: load_model)
|
||||
case .NIP05DomainEvents(let events, let nip05_domain_favicon):
|
||||
NIP05DomainTimelineView(damus_state: damusState, model: events, nip05_domain_favicon: nip05_domain_favicon)
|
||||
case .NIP05DomainPubkeys(let domain, let nip05_domain_favicon, let pubkeys):
|
||||
NIP05DomainPubkeysView(damus_state: damusState, domain: domain, nip05_domain_favicon: nip05_domain_favicon, pubkeys: pubkeys)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +216,7 @@ enum Route: Hashable {
|
||||
case .Search(let search):
|
||||
hasher.combine("search")
|
||||
hasher.combine(search.search)
|
||||
case .NDBSearch(let results):
|
||||
case .NDBSearch:
|
||||
hasher.combine("results")
|
||||
case .EULA:
|
||||
hasher.combine("eula")
|
||||
@@ -231,6 +238,12 @@ enum Route: Hashable {
|
||||
case .Script(let model):
|
||||
hasher.combine("script")
|
||||
hasher.combine(model.data.count)
|
||||
case .NIP05DomainEvents(let events, _):
|
||||
hasher.combine("nip05DomainEvents")
|
||||
hasher.combine(events.domain)
|
||||
case .NIP05DomainPubkeys(let domain, _, _):
|
||||
hasher.combine("nip05DomainPubkeys")
|
||||
hasher.combine(domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,11 +35,11 @@ func remove_nostr_uri_prefix(_ s: String) -> String {
|
||||
return uri
|
||||
}
|
||||
|
||||
func abbreviateURL(_ url: URL) -> String {
|
||||
func abbreviateURL(_ url: URL, maxLength: Int = MAX_CHAR_URL) -> String {
|
||||
let urlString = url.absoluteString
|
||||
|
||||
if urlString.count > MAX_CHAR_URL {
|
||||
return String(urlString.prefix(MAX_CHAR_URL)) + "..."
|
||||
if urlString.count > maxLength {
|
||||
return String(urlString.prefix(maxLength)) + "…"
|
||||
}
|
||||
return urlString
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// HumanReadableErrors.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-05-05.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension WalletConnect.FullWalletResponse.InitializationError {
|
||||
var humanReadableError: ErrorView.UserPresentableError? {
|
||||
switch self {
|
||||
case .incorrectAuthorPubkey:
|
||||
nil // Anyone can send a response event with an incorrect author pubkey, it is not really an "error". We should silently ignore it.
|
||||
case .missingRequestIdReference:
|
||||
.init(
|
||||
user_visible_description: NSLocalizedString("Wallet provider returned an invalid response.", comment: "Error description shown to the user when a response from the wallet provider is invalid"),
|
||||
tip: NSLocalizedString("Please copy the technical info and send it to our support team.", comment: "Tip on how to resolve issue when wallet returns an invalid response"),
|
||||
technical_info: "Wallet response does not make a reference to any request; No request ID `e` tag was found."
|
||||
)
|
||||
case .failedToDecodeJSON(let error):
|
||||
.init(
|
||||
user_visible_description: NSLocalizedString("Wallet provider returned a response that we do not understand.", comment: "Error description shown to the user when a response from the wallet provider contains data the app does not understand"),
|
||||
tip: NSLocalizedString("Please copy the technical info and send it to our support team.", comment: "Tip on how to resolve issue when wallet returns an invalid response"),
|
||||
technical_info: "Failed to decode NWC Wallet response JSON. Error: \(error)"
|
||||
)
|
||||
case .failedToDecrypt(let error):
|
||||
.init(
|
||||
user_visible_description: NSLocalizedString("Wallet provider returned a response that we could not decrypt.", comment: "Error description shown to the user when a response from the wallet provider contains data the app could not decrypt."),
|
||||
tip: NSLocalizedString("Please copy the technical info and send it to our support team.", comment: "Tip on how to resolve issue when wallet returns an invalid response"),
|
||||
technical_info: "Failed to decrypt NWC Wallet response. Error: \(error)"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension WalletConnect.WalletResponseErr {
|
||||
var humanReadableError: ErrorView.UserPresentableError? {
|
||||
guard let code = self.code else {
|
||||
return .init(
|
||||
user_visible_description: String(format: NSLocalizedString("Your connected wallet raised an unknown error. Message: %s", comment: "Human readable error description for unknown error"), self.message ?? NSLocalizedString("Empty error message", comment: "A human readable placeholder to indicate that the error message is empty")),
|
||||
tip: NSLocalizedString("Please contact the developer of your wallet provider for help.", comment: "Human readable error description for an unknown error raised by a wallet provider."),
|
||||
technical_info: "NWC wallet provider returned an error response without a valid reason code. Message: \(self.message ?? "Empty error message")"
|
||||
)
|
||||
}
|
||||
switch code {
|
||||
case .rateLimited:
|
||||
return .init(
|
||||
user_visible_description: NSLocalizedString("Your wallet is temporarily being rate limited.", comment: "Error description for rate limit error"),
|
||||
tip: NSLocalizedString("Wait a few moments, and then try again.", comment: "Tip for rate limit error"),
|
||||
technical_info: "Wallet returned a rate limit error with message: \(self.message ?? "No further details provided")"
|
||||
)
|
||||
case .notImplemented:
|
||||
return .init(
|
||||
user_visible_description: NSLocalizedString("This feature is not implemented by your wallet.", comment: "Error description for not implemented feature"),
|
||||
tip: NSLocalizedString("Please check for updates or contact your wallet provider.", comment: "Tip for not implemented error"),
|
||||
technical_info: "Wallet reported a not implemented error. Message: \(self.message ?? "No further details provided")"
|
||||
)
|
||||
case .insufficientBalance:
|
||||
return .init(
|
||||
user_visible_description: NSLocalizedString("Your wallet does not have sufficient balance for this transaction.", comment: "Error description for insufficient balance"),
|
||||
tip: NSLocalizedString("Please deposit more funds and try again.", comment: "Tip for insufficient balance errors"),
|
||||
technical_info: "Wallet returned an insufficient balance error. Message: \(self.message ?? "No further details provided")"
|
||||
)
|
||||
case .quotaExceeded:
|
||||
return .init(
|
||||
user_visible_description: NSLocalizedString("Your transaction quota has been exceeded.", comment: "Error description for quota exceeded"),
|
||||
tip: NSLocalizedString("Wait for the quota to reset, or configure your wallet provider to allow a higher limit.", comment: "Tip for quota exceeded"),
|
||||
technical_info: "Wallet reported a quota exceeded error. Message: \(self.message ?? "No further details provided")"
|
||||
)
|
||||
case .restricted:
|
||||
return .init(
|
||||
user_visible_description: NSLocalizedString("This operation is restricted by your wallet.", comment: "Error description for restricted operation"),
|
||||
tip: NSLocalizedString("Check your account permissions or contact support.", comment: "Tip for restricted operation"),
|
||||
technical_info: "Wallet returned a restricted error. Message: \(self.message ?? "No further details provided")"
|
||||
)
|
||||
case .unauthorized:
|
||||
return .init(
|
||||
user_visible_description: NSLocalizedString("You are not authorized to perform this action with your wallet.", comment: "Error description for unauthorized access"),
|
||||
tip: NSLocalizedString("Please verify your credentials or permissions.", comment: "Tip for unauthorized access"),
|
||||
technical_info: "Wallet returned an unauthorized error. Message: \(self.message ?? "No further details provided")"
|
||||
)
|
||||
case .internalError:
|
||||
return .init(
|
||||
user_visible_description: NSLocalizedString("An internal error occurred in your wallet.", comment: "Error description for an internal error"),
|
||||
tip: NSLocalizedString("Try restarting your wallet or contacting support if the problem persists.", comment: "Tip for internal error"),
|
||||
technical_info: "Wallet reported an internal error. Message: \(self.message ?? "No further details provided")"
|
||||
)
|
||||
case .other:
|
||||
return .init(
|
||||
user_visible_description: NSLocalizedString("An unspecified error occurred in your wallet.", comment: "Error description for an unspecified error"),
|
||||
tip: NSLocalizedString("Please try again or contact your wallet provider for further assistance.", comment: "Tip for unspecified error"),
|
||||
technical_info: "Wallet returned an error: \(self.message ?? "No further details provided")"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,11 @@ extension WalletConnect {
|
||||
/// Pay an invoice
|
||||
case payInvoice(
|
||||
/// bolt-11 invoice string
|
||||
invoice: String
|
||||
invoice: String,
|
||||
/// The full description of the invoice (If description does not fit in the BOLT-11 invoice, this is the pre-image of the description hash)
|
||||
description: String?,
|
||||
/// Optional metadata object containing more information
|
||||
metadata: Metadata?
|
||||
)
|
||||
/// Get the current wallet balance
|
||||
case getBalance
|
||||
@@ -33,6 +37,38 @@ extension WalletConnect {
|
||||
type: String?
|
||||
)
|
||||
|
||||
static func payZapRequest(invoice: String, zapRequest: NostrEvent?) -> Self {
|
||||
guard let zapRequest, let zapRequestEncoded = encode_json(zapRequest) else {
|
||||
return WalletConnect.Request.payInvoice(
|
||||
invoice: invoice,
|
||||
description: nil,
|
||||
metadata: nil
|
||||
)
|
||||
}
|
||||
return WalletConnect.Request.payInvoice(
|
||||
invoice: invoice,
|
||||
description: zapRequestEncoded,
|
||||
metadata: .init(nostr: zapRequest)
|
||||
)
|
||||
}
|
||||
|
||||
struct Metadata: Codable, Equatable, Hashable {
|
||||
/// NIP-57-compliant `kind:9734` zap request event
|
||||
let nostr: NostrEvent?
|
||||
|
||||
init(nostr: NostrEvent?) {
|
||||
self.nostr = nostr
|
||||
}
|
||||
|
||||
init(from decoder: any Decoder) throws {
|
||||
let container: KeyedDecodingContainer<WalletConnect.Request.Metadata.CodingKeys> = try decoder.container(keyedBy: WalletConnect.Request.Metadata.CodingKeys.self)
|
||||
guard let decodedZapRequest = try? container.decodeIfPresent(NostrEvent.self, forKey: WalletConnect.Request.Metadata.CodingKeys.nostr) else {
|
||||
self.nostr = nil // Be lenient and fallback to nil if the NWC provider provided something invalid, since metadata is not strictly spec'd yet.
|
||||
return
|
||||
}
|
||||
self.nostr = decodedZapRequest
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Interface
|
||||
|
||||
@@ -61,7 +97,7 @@ extension WalletConnect {
|
||||
|
||||
/// Keys for the JSON inside the "params" object
|
||||
private enum ParamKeys: String, CodingKey {
|
||||
case invoice
|
||||
case invoice, description, metadata
|
||||
case from, until, limit, offset, unpaid, type
|
||||
}
|
||||
|
||||
@@ -82,7 +118,9 @@ extension WalletConnect {
|
||||
case Method.payInvoice.rawValue:
|
||||
let paramsContainer = try container.nestedContainer(keyedBy: ParamKeys.self, forKey: .params)
|
||||
let invoice = try paramsContainer.decode(String.self, forKey: .invoice)
|
||||
self = .payInvoice(invoice: invoice)
|
||||
let description: String? = try paramsContainer.decodeIfPresent(String.self, forKey: .description)
|
||||
let metadata: Metadata? = try paramsContainer.decodeIfPresent(Metadata.self, forKey: .metadata)
|
||||
self = .payInvoice(invoice: invoice, description: description, metadata: metadata)
|
||||
|
||||
case Method.getBalance.rawValue:
|
||||
// No params to decode
|
||||
@@ -112,10 +150,12 @@ extension WalletConnect {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
switch self {
|
||||
case .payInvoice(let invoice):
|
||||
case .payInvoice(let invoice, let description, let metadata):
|
||||
try container.encode(Method.payInvoice.rawValue, forKey: .method)
|
||||
var paramsContainer = container.nestedContainer(keyedBy: ParamKeys.self, forKey: .params)
|
||||
try paramsContainer.encode(invoice, forKey: .invoice)
|
||||
try paramsContainer.encodeIfPresent(description, forKey: .description)
|
||||
try paramsContainer.encodeIfPresent(metadata, forKey: .metadata)
|
||||
|
||||
case .getBalance:
|
||||
try container.encode(Method.getBalance.rawValue, forKey: .method)
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Daniel D’Aquino on 2025-03-10.
|
||||
//
|
||||
|
||||
import Combine
|
||||
|
||||
extension WalletConnect {
|
||||
/// Models a response from the NWC provider
|
||||
struct Response: Decodable {
|
||||
@@ -50,35 +52,80 @@ extension WalletConnect {
|
||||
let req_id: NoteId
|
||||
let response: Response
|
||||
|
||||
init?(from: NostrEvent, nwc: WalletConnect.ConnectURL) async {
|
||||
guard let note_id = from.referenced_ids.first else {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.req_id = note_id
|
||||
|
||||
let ares = Task {
|
||||
guard let json = decrypt_dm(nwc.keypair.privkey, pubkey: nwc.pubkey, content: from.content, encoding: .base64),
|
||||
let resp: WalletConnect.Response = decode_json(json)
|
||||
else {
|
||||
let resp: WalletConnect.Response? = nil
|
||||
return resp
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
init(from event: NostrEvent, nwc: WalletConnect.ConnectURL) async throws(InitializationError) {
|
||||
guard event.pubkey == nwc.pubkey else { throw .incorrectAuthorPubkey }
|
||||
|
||||
guard let res = await ares.value else {
|
||||
return nil
|
||||
guard let referencedNoteId = event.referenced_ids.first else { throw .missingRequestIdReference }
|
||||
|
||||
self.req_id = referencedNoteId
|
||||
|
||||
var json = ""
|
||||
do {
|
||||
json = try NIP04.decryptContent(
|
||||
recipientPrivateKey: nwc.keypair.privkey,
|
||||
senderPubkey: nwc.pubkey,
|
||||
content: event.content,
|
||||
encoding: .base64
|
||||
)
|
||||
}
|
||||
|
||||
self.response = res
|
||||
catch { throw .failedToDecrypt(error) }
|
||||
|
||||
do {
|
||||
let response: WalletConnect.Response = try decode_json_throwing(json)
|
||||
self.response = response
|
||||
}
|
||||
catch { throw .failedToDecodeJSON(error) }
|
||||
}
|
||||
|
||||
enum InitializationError: Error {
|
||||
case incorrectAuthorPubkey
|
||||
case missingRequestIdReference
|
||||
case failedToDecodeJSON(any Error)
|
||||
case failedToDecrypt(any Error)
|
||||
}
|
||||
}
|
||||
|
||||
struct WalletResponseErr: Codable {
|
||||
let code: String?
|
||||
let code: Code?
|
||||
let message: String?
|
||||
|
||||
enum Code: String, Codable {
|
||||
/// The client is sending commands too fast. It should retry in a few seconds.
|
||||
case rateLimited = "RATE_LIMITED"
|
||||
/// The command is not known or is intentionally not implemented.
|
||||
case notImplemented = "NOT_IMPLEMENTED"
|
||||
/// The wallet does not have enough funds to cover a fee reserve or the payment amount.
|
||||
case insufficientBalance = "INSUFFICIENT_BALANCE"
|
||||
/// The wallet has exceeded its spending quota.
|
||||
case quotaExceeded = "QUOTA_EXCEEDED"
|
||||
/// This public key is not allowed to do this operation.
|
||||
case restricted = "RESTRICTED"
|
||||
/// This public key has no wallet connected.
|
||||
case unauthorized = "UNAUTHORIZED"
|
||||
/// An internal error.
|
||||
case internalError = "INTERNAL"
|
||||
/// Other error.
|
||||
case other = "OTHER"
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case code, message
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
// Attempt to decode the code as a String
|
||||
if let codeString = try container.decodeIfPresent(String.self, forKey: .code),
|
||||
let validCode = Code(rawValue: codeString) {
|
||||
self.code = validCode
|
||||
} else {
|
||||
// If the code is either missing or not one of the allowed cases, set it to nil
|
||||
self.code = nil
|
||||
}
|
||||
|
||||
self.message = try container.decodeIfPresent(String.self, forKey: .message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ extension WalletConnect {
|
||||
static func subscribe(url: WalletConnectURL, pool: RelayPool) {
|
||||
var filter = NostrFilter(kinds: [.nwc_response])
|
||||
filter.authors = [url.pubkey]
|
||||
filter.pubkeys = [url.keypair.pubkey]
|
||||
filter.limit = 0
|
||||
let sub = NostrSubscribe(filters: [filter], sub_id: "nwc")
|
||||
|
||||
@@ -40,8 +41,9 @@ extension WalletConnect {
|
||||
/// - on_flush: A callback to call after the event has been flushed to the network
|
||||
/// - Returns: The Nostr Event that was sent to the network, representing the request that was made
|
||||
@discardableResult
|
||||
static func pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: String, delay: TimeInterval? = 5.0, on_flush: OnFlush? = nil) -> NostrEvent? {
|
||||
let req = WalletConnect.Request.payInvoice(invoice: invoice)
|
||||
static func pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: String, zap_request: NostrEvent?, delay: TimeInterval? = 5.0, on_flush: OnFlush? = nil) -> NostrEvent? {
|
||||
|
||||
let req = WalletConnect.Request.payZapRequest(invoice: invoice, zapRequest: zap_request)
|
||||
guard let ev = req.to_nostr_event(to_pk: url.pubkey, keypair: url.keypair) else {
|
||||
return nil
|
||||
}
|
||||
@@ -142,7 +144,7 @@ extension WalletConnect {
|
||||
}
|
||||
|
||||
print("damus-donation donating...")
|
||||
WalletConnect.pay(url: nwc, pool: pool, post: postbox, invoice: invoice, delay: nil)
|
||||
WalletConnect.pay(url: nwc, pool: pool, post: postbox, invoice: invoice, zap_request: nil, delay: nil)
|
||||
}
|
||||
|
||||
/// Handles a received Nostr Wallet Connect error
|
||||
|
||||
@@ -86,7 +86,7 @@ extension WalletConnect {
|
||||
let created_at: UInt64 // unixtimestamp, // invoice/payment creation time
|
||||
let expires_at: UInt64? // unixtimestamp, // invoice expiration time, optional if not applicable
|
||||
let settled_at: UInt64? // unixtimestamp, // invoice/payment settlement time, optional if unpaid
|
||||
//"metadata": {} // generic metadata that can be used to add things like zap/boostagram details for a payer name/comment/etc.
|
||||
let metadata: WalletConnect.Request.Metadata? // generic metadata that can be used to add things like zap/boostagram details for a payer name/comment/etc.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -270,7 +270,7 @@ struct EventActionBar: View {
|
||||
|
||||
generator.impactOccurred()
|
||||
|
||||
damus_state.postbox.send(like_ev)
|
||||
damus_state.nostrNetwork.postbox.send(like_ev)
|
||||
}
|
||||
|
||||
// MARK: Helper structures
|
||||
|
||||
@@ -25,7 +25,7 @@ struct RepostAction: View {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.postbox.send(boost)
|
||||
damus_state.nostrNetwork.postbox.send(boost)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Repost", comment: "Button to repost a note"), image: "repost")
|
||||
.frame(maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .leading)
|
||||
|
||||
@@ -15,6 +15,8 @@ struct AddRelayView: View {
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
typealias UpdateError = NostrNetworkManager.UserRelayListManager.UpdateError
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Add relay", comment: "Title text to indicate user to an add a relay.")
|
||||
@@ -82,38 +84,21 @@ struct AddRelayView: View {
|
||||
new_relay = "wss://" + new_relay
|
||||
}
|
||||
|
||||
guard let url = RelayURL(new_relay),
|
||||
let ev = state.contacts.event,
|
||||
let keypair = state.keypair.to_full() else {
|
||||
guard let url = RelayURL(new_relay) else {
|
||||
relayAddErrorTitle = NSLocalizedString("Invalid relay address", comment: "Heading for an error when adding a relay")
|
||||
relayAddErrorMessage = NSLocalizedString("Please check the address and try again", comment: "Tip for an error where the relay address being added is invalid")
|
||||
return
|
||||
}
|
||||
|
||||
let info = RelayInfo.rw
|
||||
let descriptor = RelayDescriptor(url: url, info: info)
|
||||
|
||||
do {
|
||||
try state.pool.add_relay(descriptor)
|
||||
try state.nostrNetwork.userRelayList.insert(relay: NIP65.RelayList.RelayItem(url: url, rwConfiguration: .readWrite))
|
||||
relayAddErrorTitle = nil // Clear error title
|
||||
relayAddErrorMessage = nil // Clear error message
|
||||
} catch RelayError.RelayAlreadyExists {
|
||||
relayAddErrorTitle = NSLocalizedString("Duplicate relay", comment: "Title of the duplicate relay error message.")
|
||||
relayAddErrorMessage = NSLocalizedString("The relay you are trying to add is already added.\nYou're all set!", comment: "An error message that appears when the user attempts to add a relay that has already been added.")
|
||||
return
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
catch {
|
||||
present_sheet(.error(self.humanReadableError(for: error)))
|
||||
}
|
||||
|
||||
state.pool.connect(to: [url])
|
||||
|
||||
if let new_ev = add_relay(ev: ev, keypair: keypair, current_relays: state.pool.our_descriptors, relay: url, info: info) {
|
||||
process_contact_event(state: state, ev: ev)
|
||||
|
||||
state.pool.send(.event(new_ev))
|
||||
}
|
||||
|
||||
if let relay_metadata = make_relay_metadata(relays: state.pool.our_descriptors, keypair: keypair) {
|
||||
state.postbox.send(relay_metadata)
|
||||
}
|
||||
new_relay = ""
|
||||
|
||||
this_app.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
|
||||
@@ -134,6 +119,17 @@ struct AddRelayView: View {
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
func humanReadableError(for error: any Error) -> ErrorView.UserPresentableError {
|
||||
guard let error = error as? UpdateError else {
|
||||
return .init(
|
||||
user_visible_description: NSLocalizedString("An unknown error occurred while adding a relay.", comment: "Title of an unknown relay error message."),
|
||||
tip: NSLocalizedString("Please contact support.", comment: "Tip for an unknown relay error message."),
|
||||
technical_info: error.localizedDescription
|
||||
)
|
||||
}
|
||||
return error.humanReadableError
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
||||
@@ -244,7 +244,7 @@ struct ChatEventView: View {
|
||||
let generator = UIImpactFeedbackGenerator(style: .medium)
|
||||
generator.impactOccurred()
|
||||
|
||||
damus_state.postbox.send(like_ev)
|
||||
damus_state.nostrNetwork.postbox.send(like_ev)
|
||||
}
|
||||
|
||||
var action_bar: some View {
|
||||
|
||||
@@ -161,7 +161,7 @@ struct ConfigView: View {
|
||||
}
|
||||
.navigationTitle(NSLocalizedString("Settings", comment: "Navigation title for Settings view."))
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.searchable(text: $searchText,prompt: "Search within settings")
|
||||
.searchable(text: $searchText, prompt: NSLocalizedString("Search within settings", comment: "Text to prompt the user to search settings."))
|
||||
.alert(NSLocalizedString("WARNING:\n\nTHIS WILL SIGN AN EVENT THAT DELETES THIS ACCOUNT.\n\nYOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.\n\n ARE YOU SURE YOU WANT TO CONTINUE?", comment: "Alert for deleting the users account."), isPresented: $delete_account_warning) {
|
||||
|
||||
Button(NSLocalizedString("Cancel", comment: "Cancel deleting the user."), role: .cancel) {
|
||||
@@ -182,7 +182,7 @@ struct ConfigView: View {
|
||||
let ev = created_deleted_account_profile(keypair: keypair) else {
|
||||
return
|
||||
}
|
||||
state.postbox.send(ev)
|
||||
state.nostrNetwork.postbox.send(ev)
|
||||
logout(state)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ struct DMChatView: View, KeyboardReadable {
|
||||
|
||||
dms.draft = ""
|
||||
|
||||
damus_state.postbox.send(dm)
|
||||
damus_state.nostrNetwork.postbox.send(dm)
|
||||
|
||||
handle_incoming_dm(ev: dm, our_pubkey: damus_state.pubkey, dms: damus_state.dms, prev_events: NewEventsBits())
|
||||
|
||||
|
||||
@@ -50,6 +50,10 @@ struct ErrorView: View {
|
||||
.cornerRadius(10)
|
||||
.padding(.vertical, 30)
|
||||
|
||||
if let technical_info = error.technical_info {
|
||||
ErrorTechInfoCopyButton(errorInfo: technical_info)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if let damus_state, damus_state.is_privkey_user {
|
||||
@@ -69,6 +73,39 @@ struct ErrorView: View {
|
||||
.padding(.top, 20)
|
||||
}
|
||||
|
||||
struct ErrorTechInfoCopyButton: View {
|
||||
let errorInfo: String
|
||||
@State var copied: Bool = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if !copied {
|
||||
Button(action: {
|
||||
UIPasteboard.general.string = errorInfo
|
||||
copied = true
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
copied = false
|
||||
}
|
||||
}, label: {
|
||||
HStack {
|
||||
Image(systemName: "square.on.square.dashed")
|
||||
Text("Copy technical information", comment: "Button label to allow user to copy technical information from an error screen (usually to provide our support team for further troubleshooting)")
|
||||
}
|
||||
})
|
||||
}
|
||||
else {
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle")
|
||||
Text("Copied!", comment: "Label indicating that the error technical information was successfully copied to the clipboard, which shows up as soon as the user clicks the copy button.")
|
||||
}
|
||||
.foregroundStyle(.damusGreen)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 20)
|
||||
}
|
||||
}
|
||||
|
||||
/// An error that is displayed to the user, and can be sent to the Developers as well.
|
||||
struct UserPresentableError {
|
||||
/// The description of the error to be shown to the user
|
||||
@@ -113,7 +150,7 @@ struct ErrorView: View {
|
||||
error: .init(
|
||||
user_visible_description: "We are still too early",
|
||||
tip: "Stay humble, keep building, stack sats",
|
||||
technical_info: nil
|
||||
technical_info: "UTXOs too small, must stack more sats"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,12 +24,12 @@ struct EventLoaderView<Content: View>: View {
|
||||
}
|
||||
|
||||
func unsubscribe() {
|
||||
damus_state.pool.unsubscribe(sub_id: subscription_uuid)
|
||||
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subscription_uuid)
|
||||
}
|
||||
|
||||
func subscribe(filters: [NostrFilter]) {
|
||||
damus_state.pool.register_handler(sub_id: subscription_uuid, handler: handle_event)
|
||||
damus_state.pool.send(.subscribe(.init(filters: filters, sub_id: subscription_uuid)))
|
||||
damus_state.nostrNetwork.pool.register_handler(sub_id: subscription_uuid, handler: handle_event)
|
||||
damus_state.nostrNetwork.pool.send(.subscribe(.init(filters: filters, sub_id: subscription_uuid)))
|
||||
}
|
||||
|
||||
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
||||
|
||||
@@ -113,7 +113,7 @@ struct MenuItems: View {
|
||||
if let full_keypair = self.damus_state.keypair.to_full(),
|
||||
let new_mutelist_ev = toggle_from_mutelist(keypair: full_keypair, prev: damus_state.mutelist_manager.event, to_toggle: .thread(event.thread_id(), duration?.date_from_now)) {
|
||||
damus_state.mutelist_manager.set_mutelist(new_mutelist_ev)
|
||||
damus_state.postbox.send(new_mutelist_ev)
|
||||
damus_state.nostrNetwork.postbox.send(new_mutelist_ev)
|
||||
}
|
||||
let muted = damus_state.mutelist_manager.is_event_muted(event)
|
||||
isMutedThread = muted
|
||||
|
||||
@@ -122,10 +122,7 @@ struct LongformPreviewBody: View {
|
||||
} else if blur_images || (blur_images && !state.settings.media_previews) {
|
||||
ZStack {
|
||||
titleImage(url: url)
|
||||
Blur()
|
||||
.onTapGesture {
|
||||
blur_images = false
|
||||
}
|
||||
BlurOverlayView(blur_images: $blur_images, artifacts: nil, size: nil, damus_state: nil, parentView: .longFormView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ class LoadableNostrEventViewModel: ObservableObject {
|
||||
case .zap, .zap_request:
|
||||
guard let zap = await get_zap(from: ev, state: damus_state) else { return .not_found }
|
||||
return .loaded(route: Route.Zaps(target: zap.target))
|
||||
case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .zap, .zap_request, .nwc_request, .nwc_response, .http_auth, .status:
|
||||
case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .nwc_request, .nwc_response, .http_auth, .status, .relay_list:
|
||||
return .unknown_or_unsupported_kind
|
||||
}
|
||||
case .naddr(let naddr):
|
||||
|
||||
@@ -87,7 +87,7 @@ struct AddMuteItemView: View {
|
||||
}
|
||||
|
||||
state.mutelist_manager.set_mutelist(mutelist)
|
||||
state.postbox.send(mutelist)
|
||||
state.nostrNetwork.postbox.send(mutelist)
|
||||
}
|
||||
|
||||
new_text = ""
|
||||
|
||||
@@ -30,7 +30,7 @@ struct MutelistView: View {
|
||||
}
|
||||
|
||||
damus_state.mutelist_manager.set_mutelist(new_ev)
|
||||
damus_state.postbox.send(new_ev)
|
||||
damus_state.nostrNetwork.postbox.send(new_ev)
|
||||
updateMuteItems()
|
||||
} label: {
|
||||
Label(NSLocalizedString("Delete", comment: "Button to remove a user from their mutelist."), image: "delete")
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// NIP05DomainPubkeysView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 5/23/25.
|
||||
//
|
||||
|
||||
import FaviconFinder
|
||||
import Kingfisher
|
||||
import SwiftUI
|
||||
|
||||
struct NIP05DomainPubkeysView: View {
|
||||
let damus_state: DamusState
|
||||
let domain: String
|
||||
let nip05_domain_favicon: FaviconURL?
|
||||
let pubkeys: [Pubkey]
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading) {
|
||||
ForEach(pubkeys, id: \.self) { pk in
|
||||
FollowUserView(target: .pubkey(pk), damus_state: damus_state)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
HStack {
|
||||
if let nip05_domain_favicon {
|
||||
KFImage(nip05_domain_favicon.source)
|
||||
.imageContext(.favicon, disable_animation: true)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 18, height: 18)
|
||||
.clipped()
|
||||
}
|
||||
Text(domain)
|
||||
.font(.headline)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let nip05_domain_favicon = FaviconURL(source: URL(string: "https://damus.io/favicon.ico")!, format: .ico, sourceType: .ico)
|
||||
let pubkeys = [test_pubkey, test_pubkey_2]
|
||||
NIP05DomainPubkeysView(damus_state: test_damus_state, domain: "damus.io", nip05_domain_favicon: nip05_domain_favicon, pubkeys: pubkeys)
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
//
|
||||
// NIP05DomainTimelineHeaderView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 5/16/25.
|
||||
//
|
||||
|
||||
import FaviconFinder
|
||||
import Kingfisher
|
||||
import SwiftUI
|
||||
|
||||
struct NIP05DomainTimelineHeaderView: View {
|
||||
let damus_state: DamusState
|
||||
@ObservedObject var model: NIP05DomainEventsModel
|
||||
let nip05_domain_favicon: FaviconURL?
|
||||
|
||||
@Environment(\.openURL) var openURL
|
||||
|
||||
var Icon: some View {
|
||||
ZStack {
|
||||
if let nip05_domain_favicon {
|
||||
KFImage(nip05_domain_favicon.source)
|
||||
.imageContext(.favicon, disable_animation: true)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 18, height: 18)
|
||||
.clipped()
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var friendsOfFriends: [Pubkey] {
|
||||
// Order it such that the pubkeys that have events come first in the array so that their profile pictures
|
||||
// show first.
|
||||
let pubkeys = model.events.all_events.map { $0.pubkey } + (model.filter.authors ?? [])
|
||||
|
||||
// Filter out duplicates but retain order, and filter out any that do not have a validated NIP-05.
|
||||
return (NSMutableOrderedSet(array: pubkeys).array as? [Pubkey] ?? [])
|
||||
.filter {
|
||||
damus_state.contacts.is_in_friendosphere($0) && damus_state.profiles.is_validated($0) != nil
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
if nip05_domain_favicon != nil {
|
||||
Icon
|
||||
}
|
||||
|
||||
Text(model.domain)
|
||||
.foregroundStyle(DamusLogoGradient.gradient)
|
||||
.font(.title.bold())
|
||||
.onTapGesture {
|
||||
if let url = URL(string: "https://\(model.domain)") {
|
||||
openURL(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let friendsOfFriends = friendsOfFriends
|
||||
|
||||
HStack {
|
||||
CondensedProfilePicturesView(state: damus_state, pubkeys: friendsOfFriends, maxPictures: 3)
|
||||
let friendsOfFriendsString = friendsOfFriendsString(friendsOfFriends, ndb: damus_state.ndb)
|
||||
Text(friendsOfFriendsString)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.gray)
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
.onTapGesture {
|
||||
if !friendsOfFriends.isEmpty {
|
||||
damus_state.nav.push(route: Route.NIP05DomainPubkeys(domain: model.domain, nip05_domain_favicon: nip05_domain_favicon, pubkeys: friendsOfFriends))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func friendsOfFriendsString(_ friendsOfFriends: [Pubkey], ndb: Ndb, locale: Locale = Locale.current) -> String {
|
||||
let bundle = bundleForLocale(locale: locale)
|
||||
let names: [String] = friendsOfFriends.prefix(3).map { pk in
|
||||
let profile = ndb.lookup_profile(pk)?.unsafeUnownedValue?.profile
|
||||
return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 20)
|
||||
}
|
||||
|
||||
switch friendsOfFriends.count {
|
||||
case 0:
|
||||
return "No one in your trusted network is associated with this domain."
|
||||
case 1:
|
||||
let format = NSLocalizedString("Notes from %@", bundle: bundle, comment: "Text to indicate that notes from one pubkey in our trusted network are shown below.")
|
||||
return String(format: format, locale: locale, names[0])
|
||||
case 2:
|
||||
let format = NSLocalizedString("Notes from %@ & %@", bundle: bundle, comment: "Text to indicate that notes from two pubkeys in our trusted network are shown below.")
|
||||
return String(format: format, locale: locale, names[0], names[1])
|
||||
case 3:
|
||||
let format = NSLocalizedString("Notes from %@, %@ & %@", bundle: bundle, comment: "Text to indicate that notes from three pubkeys in our trusted network are shown below.")
|
||||
return String(format: format, locale: locale, names[0], names[1], names[2])
|
||||
default:
|
||||
let format = localizedStringFormat(key: "notes_from_three_and_others", locale: locale)
|
||||
return String(format: format, locale: locale, friendsOfFriends.count - 3, names[0], names[1], names[2])
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let model = NIP05DomainEventsModel(state: test_damus_state, domain: "damus.io")
|
||||
let nip05_domain_favicon = FaviconURL(source: URL(string: "https://damus.io/favicon.ico")!, format: .ico, sourceType: .ico)
|
||||
NIP05DomainTimelineHeaderView(damus_state: test_damus_state, model: model, nip05_domain_favicon: nip05_domain_favicon)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// NIP05DomainTimelineView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 4/11/25.
|
||||
//
|
||||
|
||||
import FaviconFinder
|
||||
import Kingfisher
|
||||
import SwiftUI
|
||||
|
||||
struct NIP05DomainTimelineView: View {
|
||||
let damus_state: DamusState
|
||||
@ObservedObject var model: NIP05DomainEventsModel
|
||||
let nip05_domain_favicon: FaviconURL?
|
||||
|
||||
func nip05_filter(ev: NostrEvent) -> Bool {
|
||||
damus_state.contacts.is_in_friendosphere(ev.pubkey) && damus_state.profiles.is_validated(ev.pubkey) != nil
|
||||
}
|
||||
|
||||
var contentFilters: ContentFilters {
|
||||
var filters = Array<(NostrEvent) -> Bool>()
|
||||
filters.append(contentsOf: ContentFilters.defaults(damus_state: damus_state))
|
||||
filters.append(nip05_filter)
|
||||
return ContentFilters(filters: filters)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let height: CGFloat = 250.0
|
||||
|
||||
TimelineView(events: model.events, loading: $model.loading, damus: damus_state, show_friend_icon: true, filter: contentFilters.filter(ev:)) {
|
||||
ZStack(alignment: .leading) {
|
||||
DamusBackground(maxHeight: height)
|
||||
.mask(LinearGradient(gradient: Gradient(colors: [.black, .black, .black, .clear]), startPoint: .top, endPoint: .bottom))
|
||||
NIP05DomainTimelineHeaderView(damus_state: damus_state, model: model, nip05_domain_favicon: nip05_domain_favicon)
|
||||
.padding(.leading, 30)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.padding(.bottom, tabHeight)
|
||||
.onAppear {
|
||||
guard model.events.all_events.isEmpty else { return }
|
||||
|
||||
model.subscribe()
|
||||
|
||||
if let pubkeys = model.filter.authors {
|
||||
for pubkey in pubkeys {
|
||||
check_nip05_validity(pubkey: pubkey, profiles: damus_state.profiles)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
model.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let damus_state = test_damus_state
|
||||
let model = NIP05DomainEventsModel(state: damus_state, domain: "damus.io")
|
||||
let nip05_domain_favicon = FaviconURL(source: URL(string: "https://damus.io/favicon.ico")!, format: .ico, sourceType: .ico)
|
||||
NIP05DomainTimelineView(damus_state: test_damus_state, model: model, nip05_domain_favicon: nip05_domain_favicon)
|
||||
}
|
||||
@@ -23,6 +23,7 @@ struct Blur: UIViewRepresentable {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct NoteContentView: View {
|
||||
|
||||
let damus_state: DamusState
|
||||
@@ -166,10 +167,7 @@ struct NoteContentView: View {
|
||||
ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.media) { dismiss in
|
||||
fullscreen_preview(dismiss: dismiss)
|
||||
}
|
||||
Blur()
|
||||
.onTapGesture {
|
||||
blur_images = false
|
||||
}
|
||||
BlurOverlayView(blur_images: $blur_images, artifacts: artifacts, size: size, damus_state: damus_state, parentView: .noteContentView)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -384,6 +382,64 @@ func lookup_cached_preview_size(previews: PreviewCache, evid: NoteId) -> CGFloat
|
||||
return height
|
||||
}
|
||||
|
||||
struct BlurOverlayView: View {
|
||||
@Binding var blur_images: Bool
|
||||
let artifacts: NoteArtifactsSeparated?
|
||||
let size: EventViewKind?
|
||||
let damus_state: DamusState?
|
||||
let parentView: ParentViewType
|
||||
var body: some View {
|
||||
ZStack {
|
||||
|
||||
Color.black
|
||||
.opacity(0.54)
|
||||
|
||||
Blur()
|
||||
|
||||
VStack(alignment: .center) {
|
||||
Image(systemName: "eye.slash")
|
||||
.foregroundStyle(.white)
|
||||
.bold()
|
||||
.padding(EdgeInsets(top: 5, leading: 10, bottom: 0, trailing: 10))
|
||||
Text(NSLocalizedString("Media from someone you don't follow", comment: "Label on the image blur mask"))
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(Color.white)
|
||||
.font(.title2)
|
||||
.padding(EdgeInsets(top: 5, leading: 10, bottom: 0, trailing: 10))
|
||||
Button(NSLocalizedString("Tap to load", comment: "Label for button that allows user to dismiss media content warning and unblur the image")) {
|
||||
blur_images = false
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.fontWeight(.bold)
|
||||
.foregroundStyle(.white)
|
||||
.padding(EdgeInsets(top: 5, leading: 10, bottom: 0, trailing: 10))
|
||||
|
||||
if parentView == .noteContentView,
|
||||
let artifacts = artifacts,
|
||||
let size = size,
|
||||
let damus_state = damus_state
|
||||
{
|
||||
switch artifacts.media[0] {
|
||||
case .image(let url), .video(let url):
|
||||
Text(abbreviateURL(url, maxLength: 30))
|
||||
.font(eventviewsize_to_font(size, font_size: damus_state.settings.font_size * 0.8))
|
||||
.foregroundStyle(.white)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(EdgeInsets(top: 20, leading: 10, bottom: 5, trailing: 10))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
blur_images = false
|
||||
}
|
||||
}
|
||||
|
||||
enum ParentViewType {
|
||||
case noteContentView, longFormView
|
||||
}
|
||||
}
|
||||
|
||||
struct NoteContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let state = test_damus_state
|
||||
@@ -401,7 +457,7 @@ struct NoteContentView_Previews: PreviewProvider {
|
||||
.previewDisplayName("Super short note")
|
||||
|
||||
VStack {
|
||||
NoteContentView(damus_state: state, event: test_encoded_note_with_image!, blur_images: false, size: .normal, options: [])
|
||||
NoteContentView(damus_state: state, event: test_encoded_note_with_image!, blur_images: true, size: .normal, options: [])
|
||||
}
|
||||
.previewDisplayName("Note with image")
|
||||
|
||||
@@ -434,4 +490,3 @@ func separate_images(ev: NostrEvent, keypair: Keypair) -> [MediaUrl]? {
|
||||
let mediaUrls = urlBlocks.map { MediaUrl.image($0) }
|
||||
return mediaUrls.isEmpty ? nil : mediaUrls
|
||||
}
|
||||
|
||||
|
||||
@@ -86,10 +86,10 @@ struct DamusAppNotificationView: View {
|
||||
Task {
|
||||
do {
|
||||
let url = try await damus_state.purple.generate_verified_ln_checkout_link(product_template_name: product_template_name)
|
||||
await self.open_url(url: url)
|
||||
self.open_url(url: url)
|
||||
}
|
||||
catch {
|
||||
await self.open_url(url: damus_state.purple.environment.purple_landing_page_url().appendingPathComponent("checkout"))
|
||||
self.open_url(url: damus_state.purple.environment.purple_landing_page_url().appendingPathComponent("checkout"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,10 @@ class NotificationFilter: ObservableObject, Equatable {
|
||||
|
||||
if let item = item.filter({ ev in
|
||||
self.friend_filter.filter(contacts: contacts, pubkey: ev.pubkey) &&
|
||||
(!hellthread_notifications_disabled || !ev.is_hellthread(max_pubkeys: hellthread_notification_max_pubkeys))
|
||||
(!hellthread_notifications_disabled || !ev.is_hellthread(max_pubkeys: hellthread_notification_max_pubkeys)) &&
|
||||
// Allow notes that are created no more than 3 seconds in the future
|
||||
// to account for natural clock skew between sender and receiver.
|
||||
ev.age >= -3
|
||||
}) {
|
||||
acc.append(item)
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ class SuggestedUsersViewModel: ObservableObject {
|
||||
|
||||
private func subscribeToSuggestedProfiles(pubkeys: [Pubkey]) {
|
||||
let filter = NostrFilter(kinds: [.metadata], authors: pubkeys)
|
||||
damus_state.pool.subscribe(sub_id: sub_id, filters: [filter], handler: handle_event)
|
||||
damus_state.nostrNetwork.pool.subscribe(sub_id: sub_id, filters: [filter], handler: handle_event)
|
||||
}
|
||||
|
||||
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
||||
|
||||
+34
-10
@@ -79,6 +79,7 @@ struct PostView: View {
|
||||
var autoSaveModel: AutoSaveIndicatorView.AutoSaveViewModel
|
||||
|
||||
@State var preUploadedMedia: [PreUploadedMedia] = []
|
||||
@State var mediaUploadUnderProgress: MediaUpload? = nil
|
||||
|
||||
@StateObject var image_upload: ImageUploadModel = ImageUploadModel()
|
||||
@StateObject var tagModel: TagModel = TagModel()
|
||||
@@ -330,11 +331,6 @@ struct PostView: View {
|
||||
PostButton
|
||||
}
|
||||
|
||||
if let progress = image_upload.progress {
|
||||
ProgressView(value: progress, total: 1.0)
|
||||
.progressViewStyle(.linear)
|
||||
}
|
||||
|
||||
Divider()
|
||||
.foregroundColor(DamusColors.neutral3)
|
||||
.padding(.top, 5)
|
||||
@@ -346,6 +342,7 @@ struct PostView: View {
|
||||
|
||||
@discardableResult
|
||||
func handle_upload(media: MediaUpload) async -> Bool {
|
||||
mediaUploadUnderProgress = media
|
||||
let uploader = damus_state.settings.default_media_uploader
|
||||
|
||||
let img = getImage(media: media)
|
||||
@@ -354,6 +351,7 @@ struct PostView: View {
|
||||
async let blurhash = calculate_blurhash(img: img)
|
||||
let res = await image_upload.start(media: media, uploader: uploader, mediaType: .normal, keypair: damus_state.keypair)
|
||||
|
||||
mediaUploadUnderProgress = nil
|
||||
switch res {
|
||||
case .success(let url):
|
||||
guard let url = URL(string: url) else {
|
||||
@@ -401,10 +399,13 @@ struct PostView: View {
|
||||
}
|
||||
.id("post")
|
||||
|
||||
PVImageCarouselView(media: $uploadedMedias, deviceWidth: deviceSize.size.width)
|
||||
.onChange(of: uploadedMedias) { media in
|
||||
post_changed(post: post, media: media)
|
||||
}
|
||||
PVImageCarouselView(media: $uploadedMedias,
|
||||
mediaUnderProgress: $mediaUploadUnderProgress,
|
||||
imageUploadModel: image_upload,
|
||||
deviceWidth: deviceSize.size.width)
|
||||
.onChange(of: uploadedMedias) { media in
|
||||
post_changed(post: post, media: media)
|
||||
}
|
||||
|
||||
if case .quoting(let ev) = action {
|
||||
BuilderEventView(damus: damus_state, event: ev)
|
||||
@@ -620,6 +621,8 @@ struct PostView_Previews: PreviewProvider {
|
||||
|
||||
struct PVImageCarouselView: View {
|
||||
@Binding var media: [UploadedMedia]
|
||||
@Binding var mediaUnderProgress: MediaUpload?
|
||||
@ObservedObject var imageUploadModel: ImageUploadModel
|
||||
|
||||
let deviceWidth: CGFloat
|
||||
|
||||
@@ -667,6 +670,25 @@ struct PVImageCarouselView: View {
|
||||
.padding(.bottom, 35)
|
||||
}
|
||||
}
|
||||
if let mediaUP = mediaUnderProgress, let progress = imageUploadModel.progress {
|
||||
ZStack {
|
||||
// Media under upload-progress
|
||||
Image(uiImage: getImage(media: mediaUP))
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: media.count == 0 ? deviceWidth * 0.8 : 250, height: media.count == 0 ? 400 : 250)
|
||||
.cornerRadius(10)
|
||||
.opacity(0.3)
|
||||
.padding()
|
||||
// Circle showing progress on top of media
|
||||
Circle()
|
||||
.trim(from: 0, to: CGFloat(progress))
|
||||
.stroke(Color.damusPurple, lineWidth: 5.0)
|
||||
.rotationEffect(.degrees(-90))
|
||||
.frame(width: 30, height: 30)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
@@ -763,7 +785,7 @@ func load_draft_for_post(drafts: Drafts, action: PostAction) -> DraftArtifacts?
|
||||
}
|
||||
// If there are no exact matches to the highlight, try to load a draft for the same highlight source
|
||||
// We do this to improve UX, because we don't want to leave the post view blank if they only selected a slightly different piece of text from before.
|
||||
var other_matches = drafts.highlights
|
||||
let other_matches = drafts.highlights
|
||||
.filter { $0.key.source == highlight.source }
|
||||
// It's not an exact match, so there is no way of telling which one is the preferred draft. So just load the first one we found.
|
||||
return other_matches.first?.value
|
||||
@@ -883,6 +905,8 @@ func build_post(state: DamusState, post: NSAttributedString, action: PostAction,
|
||||
case .quoting(let ev):
|
||||
content.append("\n\nnostr:" + bech32_note_id(ev.id))
|
||||
|
||||
tags.append(["q", ev.id.hex()]);
|
||||
|
||||
if let quoted_ev = state.events.lookup(ev.id) {
|
||||
tags.append(["p", quoted_ev.pubkey.hex()])
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ struct EditMetadataView: View {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.postbox.send(metadata_ev)
|
||||
damus_state.nostrNetwork.postbox.send(metadata_ev)
|
||||
}
|
||||
|
||||
func is_ln_valid(ln: String) -> Bool {
|
||||
|
||||
@@ -287,7 +287,7 @@ struct EditPictureControl: View {
|
||||
|
||||
var accessibility_value: String? {
|
||||
if style.first_time_setup {
|
||||
if let current_image_url = model.current_image_url {
|
||||
if model.current_image_url != nil {
|
||||
switch self.model.context {
|
||||
case .normal:
|
||||
return NSLocalizedString("Image is setup", comment: "Accessibility value on image control")
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// Created by William Casarin on 2022-04-16.
|
||||
//
|
||||
|
||||
import FaviconFinder
|
||||
import SwiftUI
|
||||
|
||||
enum FriendType {
|
||||
@@ -43,6 +44,7 @@ struct ProfileName: View {
|
||||
@State var nip05: NIP05?
|
||||
@State var donation: Int?
|
||||
@State var purple_account: DamusPurple.Account?
|
||||
@State var nip05_domain_favicon: FaviconURL?
|
||||
|
||||
init(pubkey: Pubkey, prefix: String = "", damus: DamusState, show_nip5_domain: Bool = true, supporterBadgeStyle: SupporterBadge.Style = .compact) {
|
||||
self.pubkey = pubkey
|
||||
@@ -61,13 +63,15 @@ struct ProfileName: View {
|
||||
var current_nip05: NIP05? {
|
||||
nip05 ?? damus_state.profiles.is_validated(pubkey)
|
||||
}
|
||||
|
||||
|
||||
func current_display_name(profile: Profile?) -> DisplayName {
|
||||
return display_name ?? Profile.displayName(profile: profile, pubkey: pubkey)
|
||||
}
|
||||
|
||||
func name_choice(profile: Profile?) -> String {
|
||||
return prefix == "@" ? current_display_name(profile: profile).username.truncate(maxLength: 50) : current_display_name(profile: profile).displayName.truncate(maxLength: 50)
|
||||
let displayName = current_display_name(profile: profile)
|
||||
let untruncatedName = prefix == "@" ? displayName.username : displayName.displayName
|
||||
return untruncatedName.truncate(maxLength: 50)
|
||||
}
|
||||
|
||||
func onlyzapper(profile: Profile?) -> Bool {
|
||||
@@ -99,7 +103,7 @@ struct ProfileName: View {
|
||||
.fontWeight(prefix == "@" ? .none : .bold)
|
||||
|
||||
if let nip05 = current_nip05 {
|
||||
NIP05Badge(nip05: nip05, pubkey: pubkey, contacts: damus_state.contacts, show_domain: show_nip5_domain, profiles: damus_state.profiles)
|
||||
NIP05Badge(nip05: nip05, pubkey: pubkey, damus_state: damus_state, show_domain: show_nip5_domain, nip05_domain_favicon: nip05_domain_favicon)
|
||||
}
|
||||
|
||||
if let friend = friend_type, current_nip05 == nil {
|
||||
@@ -116,9 +120,15 @@ struct ProfileName: View {
|
||||
|
||||
}
|
||||
.task {
|
||||
if damus_state.purple.enable_purple {
|
||||
self.purple_account = try? await damus_state.purple.get_maybe_cached_account(pubkey: pubkey)
|
||||
}
|
||||
if damus_state.purple.enable_purple {
|
||||
self.purple_account = try? await damus_state.purple.get_maybe_cached_account(pubkey: pubkey)
|
||||
}
|
||||
}
|
||||
.task {
|
||||
if let domain = current_nip05?.host {
|
||||
self.nip05_domain_favicon = try? await damus_state.favicon_cache.lookup(domain)
|
||||
.largest()
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.profile_updated)) { update in
|
||||
if update.pubkey != pubkey {
|
||||
@@ -149,6 +159,24 @@ struct ProfileName: View {
|
||||
let nip05 = damus_state.profiles.is_validated(pubkey)
|
||||
if nip05 != self.nip05 {
|
||||
self.nip05 = nip05
|
||||
|
||||
if let domain = nip05?.host {
|
||||
Task {
|
||||
let favicon = try? await damus_state.favicon_cache.lookup(domain)
|
||||
.filter {
|
||||
if let size = $0.size {
|
||||
return size.width <= 128 && size.height <= 128
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
.largest()
|
||||
|
||||
await MainActor.run {
|
||||
self.nip05_domain_favicon = favicon
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if donation != profile.damus_donation {
|
||||
|
||||
@@ -219,7 +219,7 @@ struct ProfileView: View {
|
||||
}
|
||||
|
||||
damus_state.mutelist_manager.set_mutelist(new_ev)
|
||||
damus_state.postbox.send(new_ev)
|
||||
damus_state.nostrNetwork.postbox.send(new_ev)
|
||||
}
|
||||
} else {
|
||||
Button(NSLocalizedString("Mute", comment: "Button to mute a profile"), role: .destructive) {
|
||||
@@ -396,18 +396,18 @@ struct ProfileView: View {
|
||||
}
|
||||
}
|
||||
|
||||
if let relays = profile.relays {
|
||||
if let relays = profile.relay_urls {
|
||||
// Only open relay config view if the user is logged in with private key and they are looking at their own profile.
|
||||
let noun_string = pluralizedString(key: "relays_count", count: relays.keys.count)
|
||||
let noun_string = pluralizedString(key: "relays_count", count: relays.count)
|
||||
let noun_text = Text(noun_string).font(.subheadline).foregroundColor(.gray)
|
||||
let relay_text = Text("\(Text(verbatim: relays.keys.count.formatted()).font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.")
|
||||
let relay_text = Text("\(Text(verbatim: relays.count.formatted()).font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.")
|
||||
if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user {
|
||||
NavigationLink(value: Route.RelayConfig) {
|
||||
relay_text
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
} else {
|
||||
NavigationLink(value: Route.UserRelays(relays: Array(relays.keys).sorted())) {
|
||||
NavigationLink(value: Route.UserRelays(relays: relays.sorted())) {
|
||||
relay_text
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
|
||||
@@ -297,7 +297,7 @@ fileprivate struct ProfileActionSheetZapButton: View {
|
||||
.foregroundColor(Color.primary)
|
||||
.profile_button_style(scheme: colorScheme)
|
||||
case .zap_success:
|
||||
Image("checkmark")
|
||||
Image("checkmark-damus")
|
||||
.foregroundColor(Color.green)
|
||||
.profile_button_style(scheme: colorScheme)
|
||||
case .zap_failure:
|
||||
|
||||
@@ -46,7 +46,7 @@ struct PubkeyView: View {
|
||||
let bech32 = pubkey.npub
|
||||
|
||||
HStack {
|
||||
Text(verbatim: "\(abbrev_pubkey(bech32, amount: sidemenu ? 12 : 16))")
|
||||
Text(verbatim: "\(abbrev_identifier(bech32, amount: sidemenu ? 12 : 16))")
|
||||
.font(sidemenu ? .system(size: 10) : .footnote)
|
||||
.foregroundColor(keyColor())
|
||||
.padding(5)
|
||||
|
||||
@@ -57,11 +57,14 @@ struct QRScanNSECView: View {
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
@State var showQR = true
|
||||
@State var privKeyFound = false
|
||||
@State var shouldSaveKey = true
|
||||
return QRScanNSECView(showQR: $showQR,
|
||||
privKeyFound: $privKeyFound,
|
||||
codeScannerCompletion: { _ in })
|
||||
struct QRScanNSECView_Previews: PreviewProvider {
|
||||
@State static var showQR = true
|
||||
@State static var privKeyFound = false
|
||||
@State static var shouldSaveKey = true
|
||||
|
||||
static var previews: some View {
|
||||
QRScanNSECView(showQR: $showQR,
|
||||
privKeyFound: $privKeyFound,
|
||||
codeScannerCompletion: { _ in })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,11 @@ struct RelayFilterView: View {
|
||||
self.state = state
|
||||
self.timeline = timeline
|
||||
|
||||
//_relays = State(initialValue: state.pool.descriptors)
|
||||
//_relays = State(initialValue: state.networkManager.pool.descriptors)
|
||||
}
|
||||
|
||||
var relays: [RelayDescriptor] {
|
||||
return state.pool.our_descriptors
|
||||
var relays: [RelayPool.RelayDescriptor] {
|
||||
return state.nostrNetwork.pool.our_descriptors
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
@@ -23,7 +23,7 @@ enum RelayTab: Int, CaseIterable{
|
||||
|
||||
struct RelayConfigView: View {
|
||||
let state: DamusState
|
||||
@State var relays: [RelayDescriptor]
|
||||
@State var relays: [RelayPool.RelayDescriptor]
|
||||
@State private var showActionButtons = false
|
||||
@State var show_add_relay: Bool = false
|
||||
@State var selectedTab = 0
|
||||
@@ -32,15 +32,15 @@ struct RelayConfigView: View {
|
||||
|
||||
init(state: DamusState) {
|
||||
self.state = state
|
||||
_relays = State(initialValue: state.pool.our_descriptors)
|
||||
_relays = State(initialValue: state.nostrNetwork.pool.our_descriptors)
|
||||
UITabBar.appearance().isHidden = true
|
||||
}
|
||||
|
||||
var recommended: [RelayDescriptor] {
|
||||
let rs: [RelayDescriptor] = []
|
||||
var recommended: [RelayPool.RelayDescriptor] {
|
||||
let rs: [RelayPool.RelayDescriptor] = []
|
||||
let recommended_relay_addresses = get_default_bootstrap_relays()
|
||||
return recommended_relay_addresses.reduce(into: rs) { xs, x in
|
||||
xs.append(RelayDescriptor(url: x, info: .rw))
|
||||
xs.append(RelayPool.RelayDescriptor(url: x, info: .readWrite))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ struct RelayConfigView: View {
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.relays_changed)) { _ in
|
||||
self.relays = state.pool.our_descriptors
|
||||
self.relays = state.nostrNetwork.pool.our_descriptors
|
||||
}
|
||||
.onAppear {
|
||||
notify(.display_tabbar(false))
|
||||
@@ -109,7 +109,7 @@ struct RelayConfigView: View {
|
||||
.ignoresSafeArea(.all)
|
||||
}
|
||||
|
||||
func RelayList(title: String, relayList: [RelayDescriptor], recommended: Bool) -> some View {
|
||||
func RelayList(title: String, relayList: [RelayPool.RelayDescriptor], recommended: Bool) -> some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
HStack {
|
||||
Text(title)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user