From 88f938d11c6141b0cb318c89987c3d64c10e4be6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20D=E2=80=99Aquino?= Date: Fri, 1 Dec 2023 21:26:06 +0000 Subject: [PATCH] Bring local notification logic into the push notification target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit brings key local notification logic into the notification extension target to allow the extension to reuse much of the functionality surrounding the processing and formatting of notifications. More specifically, the functions `process_local_notification` and `create_local_notification` were brought into the extension target. This will enable us to reuse much of the pre-existing notification logic (and avoid having to reimplement all of that) However, those functions had high dependencies on other parts of the code, so significant refactorings were needed to make this happen: - `create_local_notification` and `process_local_notification` had its function signatures changed to avoid the need to `DamusState` (which pulls too many other dependecies) - Other necessary dependencies, such as `Profiles`, `UserSettingsStore` had to be pulled into the extension target. Subsequently, sub-dependencies of those items had to be pulled in as well - In several cases, files were split to avoid pulling too many dependencies (e.g. Some Model files depended on some functions in View files, so in those cases I moved those functions into their own separate file to avoid pulling in view logic into the extension target) - Notification processing logic was changed a bit to remove dependency on `EventCache` in favor of using ndb directly (As instructed in a TODO comment in EventCache, and because EventCache has too many other dependencies) tldr: A LOT of things were moved around, a bit of logic was changed around local notifications to avoid using `EventCache`, but otherwise this commit is meant to be a no-op without any new features or user-facing functional changes. Testing ------- Device: iPhone 15 Pro iOS: 17.0.1 Damus: This commit Coverage: 1. Ran unit tests to check for regressions (none detected) 2. Launched the app and navigated around and did some interactions to perform a quick functional smoke test (no regressions found) 3. Sent a few push notifications to check they still work as expected (PASS) Signed-off-by: Daniel D’Aquino Signed-off-by: William Casarin --- damus.xcodeproj/project.pbxproj | 132 +++++++ .../DamusNotificationService.xcscheme | 2 +- damus/Models/Contacts+.swift | 153 ++++++++ damus/Models/Contacts.swift | 142 ------- damus/Models/FollowState.swift | 15 + damus/Models/FriendFilter.swift | 34 ++ damus/Models/HomeModel.swift | 138 +------ damus/Models/LongformEvent.swift | 36 ++ damus/Models/MediaUploader.swift | 117 ++++++ damus/Models/NewEventsBits.swift | 23 ++ damus/Models/NoteContent.swift | 351 +++++++++++++++++ damus/Models/NotificationsManager.swift | 125 ++++++ damus/Models/UserSettingsStore.swift | 1 + damus/Models/ZapType.swift | 28 ++ damus/Util/CollectionExtension.swift | 15 + damus/Util/CompatibleAttribute.swift | 10 + damus/Util/Constants.swift | 8 +- damus/Util/LNUrls.swift | 33 ++ damus/Util/SequenceUtils.swift | 23 ++ damus/Util/Zap.swift | 33 -- damus/Views/AttachMediaUtility.swift | 109 ------ damus/Views/EventDetailView.swift | 8 - .../Views/Events/Longform/LongformView.swift | 28 -- damus/Views/NoteContentView.swift | 358 +----------------- .../Views/Notifications/EventGroupView.swift | 6 - .../Notifications/NotificationsView.swift | 26 -- damus/Views/Profile/ProfileView.swift | 7 - .../Settings/ReactionsSettingsView.swift | 2 - damus/Views/Zaps/ZapTypePicker.swift | 20 - nostrdb/NdbNote+.swift | 8 +- nostrdb/NdbNote.swift | 10 + 31 files changed, 1129 insertions(+), 872 deletions(-) create mode 100644 damus/Models/Contacts+.swift create mode 100644 damus/Models/FollowState.swift create mode 100644 damus/Models/FriendFilter.swift create mode 100644 damus/Models/LongformEvent.swift create mode 100644 damus/Models/MediaUploader.swift create mode 100644 damus/Models/NewEventsBits.swift create mode 100644 damus/Models/NoteContent.swift create mode 100644 damus/Models/NotificationsManager.swift create mode 100644 damus/Models/ZapType.swift create mode 100644 damus/Util/CollectionExtension.swift create mode 100644 damus/Util/SequenceUtils.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 4c602e91..ca3aed98 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -479,6 +479,33 @@ D7C6787E2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C6787D2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift */; }; D7CB5D3B2B112FBB00AD4105 /* NotificationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70A3B162B02DCE5008BD568 /* NotificationFormatter.swift */; }; D7CB5D3C2B1130C600AD4105 /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128B29EB19C40006FA5A /* LocalNotification.swift */; }; + D7CB5D3E2B116DAD00AD4105 /* NotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CB5D3D2B116DAD00AD4105 /* NotificationsManager.swift */; }; + D7CB5D3F2B116DAD00AD4105 /* NotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CB5D3D2B116DAD00AD4105 /* NotificationsManager.swift */; }; + D7CB5D402B116E8A00AD4105 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; }; + D7CB5D412B116F0900AD4105 /* StringCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA5588229F33F5B00DC6A45 /* StringCodable.swift */; }; + D7CB5D422B116F8900AD4105 /* Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3AC79A28306D7B00E1F516 /* Contacts.swift */; }; + D7CB5D432B116F9B00AD4105 /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; }; + D7CB5D452B116FE800AD4105 /* Contacts+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CB5D442B116FE800AD4105 /* Contacts+.swift */; }; + D7CB5D462B11703D00AD4105 /* Notify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA3529F2A76AE80003BB08B /* Notify.swift */; }; + D7CB5D472B11718700AD4105 /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; }; + D7CB5D482B11719300AD4105 /* Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9DB280C38C000D9BBE8 /* Profiles.swift */; }; + D7CB5D4B2B11721600AD4105 /* ZapType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CB5D4A2B11721600AD4105 /* ZapType.swift */; }; + D7CB5D4C2B11721600AD4105 /* ZapType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CB5D4A2B11721600AD4105 /* ZapType.swift */; }; + D7CB5D4E2B11728000AD4105 /* NewEventsBits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CB5D4D2B11728000AD4105 /* NewEventsBits.swift */; }; + D7CB5D4F2B11728000AD4105 /* NewEventsBits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CB5D4D2B11728000AD4105 /* NewEventsBits.swift */; }; + D7CB5D512B1174D100AD4105 /* FriendFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CB5D502B1174D100AD4105 /* FriendFilter.swift */; }; + D7CB5D522B1174D100AD4105 /* FriendFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CB5D502B1174D100AD4105 /* FriendFilter.swift */; }; + D7CB5D532B1174E900AD4105 /* DeepLPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */; }; + D7CB5D542B1174F700AD4105 /* NIP05.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8838529656C8B00DC99E7 /* NIP05.swift */; }; + D7CB5D552B11758A00AD4105 /* UnmuteThreadNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4E137C2A76D63600BDD832 /* UnmuteThreadNotify.swift */; }; + D7CB5D562B11759900AD4105 /* MuteThreadNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C4E137A2A76D5FB00BDD832 /* MuteThreadNotify.swift */; }; + D7CB5D572B11762900AD4105 /* UserStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5E54022A9522F600FF6E60 /* UserStatus.swift */; }; + D7CB5D582B11763C00AD4105 /* NewMutesNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA352A72A76B37E003BB08B /* NewMutesNotify.swift */; }; + D7CB5D592B11764000AD4105 /* NewUnmutesNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA352AB2A76C07F003BB08B /* NewUnmutesNotify.swift */; }; + D7CB5D5C2B1176B200AD4105 /* MediaUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CB5D5B2B1176B200AD4105 /* MediaUploader.swift */; }; + D7CB5D5D2B1176B200AD4105 /* MediaUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CB5D5B2B1176B200AD4105 /* MediaUploader.swift */; }; + D7CB5D5F2B11770C00AD4105 /* FollowState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CB5D5E2B11770C00AD4105 /* FollowState.swift */; }; + D7CB5D602B11770C00AD4105 /* FollowState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CB5D5E2B11770C00AD4105 /* FollowState.swift */; }; D7CCFC072B05833200323D86 /* NdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90548A2A6AEDEE00811EEC /* NdbNote.swift */; }; D7CCFC082B05834500323D86 /* NoteId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC14FF42A740BB7007AEB17 /* NoteId.swift */; }; D7CCFC0B2B0585EA00323D86 /* nostrdb.c in Sources */ = {isa = PBXBuildFile; fileRef = 4CE9FBB82A6B3B26007E485C /* nostrdb.c */; settings = {COMPILER_FLAGS = "-w"; }; }; @@ -543,6 +570,26 @@ D7CE1B492B0BE729002EDAD4 /* DisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */; }; D7DBD41F2B02F15E002A6197 /* NostrKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */; }; D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */; }; + D7EDED152B11776B0018B19C /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; }; + D7EDED162B1177840018B19C /* LNUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883B5297730E400DC99E7 /* LNUrls.swift */; }; + D7EDED172B1177960018B19C /* TranslationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95C9298DF87B00F3D526 /* TranslationService.swift */; }; + D7EDED182B1177A00018B19C /* LNUrlPayRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883A52975F83C00DC99E7 /* LNUrlPayRequest.swift */; }; + D7EDED1C2B1178FE0018B19C /* NoteContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EDED1B2B1178FE0018B19C /* NoteContent.swift */; }; + D7EDED1E2B11797D0018B19C /* LongformEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EDED1D2B11797D0018B19C /* LongformEvent.swift */; }; + D7EDED1F2B11797D0018B19C /* LongformEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EDED1D2B11797D0018B19C /* LongformEvent.swift */; }; + D7EDED212B117DCA0018B19C /* SequenceUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EDED202B117DCA0018B19C /* SequenceUtils.swift */; }; + D7EDED222B117DCA0018B19C /* SequenceUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EDED202B117DCA0018B19C /* SequenceUtils.swift */; }; + D7EDED232B117DFB0018B19C /* NoteContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EDED1B2B1178FE0018B19C /* NoteContent.swift */; }; + D7EDED262B117FC80018B19C /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; }; + D7EDED272B117FF10018B19C /* CompatibleAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */; }; + D7EDED282B1180940018B19C /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; }; + D7EDED292B1182060018B19C /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; }; + D7EDED2A2B128CB40018B19C /* Nip98HTTPAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCD1E692A874C4E0099A953 /* Nip98HTTPAuth.swift */; }; + D7EDED2B2B128CDB0018B19C /* Hashtags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */; }; + D7EDED2C2B128CFA0018B19C /* DamusColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */; }; + D7EDED2E2B128E8A0018B19C /* CollectionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EDED2D2B128E8A0018B19C /* CollectionExtension.swift */; }; + D7EDED2F2B128E8A0018B19C /* CollectionExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EDED2D2B128E8A0018B19C /* CollectionExtension.swift */; }; + D7EDED312B1290B80018B19C /* MarkdownUI in Frameworks */ = {isa = PBXBuildFile; productRef = D7EDED302B1290B80018B19C /* MarkdownUI */; }; D7FB10A72B0C371A00FA8D42 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2B10272A7B0F5C008AA43E /* Log.swift */; }; D7FF94002AC7AC5300FD969D /* RelayURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */; }; E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */; }; @@ -1284,7 +1331,18 @@ D79C4C182AFEB061003A41B4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; D79C4C1C2AFEB061003A41B4 /* DamusNotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DamusNotificationService.entitlements; sourceTree = ""; }; D7C6787D2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP98AuthenticatedRequest.swift; sourceTree = ""; }; + D7CB5D3D2B116DAD00AD4105 /* NotificationsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsManager.swift; sourceTree = ""; }; + D7CB5D442B116FE800AD4105 /* Contacts+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Contacts+.swift"; sourceTree = ""; }; + D7CB5D4A2B11721600AD4105 /* ZapType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapType.swift; sourceTree = ""; }; + D7CB5D4D2B11728000AD4105 /* NewEventsBits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewEventsBits.swift; sourceTree = ""; }; + D7CB5D502B1174D100AD4105 /* FriendFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FriendFilter.swift; sourceTree = ""; }; + D7CB5D5B2B1176B200AD4105 /* MediaUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploader.swift; sourceTree = ""; }; + D7CB5D5E2B11770C00AD4105 /* FollowState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowState.swift; sourceTree = ""; }; D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = ""; }; + D7EDED1B2B1178FE0018B19C /* NoteContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContent.swift; sourceTree = ""; }; + D7EDED1D2B11797D0018B19C /* LongformEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongformEvent.swift; sourceTree = ""; }; + D7EDED202B117DCA0018B19C /* SequenceUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SequenceUtils.swift; sourceTree = ""; }; + D7EDED2D2B128E8A0018B19C /* CollectionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtension.swift; sourceTree = ""; }; D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayURL.swift; sourceTree = ""; }; E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = ""; }; E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = ""; }; @@ -1338,6 +1396,7 @@ buildActionMask = 2147483647; files = ( D789D1202AFEFBF20083A7AB /* secp256k1 in Frameworks */, + D7EDED312B1290B80018B19C /* MarkdownUI in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1482,6 +1541,15 @@ 3A90B1802A4EA3AF00000D94 /* UserSearchCache.swift */, D723C38D2AB8D83400065664 /* ContentFilters.swift */, D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */, + D7CB5D3D2B116DAD00AD4105 /* NotificationsManager.swift */, + D7CB5D442B116FE800AD4105 /* Contacts+.swift */, + D7CB5D4A2B11721600AD4105 /* ZapType.swift */, + D7CB5D4D2B11728000AD4105 /* NewEventsBits.swift */, + D7CB5D502B1174D100AD4105 /* FriendFilter.swift */, + D7CB5D5B2B1176B200AD4105 /* MediaUploader.swift */, + D7CB5D5E2B11770C00AD4105 /* FollowState.swift */, + D7EDED1B2B1178FE0018B19C /* NoteContent.swift */, + D7EDED1D2B11797D0018B19C /* LongformEvent.swift */, ); path = Models; sourceTree = ""; @@ -1998,6 +2066,8 @@ D2277EE92A089BD5006C3807 /* Router.swift */, 4C2B10272A7B0F5C008AA43E /* Log.swift */, 50C3E0892AA8E3F7006A4BC0 /* AVPlayer+Additions.swift */, + D7EDED202B117DCA0018B19C /* SequenceUtils.swift */, + D7EDED2D2B128E8A0018B19C /* CollectionExtension.swift */, ); path = Util; sourceTree = ""; @@ -2640,10 +2710,12 @@ buildRules = ( ); dependencies = ( + D7EDED252B117F7C0018B19C /* PBXTargetDependency */, ); name = DamusNotificationService; packageProductDependencies = ( D789D11F2AFEFBF20083A7AB /* secp256k1 */, + D7EDED302B1290B80018B19C /* MarkdownUI */, ); productName = DamusNotificationService; productReference = D79C4C142AFEB061003A41B4 /* DamusNotificationService.appex */; @@ -2809,6 +2881,7 @@ 4C4793072A993E6200489948 /* emitter.c in Sources */, 4C4793062A993E5300489948 /* json_parser.c in Sources */, 4C4793052A993E3200489948 /* builder.c in Sources */, + D7CB5D5F2B11770C00AD4105 /* FollowState.swift in Sources */, 4C4793042A993DC000489948 /* midl.c in Sources */, 0E8A4BB72AE4359200065E81 /* NostrFilter+Hashable.swift in Sources */, 4C4793012A993CDA00489948 /* mdb.c in Sources */, @@ -2822,12 +2895,14 @@ 4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */, 4CDD1AE22A6B3074001CD4DF /* NdbTagsIterator.swift in Sources */, 4C216F34286F5ACD00040376 /* DMView.swift in Sources */, + D7CB5D512B1174D100AD4105 /* FriendFilter.swift in Sources */, 4C32B9572A9AD44700DC3548 /* Root.swift in Sources */, 4C3EA64428FF558100C48A62 /* sha256.c in Sources */, 504323A72A34915F006AE6DC /* RelayModel.swift in Sources */, 4CA9276A2A290FC00098A105 /* ContextButton.swift in Sources */, 4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */, 4C32B9542A9AD44700DC3548 /* FlatBuffersUtils.swift in Sources */, + D7EDED1C2B1178FE0018B19C /* NoteContent.swift in Sources */, 4C363AA828297703006E126D /* InsertSort.swift in Sources */, 4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */, 4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */, @@ -2852,6 +2927,7 @@ 4CC14FF12A73FCDB007AEB17 /* Pubkey.swift in Sources */, 4CA9275D2A28FF630098A105 /* LongformView.swift in Sources */, 4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */, + D7EDED1E2B11797D0018B19C /* LongformEvent.swift in Sources */, 504323A92A3495B6006AE6DC /* RelayModelCache.swift in Sources */, 3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */, D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */, @@ -2899,12 +2975,14 @@ 4C8D1A6F29F31E5000ACDF75 /* FriendsButton.swift in Sources */, 3A5E47C52A4A6CF400C0D090 /* Trie.swift in Sources */, B57B4C642B312BFA00A232C0 /* RelayAuthenticationDetail.swift in Sources */, + D7EDED2E2B128E8A0018B19C /* CollectionExtension.swift in Sources */, 4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */, BA3759972ABCCF360018D73B /* CameraPreview.swift in Sources */, 4C75EFA627FF87A20006080F /* Nostr.swift in Sources */, 4CA927672A290F8B0098A105 /* RelativeTime.swift in Sources */, 4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */, 4C7D096D2A0AEA0400943473 /* CodeScanner.swift in Sources */, + D7CB5D4B2B11721600AD4105 /* ZapType.swift in Sources */, 4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */, 3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */, 4C32B94E2A9AD44700DC3548 /* Mutable.swift in Sources */, @@ -2951,6 +3029,7 @@ 4CEE2AF7280B2DEA00AB5EEF /* ProfileName.swift in Sources */, 4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */, 31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */, + D7CB5D3E2B116DAD00AD4105 /* NotificationsManager.swift in Sources */, 50A16FFF2AA76A0900DFEC1F /* VideoController.swift in Sources */, F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */, 4C285C8228385570008A31F1 /* CarouselView.swift in Sources */, @@ -2976,6 +3055,7 @@ 4CA352AA2A76BF3A003BB08B /* LocalNotificationNotify.swift in Sources */, D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */, 4C7D09682A0AE9B200943473 /* NWCScannerView.swift in Sources */, + D7CB5D452B116FE800AD4105 /* Contacts+.swift in Sources */, 4CA352A42A76AFF3003BB08B /* UpdateStatsNotify.swift in Sources */, D798D21E2B0858BB00234419 /* MigratedTypes.swift in Sources */, 4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */, @@ -3113,6 +3193,7 @@ D76556D62B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift in Sources */, 3165648B295B70D500C64604 /* LinkView.swift in Sources */, 4C8D00CF29E38B950036AF10 /* nostr_bech32.c in Sources */, + D7CB5D5C2B1176B200AD4105 /* MediaUploader.swift in Sources */, 4C1253562A76C8C60004F4B8 /* BroadcastNotify.swift in Sources */, 4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */, 4C32B9532A9AD44700DC3548 /* Verifier.swift in Sources */, @@ -3139,6 +3220,7 @@ 4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */, 4C32B9502A9AD44700DC3548 /* FlatBufferBuilder.swift in Sources */, 50A60D142A28BEEE00186190 /* RelayLog.swift in Sources */, + D7EDED212B117DCA0018B19C /* SequenceUtils.swift in Sources */, BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */, 4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */, 4C32B9512A9AD44700DC3548 /* FlatbuffersErrors.swift in Sources */, @@ -3201,6 +3283,7 @@ 5CC868DD2AA29B3200FB22BA /* NeutralButtonStyle.swift in Sources */, 4C75EFB528049D790006080F /* Relay.swift in Sources */, 4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */, + D7CB5D4E2B11728000AD4105 /* NewEventsBits.swift in Sources */, 4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */, B57B4C622B312BD700A232C0 /* ReconnectRelaysNotify.swift in Sources */, E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */, @@ -3280,56 +3363,83 @@ files = ( D798D21F2B0858D600234419 /* MigratedTypes.swift in Sources */, D7CE1B472B0BE719002EDAD4 /* NativeObject.swift in Sources */, + D7CB5D552B11758A00AD4105 /* UnmuteThreadNotify.swift in Sources */, D7CCFC0E2B0587C300323D86 /* EventRef.swift in Sources */, D7CCFC192B058A3F00323D86 /* Block.swift in Sources */, D7CCFC112B05884E00323D86 /* AsciiCharacter.swift in Sources */, D798D2202B08592000234419 /* NdbTagIterator.swift in Sources */, D7CE1B1D2B0BE14A002EDAD4 /* verifier.c in Sources */, + D7CB5D4F2B11728000AD4105 /* NewEventsBits.swift in Sources */, + D7CB5D412B116F0900AD4105 /* StringCodable.swift in Sources */, D7CE1B1F2B0BE1B8002EDAD4 /* damus.c in Sources */, D7CE1B1B2B0BE144002EDAD4 /* emitter.c in Sources */, + D7CB5D562B11759900AD4105 /* MuteThreadNotify.swift in Sources */, + D7EDED182B1177A00018B19C /* LNUrlPayRequest.swift in Sources */, D798D21C2B0857E400234419 /* Bech32Object.swift in Sources */, + D7CB5D572B11762900AD4105 /* UserStatus.swift in Sources */, D7CE1B402B0BE719002EDAD4 /* FlatBufferObject.swift in Sources */, D7CE1B442B0BE719002EDAD4 /* Mutable.swift in Sources */, D798D2212B08594800234419 /* NdbTagElem.swift in Sources */, D7CE1B432B0BE719002EDAD4 /* String+extension.swift in Sources */, + D7CB5D3F2B116DAD00AD4105 /* NotificationsManager.swift in Sources */, + D7CB5D602B11770C00AD4105 /* FollowState.swift in Sources */, + D7CB5D402B116E8A00AD4105 /* UserSettingsStore.swift in Sources */, D7CE1B1C2B0BE147002EDAD4 /* refmap.c in Sources */, D7CE1B242B0BE1F1002EDAD4 /* hash_u5.c in Sources */, D79C4C172AFEB061003A41B4 /* NotificationService.swift in Sources */, + D7CB5D522B1174D100AD4105 /* FriendFilter.swift in Sources */, D7CE1B362B0BE702002EDAD4 /* FbConstants.swift in Sources */, + D7EDED272B117FF10018B19C /* CompatibleAttribute.swift in Sources */, D7CE1B222B0BE1EB002EDAD4 /* utf8.c in Sources */, D7CCFC072B05833200323D86 /* NdbNote.swift in Sources */, D7CE1B3F2B0BE719002EDAD4 /* Enum.swift in Sources */, + D7EDED222B117DCA0018B19C /* SequenceUtils.swift in Sources */, D7CE1B422B0BE719002EDAD4 /* Offset.swift in Sources */, D7FB10A72B0C371A00FA8D42 /* Log.swift in Sources */, D7CE1B232B0BE1EE002EDAD4 /* bolt11.c in Sources */, D7CE1B182B0BDFDD002EDAD4 /* mdb.c in Sources */, D7CCFC162B05894300323D86 /* Pubkey.swift in Sources */, D7CE1B292B0BE239002EDAD4 /* node_id.c in Sources */, + D7EDED2C2B128CFA0018B19C /* DamusColors.swift in Sources */, D7CE1B2E2B0BE25C002EDAD4 /* talstr.c in Sources */, D798D2292B08686C00234419 /* ContentParsing.swift in Sources */, D798D2242B0859C900234419 /* LocalizationUtil.swift in Sources */, D7CE1B322B0BE6C3002EDAD4 /* NdbTxn.swift in Sources */, D7CE1B372B0BE719002EDAD4 /* Verifier.swift in Sources */, + D7EDED292B1182060018B19C /* AttachMediaUtility.swift in Sources */, D798D21A2B0856CC00234419 /* Mentions.swift in Sources */, D7CE1B212B0BE1CB002EDAD4 /* wasm.c in Sources */, D7CE1B3B2B0BE719002EDAD4 /* Int+extension.swift in Sources */, D7CCFC0B2B0585EA00323D86 /* nostrdb.c in Sources */, D7CE1B252B0BE1F4002EDAD4 /* sha256.c in Sources */, D7CE1B262B0BE1F8002EDAD4 /* bech32.c in Sources */, + D7EDED232B117DFB0018B19C /* NoteContent.swift in Sources */, D798D21B2B0856F200234419 /* NdbTagsIterator.swift in Sources */, D7CE1B352B0BE6FA002EDAD4 /* ByteBuffer.swift in Sources */, D7CE1B2F2B0BE260002EDAD4 /* list.c in Sources */, + D7CB5D422B116F8900AD4105 /* Contacts.swift in Sources */, + D7CB5D5D2B1176B200AD4105 /* MediaUploader.swift in Sources */, D7CE1B342B0BE6EE002EDAD4 /* NdbProfile.swift in Sources */, D7DBD41F2B02F15E002A6197 /* NostrKind.swift in Sources */, D7CE1B3C2B0BE719002EDAD4 /* TableVerifier.swift in Sources */, + D7EDED2F2B128E8A0018B19C /* CollectionExtension.swift in Sources */, D7CCFC082B05834500323D86 /* NoteId.swift in Sources */, D7CE1B1A2B0BE135002EDAD4 /* json_parser.c in Sources */, + D7EDED2A2B128CB40018B19C /* Nip98HTTPAuth.swift in Sources */, + D7CB5D592B11764000AD4105 /* NewUnmutesNotify.swift in Sources */, D798D2252B0859D700234419 /* Post.swift in Sources */, + D7EDED172B1177960018B19C /* TranslationService.swift in Sources */, D7CCFC0F2B0587F600323D86 /* Keys.swift in Sources */, + D7CB5D542B1174F700AD4105 /* NIP05.swift in Sources */, D798D2232B0859B700234419 /* KeychainStorage.swift in Sources */, + D7CB5D432B116F9B00AD4105 /* MutedThreadsManager.swift in Sources */, D7CE1B272B0BE224002EDAD4 /* bech32_util.c in Sources */, D7CCFC102B05880F00323D86 /* Id.swift in Sources */, + D7CB5D532B1174E900AD4105 /* DeepLPlan.swift in Sources */, + D7EDED282B1180940018B19C /* ImageUploadModel.swift in Sources */, D7CE1B2A2B0BE23E002EDAD4 /* mem.c in Sources */, + D7CB5D4C2B11721600AD4105 /* ZapType.swift in Sources */, + D7EDED2B2B128CDB0018B19C /* Hashtags.swift in Sources */, D7CE1B332B0BE6DE002EDAD4 /* Nostr.swift in Sources */, D7CE1B3D2B0BE719002EDAD4 /* Verifiable.swift in Sources */, D7CE1B382B0BE719002EDAD4 /* VeriferOptions.swift in Sources */, @@ -3338,6 +3448,7 @@ D798D2222B08598A00234419 /* ReferencedId.swift in Sources */, D7CE1B492B0BE729002EDAD4 /* DisplayName.swift in Sources */, D7CE1B192B0BE132002EDAD4 /* builder.c in Sources */, + D7EDED1F2B11797D0018B19C /* LongformEvent.swift in Sources */, D7CE1B282B0BE226002EDAD4 /* tal.c in Sources */, D7CCFC122B05886D00323D86 /* IdType.swift in Sources */, D7CE1B312B0BE69D002EDAD4 /* Ndb.swift in Sources */, @@ -3346,16 +3457,23 @@ D7CE1B462B0BE719002EDAD4 /* FlatBufferBuilder.swift in Sources */, D7CE1B3E2B0BE719002EDAD4 /* FlatbuffersErrors.swift in Sources */, D7CE1B2C2B0BE24B002EDAD4 /* amount.c in Sources */, + D7EDED152B11776B0018B19C /* LibreTranslateServer.swift in Sources */, D7CE1B202B0BE1C8002EDAD4 /* error.c in Sources */, + D7CB5D582B11763C00AD4105 /* NewMutesNotify.swift in Sources */, D798D22D2B086DC400234419 /* NostrEvent.swift in Sources */, D798D22E2B086E4800234419 /* NostrResponse.swift in Sources */, + D7EDED162B1177840018B19C /* LNUrls.swift in Sources */, D7CE1B302B0BE263002EDAD4 /* nostr_bech32.c in Sources */, D7CCFC132B05887C00323D86 /* ProofOfWork.swift in Sources */, D7CE1B392B0BE719002EDAD4 /* Table.swift in Sources */, D7CE1B452B0BE719002EDAD4 /* Root.swift in Sources */, + D7CB5D472B11718700AD4105 /* Wallet.swift in Sources */, D7CE1B412B0BE719002EDAD4 /* FlatBuffersUtils.swift in Sources */, + D7CB5D482B11719300AD4105 /* Profiles.swift in Sources */, D798D2262B085C4200234419 /* Bech32.swift in Sources */, D7CE1B482B0BE719002EDAD4 /* Message.swift in Sources */, + D7CB5D462B11703D00AD4105 /* Notify.swift in Sources */, + D7EDED262B117FC80018B19C /* StringUtil.swift in Sources */, D7CE1B1E2B0BE190002EDAD4 /* midl.c in Sources */, D7CB5D3C2B1130C600AD4105 /* LocalNotification.swift in Sources */, D7CE1B2D2B0BE250002EDAD4 /* take.c in Sources */, @@ -3380,6 +3498,10 @@ target = D79C4C132AFEB061003A41B4 /* DamusNotificationService */; targetProxy = D79C4C192AFEB061003A41B4 /* PBXContainerItemProxy */; }; + D7EDED252B117F7C0018B19C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + productRef = D7EDED242B117F7C0018B19C /* MarkdownUI */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -3980,6 +4102,16 @@ package = D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */; productName = SnapshotTesting; }; + D7EDED242B117F7C0018B19C /* MarkdownUI */ = { + isa = XCSwiftPackageProductDependency; + package = 4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */; + productName = MarkdownUI; + }; + D7EDED302B1290B80018B19C /* MarkdownUI */ = { + isa = XCSwiftPackageProductDependency; + package = 4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */; + productName = MarkdownUI; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 4CE6DEDB27F7A08100C66700 /* Project object */; diff --git a/damus.xcodeproj/xcshareddata/xcschemes/DamusNotificationService.xcscheme b/damus.xcodeproj/xcshareddata/xcschemes/DamusNotificationService.xcscheme index c7fded09..3c546f9b 100644 --- a/damus.xcodeproj/xcshareddata/xcschemes/DamusNotificationService.xcscheme +++ b/damus.xcodeproj/xcshareddata/xcschemes/DamusNotificationService.xcscheme @@ -59,7 +59,7 @@ + RemotePath = "/Users/danielnogueira/Library/Developer/CoreSimulator/Devices/99E60B35-CE5D-4B45-AC35-00818C0AF3CB/data/Containers/Bundle/Application/12BC3574-F80A-4852-869A-0D826412B040/damus.app"> NostrEvent? { + guard let ev = follow_user_event(our_contacts: our_contacts, keypair: keypair, follow: follow) else { + return nil + } + + box.send(ev) + + return ev +} + +func unfollow_reference(postbox: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, unfollow: FollowRef) -> NostrEvent? { + guard let cs = our_contacts else { + return nil + } + + guard let ev = unfollow_reference_event(our_contacts: cs, keypair: keypair, unfollow: unfollow) else { + return nil + } + + postbox.send(ev) + + return ev +} + +func unfollow_reference_event(our_contacts: NostrEvent, keypair: FullKeypair, unfollow: FollowRef) -> NostrEvent? { + let tags = our_contacts.tags.reduce(into: [[String]]()) { ts, tag in + if let tag = FollowRef.from_tag(tag: tag), tag == unfollow { + return + } + + ts.append(tag.strings()) + } + + let kind = NostrKind.contacts.rawValue + + return NostrEvent(content: our_contacts.content, keypair: keypair.to_keypair(), kind: kind, tags: Array(tags)) +} + +func follow_user_event(our_contacts: NostrEvent?, keypair: FullKeypair, follow: FollowRef) -> NostrEvent? { + guard let cs = our_contacts else { + // don't create contacts for now so we don't nuke our contact list due to connectivity issues + // we should only create contacts during profile creation + //return create_contacts(relays: relays, our_pubkey: our_pubkey, follow: follow) + return nil + } + + guard let ev = follow_with_existing_contacts(keypair: keypair, our_contacts: cs, follow: follow) else { + return nil + } + + return ev +} + + +func decode_json_relays(_ content: String) -> [String: RelayInfo]? { + return decode_json(content) +} + +func decode_json_relays(_ content: String) -> [RelayURL: RelayInfo]? { + return decode_json(content) +} + +func remove_relay(ev: NostrEvent, current_relays: [RelayDescriptor], keypair: FullKeypair, relay: RelayURL) -> NostrEvent?{ + var relays = ensure_relay_info(relays: current_relays, content: ev.content) + + relays.removeValue(forKey: relay) + + guard let content = encode_json(relays) else { + return nil + } + + return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings()) +} + +func add_relay(ev: NostrEvent, keypair: FullKeypair, current_relays: [RelayDescriptor], relay: RelayURL, info: RelayInfo) -> NostrEvent? { + var relays = ensure_relay_info(relays: current_relays, content: ev.content) + + // If kind:3 content is empty, or if the relay doesn't exist in the list, + // we want to create a kind:3 event with the new relay + guard ev.content.isEmpty || relays.index(forKey: relay) == nil else { + return nil + } + + relays[relay] = info + + guard let content = encode_json(relays) else { + return nil + } + + return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings()) +} + +func ensure_relay_info(relays: [RelayDescriptor], content: String) -> [RelayURL: RelayInfo] { + return decode_json_relays(content) ?? make_contact_relays(relays) +} + +func is_already_following(contacts: NostrEvent, follow: FollowRef) -> Bool { + return contacts.references.contains { ref in + switch (ref, follow) { + case let (.hashtag(ht), .hashtag(follow_ht)): + return ht.string() == follow_ht + case let (.pubkey(pk), .pubkey(follow_pk)): + return pk == follow_pk + case (.hashtag, .pubkey), (.pubkey, .hashtag), + (.event, _), (.quote, _), (.param, _): + return false + } + } +} +func follow_with_existing_contacts(keypair: FullKeypair, our_contacts: NostrEvent, follow: FollowRef) -> NostrEvent? { + // don't update if we're already following + if is_already_following(contacts: our_contacts, follow: follow) { + return nil + } + + let kind = NostrKind.contacts.rawValue + + var tags = our_contacts.tags.strings() + tags.append(follow.tag) + + 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.id] + 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) +} diff --git a/damus/Models/Contacts.swift b/damus/Models/Contacts.swift index f004495a..5829ed9c 100644 --- a/damus/Models/Contacts.swift +++ b/damus/Models/Contacts.swift @@ -125,145 +125,3 @@ class Contacts { return Array((pubkey_to_our_friends[pubkey] ?? Set())) } } - -func follow_reference(box: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, follow: FollowRef) -> NostrEvent? { - guard let ev = follow_user_event(our_contacts: our_contacts, keypair: keypair, follow: follow) else { - return nil - } - - box.send(ev) - - return ev -} - -func unfollow_reference(postbox: PostBox, our_contacts: NostrEvent?, keypair: FullKeypair, unfollow: FollowRef) -> NostrEvent? { - guard let cs = our_contacts else { - return nil - } - - guard let ev = unfollow_reference_event(our_contacts: cs, keypair: keypair, unfollow: unfollow) else { - return nil - } - - postbox.send(ev) - - return ev -} - -func unfollow_reference_event(our_contacts: NostrEvent, keypair: FullKeypair, unfollow: FollowRef) -> NostrEvent? { - let tags = our_contacts.tags.reduce(into: [[String]]()) { ts, tag in - if let tag = FollowRef.from_tag(tag: tag), tag == unfollow { - return - } - - ts.append(tag.strings()) - } - - let kind = NostrKind.contacts.rawValue - - return NostrEvent(content: our_contacts.content, keypair: keypair.to_keypair(), kind: kind, tags: Array(tags)) -} - -func follow_user_event(our_contacts: NostrEvent?, keypair: FullKeypair, follow: FollowRef) -> NostrEvent? { - guard let cs = our_contacts else { - // don't create contacts for now so we don't nuke our contact list due to connectivity issues - // we should only create contacts during profile creation - //return create_contacts(relays: relays, our_pubkey: our_pubkey, follow: follow) - return nil - } - - guard let ev = follow_with_existing_contacts(keypair: keypair, our_contacts: cs, follow: follow) else { - return nil - } - - return ev -} - - -func decode_json_relays(_ content: String) -> [String: RelayInfo]? { - return decode_json(content) -} - -func decode_json_relays(_ content: String) -> [RelayURL: RelayInfo]? { - return decode_json(content) -} - -func remove_relay(ev: NostrEvent, current_relays: [RelayDescriptor], keypair: FullKeypair, relay: RelayURL) -> NostrEvent?{ - var relays = ensure_relay_info(relays: current_relays, content: ev.content) - - relays.removeValue(forKey: relay) - - guard let content = encode_json(relays) else { - return nil - } - - return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings()) -} - -func add_relay(ev: NostrEvent, keypair: FullKeypair, current_relays: [RelayDescriptor], relay: RelayURL, info: RelayInfo) -> NostrEvent? { - var relays = ensure_relay_info(relays: current_relays, content: ev.content) - - // If kind:3 content is empty, or if the relay doesn't exist in the list, we want to create a kind:3 event with the new relay - guard ev.content.isEmpty || relays.index(forKey: relay) == nil else { - return nil - } - - relays[relay] = info - - guard let content = encode_json(relays) else { - return nil - } - - return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 3, tags: ev.tags.strings()) -} - -func make_relay_metadata(relays: [RelayDescriptor], keypair: FullKeypair) -> NostrEvent? { - let tags = relays.compactMap { r -> [String]? in - var tag = ["r", r.url.id] - 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) -} - -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) { - case let (.hashtag(ht), .hashtag(follow_ht)): - return ht.string() == follow_ht - case let (.pubkey(pk), .pubkey(follow_pk)): - return pk == follow_pk - case (.hashtag, .pubkey), (.pubkey, .hashtag), - (.event, _), (.quote, _), (.param, _): - return false - } - } -} -func follow_with_existing_contacts(keypair: FullKeypair, our_contacts: NostrEvent, follow: FollowRef) -> NostrEvent? { - // don't update if we're already following - if is_already_following(contacts: our_contacts, follow: follow) { - return nil - } - - let kind = NostrKind.contacts.rawValue - - var tags = our_contacts.tags.strings() - tags.append(follow.tag) - - 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 - } -} diff --git a/damus/Models/FollowState.swift b/damus/Models/FollowState.swift new file mode 100644 index 00000000..4c5a38b3 --- /dev/null +++ b/damus/Models/FollowState.swift @@ -0,0 +1,15 @@ +// +// FollowState.swift +// damus +// +// Created by Daniel D’Aquino on 2023-11-24. +// + +import Foundation + +enum FollowState { + case follows + case following + case unfollowing + case unfollows +} diff --git a/damus/Models/FriendFilter.swift b/damus/Models/FriendFilter.swift new file mode 100644 index 00000000..c56aa82e --- /dev/null +++ b/damus/Models/FriendFilter.swift @@ -0,0 +1,34 @@ +// +// FriendFilter.swift +// damus +// +// Created by Daniel D’Aquino on 2023-11-24. +// + +import Foundation + +enum FriendFilter: String, StringCodable { + case all + case friends + + init?(from string: String) { + guard let ff = FriendFilter(rawValue: string) else { + return nil + } + + self = ff + } + + func to_string() -> String { + self.rawValue + } + + func filter(contacts: Contacts, pubkey: Pubkey) -> Bool { + switch self { + case .all: + return true + case .friends: + return contacts.is_friend_or_self(pubkey) + } + } +} diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift index e6a88637..ac2dc07b 100644 --- a/damus/Models/HomeModel.swift +++ b/damus/Models/HomeModel.swift @@ -8,21 +8,6 @@ import Foundation import UIKit -struct NewEventsBits: OptionSet { - let rawValue: Int - - static let home = NewEventsBits(rawValue: 1 << 0) - static let zaps = NewEventsBits(rawValue: 1 << 1) - static let mentions = NewEventsBits(rawValue: 1 << 2) - static let reposts = NewEventsBits(rawValue: 1 << 3) - static let likes = NewEventsBits(rawValue: 1 << 4) - static let search = NewEventsBits(rawValue: 1 << 5) - static let dms = NewEventsBits(rawValue: 1 << 6) - - static let all = NewEventsBits(rawValue: 0xFFFFFFFF) - static let notifications: NewEventsBits = [.zaps, .likes, .reposts, .mentions] -} - enum Resubscribe { case following case unfollowing(FollowRef) @@ -58,7 +43,7 @@ enum HomeResubFilter { class HomeModel { // Don't trigger a user notification for events older than a certain age - static let event_max_age_for_notification: TimeInterval = 12 * 60 * 60 + static let event_max_age_for_notification: TimeInterval = EVENT_MAX_AGE_FOR_NOTIFICATION var damus_state: DamusState @@ -1176,104 +1161,16 @@ func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale: } } -func render_notification_content_preview(cache: EventCache, ev: NostrEvent, profiles: Profiles, keypair: Keypair) -> String { - - let prefix_len = 300 - let artifacts = cache.get_cache_data(ev.id).artifacts.artifacts ?? render_note_content(ev: ev, profiles: profiles, keypair: keypair) - - // special case for longform events - if ev.known_kind == .longform { - let longform = LongformEvent(event: ev) - return longform.title ?? longform.summary ?? "Longform Event" - } - - switch artifacts { - case .longform: - // we should never hit this until we have more note types built out of parts - // since we handle this case above in known_kind == .longform - return String(ev.content.prefix(prefix_len)) - - case .separated(let artifacts): - return String(NSAttributedString(artifacts.content.attributed).string.prefix(prefix_len)) - } -} - func process_local_notification(damus_state: DamusState, event ev: NostrEvent) { - guard let type = ev.known_kind else { - return - } - - if damus_state.settings.notification_only_from_following, - damus_state.contacts.follow_state(ev.pubkey) != .follows - { - return - } - - // Don't show notifications from muted threads. - if damus_state.muted_threads.isMutedThread(ev, keypair: damus_state.keypair) { - return - } - - // Don't show notifications for old events - guard ev.age < HomeModel.event_max_age_for_notification else { - return - } - - guard let local_notification = generate_local_notification_object(from: ev, damus_state: damus_state) else { - return - } - create_local_notification(profiles: damus_state.profiles, notify: local_notification) -} - -// TODO: Further break down this function and related functionality so that we can use this from the Notification service extension -func generate_local_notification_object(from ev: NostrEvent, damus_state: DamusState) -> LocalNotification? { - guard let type = ev.known_kind else { - return nil - } - - if type == .text, damus_state.settings.mention_notification { - let blocks = ev.blocks(damus_state.keypair).blocks - for case .mention(let mention) in blocks { - guard case .pubkey(let pk) = mention.ref, pk == damus_state.keypair.pubkey else { - continue - } - let content_preview = render_notification_content_preview(cache: damus_state.events, ev: ev, profiles: damus_state.profiles, keypair: damus_state.keypair) - return LocalNotification(type: .mention, event: ev, target: ev, content: content_preview) - } - } else if type == .boost, - damus_state.settings.repost_notification, - let inner_ev = ev.get_inner_event(cache: damus_state.events) - { - let content_preview = render_notification_content_preview(cache: damus_state.events, ev: inner_ev, profiles: damus_state.profiles, keypair: damus_state.keypair) - return LocalNotification(type: .repost, event: ev, target: inner_ev, content: content_preview) - } else if type == .like, - damus_state.settings.like_notification, - let evid = ev.referenced_ids.last, - let liked_event = damus_state.events.lookup(evid) - { - let content_preview = render_notification_content_preview(cache: damus_state.events, ev: liked_event, profiles: damus_state.profiles, keypair: damus_state.keypair) - return LocalNotification(type: .like, event: ev, target: liked_event, content: content_preview) - } - - return nil -} - -func create_local_notification(profiles: Profiles, notify: LocalNotification) { - let displayName = event_author_name(profiles: profiles, pubkey: notify.event.pubkey) - - let (content, identifier) = NotificationFormatter.shared.format_message(displayName: displayName, notify: notify) - - let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) - - let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger) - - UNUserNotificationCenter.current().add(request) { error in - if let error = error { - print("Error: \(error)") - } else { - print("Local notification scheduled") - } - } + process_local_notification( + ndb: damus_state.ndb, + settings: damus_state.settings, + contacts: damus_state.contacts, + muted_threads: damus_state.muted_threads, + user_keypair: damus_state.keypair, + profiles: damus_state.profiles, + event: ev + ) } @@ -1283,21 +1180,6 @@ enum ProcessZapResult { case failed } -extension Sequence { - func just_one() -> Element? { - var got_one = false - var the_x: Element? = nil - for x in self { - guard !got_one else { - return nil - } - the_x = x - got_one = true - } - return the_x - } -} - // securely get the zap target's pubkey. this can be faked so we need to be // careful func get_zap_target_pubkey(ev: NostrEvent, events: EventCache) -> Pubkey? { diff --git a/damus/Models/LongformEvent.swift b/damus/Models/LongformEvent.swift new file mode 100644 index 00000000..bdf4318e --- /dev/null +++ b/damus/Models/LongformEvent.swift @@ -0,0 +1,36 @@ +// +// LongformEvent.swift +// damus +// +// Created by Daniel Nogueira on 2023-11-24. +// + +import Foundation + +struct LongformEvent { + let event: NostrEvent + + var title: String? = nil + var image: URL? = nil + var summary: String? = nil + var published_at: Date? = nil + + static func parse(from ev: NostrEvent) -> LongformEvent { + var longform = LongformEvent(event: ev) + + for tag in ev.tags { + guard tag.count >= 2 else { continue } + switch tag[0].string() { + case "title": longform.title = tag[1].string() + case "image": longform.image = URL(string: tag[1].string()) + case "summary": longform.summary = tag[1].string() + case "published_at": + longform.published_at = Double(tag[1].string()).map { d in Date(timeIntervalSince1970: d) } + default: + break + } + } + + return longform + } +} diff --git a/damus/Models/MediaUploader.swift b/damus/Models/MediaUploader.swift new file mode 100644 index 00000000..748584cd --- /dev/null +++ b/damus/Models/MediaUploader.swift @@ -0,0 +1,117 @@ +// +// MediaUploader.swift +// damus +// +// Created by Daniel D’Aquino on 2023-11-24. +// + +import Foundation + +enum MediaUploader: String, CaseIterable, Identifiable, StringCodable { + var id: String { self.rawValue } + case nostrBuild + case nostrImg + + init?(from string: String) { + guard let mu = MediaUploader(rawValue: string) else { + return nil + } + + self = mu + } + + func to_string() -> String { + return rawValue + } + + var nameParam: String { + switch self { + case .nostrBuild: + return "\"fileToUpload\"" + case .nostrImg: + return "\"image\"" + } + } + + var supportsVideo: Bool { + switch self { + case .nostrBuild: + return true + case .nostrImg: + return false + } + } + + struct Model: Identifiable, Hashable { + var id: String { self.tag } + var index: Int + var tag: String + var displayName : String + } + + var model: Model { + switch self { + case .nostrBuild: + return .init(index: -1, tag: "nostrBuild", displayName: "nostr.build") + case .nostrImg: + return .init(index: 0, tag: "nostrImg", displayName: "nostrimg.com") + } + } + + + var postAPI: String { + switch self { + case .nostrBuild: + return "https://nostr.build/api/v2/upload/files" + case .nostrImg: + return "https://nostrimg.com/api/upload" + } + } + + func getMediaURL(from data: Data) -> String? { + switch self { + case .nostrBuild: + do { + if let jsonObject = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any], + let status = jsonObject["status"] as? String { + + if status == "success", let dataArray = jsonObject["data"] as? [[String: Any]] { + + var urls: [String] = [] + + for dataDict in dataArray { + if let mainUrl = dataDict["url"] as? String { + urls.append(mainUrl) + } + } + + return urls.joined(separator: "\n") + } else if status == "error", let message = jsonObject["message"] as? String { + print("Upload Error: \(message)") + return nil + } + } + } catch { + print("Failed JSONSerialization") + return nil + } + return nil + case .nostrImg: + guard let responseString = String(data: data, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue)) else { + print("Upload failed getting response string") + return nil + } + + guard let startIndex = responseString.range(of: "https://i.nostrimg.com/")?.lowerBound else { + return nil + } + let stringContainingName = responseString[startIndex.. Bool { + return lhs.content == rhs.content + } + + let content: CompatibleText + let words: Int + let urls: [UrlType] + let invoices: [Invoice] + + var media: [MediaUrl] { + return urls.compactMap { url in url.is_media } + } + + var images: [URL] { + return urls.compactMap { url in url.is_img } + } + + var links: [URL] { + return urls.compactMap { url in url.is_link } + } + + static func just_content(_ content: String) -> NoteArtifactsSeparated { + let txt = CompatibleText(attributed: AttributedString(stringLiteral: content)) + return NoteArtifactsSeparated(content: txt, words: 0, urls: [], invoices: []) + } +} + +enum NoteArtifactState { + case not_loaded + case loading + case loaded(NoteArtifacts) + + var artifacts: NoteArtifacts? { + if case .loaded(let artifacts) = self { + return artifacts + } + + return nil + } + + var should_preload: Bool { + switch self { + case .loaded: + return false + case .loading: + return false + case .not_loaded: + return true + } + } +} + +func note_artifact_is_separated(kind: NostrKind?) -> Bool { + return kind != .longform +} + +func render_note_content(ev: NostrEvent, profiles: Profiles, keypair: Keypair) -> NoteArtifacts { + let blocks = ev.blocks(keypair) + + if ev.known_kind == .longform { + return .longform(LongformContent(ev.content)) + } + + return .separated(render_blocks(blocks: blocks, profiles: profiles)) +} + +func render_blocks(blocks bs: Blocks, profiles: Profiles) -> 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 + } + else { + return false + } + }) + .count == 1 + + var ind: Int = -1 + let txt: CompatibleText = blocks.reduce(CompatibleText()) { str, block in + ind = ind + 1 + + switch block { + case .mention(let m): + if case .note = m.ref, one_note_ref { + return str + } + 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)) + + 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 + 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 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 { + 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) + } + + 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) + } + } + + return trimmed +} + +func url_str(_ url: URL) -> CompatibleText { + var attributedString = AttributedString(stringLiteral: url.absoluteString) + attributedString.link = url + attributedString.foregroundColor = DamusColors.purple + + return CompatibleText(attributed: attributedString) +} + +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") { + return .media(.image(url)) + } + + if str.hasSuffix(".mp4") || str.hasSuffix(".mov") || str.hasSuffix(".m3u8") { + return .media(.video(url)) + } + + return .link(url) +} + +func attributed_string_attach_icon(_ astr: inout AttributedString, img: UIImage) { + let attachment = NSTextAttachment() + attachment.image = img + let attachmentString = NSAttributedString(attachment: attachment) + let wrapped = AttributedString(attachmentString) + astr.append(wrapped) +} + +func mention_str(_ m: Mention, profiles: Profiles) -> CompatibleText { + switch m.ref { + case .pubkey(let pk): + let npub = bech32_pubkey(pk) + let profile_txn = profiles.lookup(id: pk) + let profile = profile_txn.unsafeUnownedValue + let disp = Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 50) + var attributedString = AttributedString(stringLiteral: "@\(disp)") + attributedString.link = URL(string: "damus:nostr:\(npub)") + attributedString.foregroundColor = DamusColors.purple + + return CompatibleText(attributed: attributedString) + case .note(let note_id): + let bevid = bech32_note_id(note_id) + var attributedString = AttributedString(stringLiteral: "@\(abbrev_pubkey(bevid))") + attributedString.link = URL(string: "damus:nostr:\(bevid)") + attributedString.foregroundColor = DamusColors.purple + + return CompatibleText(attributed: attributedString) + } +} + +// trim suffix whitespace and newlines +func trim_suffix(_ str: String) -> String { + return str.replacingOccurrences(of: "\\s+$", with: "", options: .regularExpression) +} + +// trim prefix whitespace and newlines +func trim_prefix(_ str: String) -> String { + return str.replacingOccurrences(of: "^\\s+", with: "", options: .regularExpression) +} + +struct LongformContent { + let markdown: MarkdownContent + let words: Int + + init(_ markdown: String) { + let blocks = [BlockNode].init(markdown: markdown) + self.markdown = MarkdownContent(blocks: blocks) + self.words = count_markdown_words(blocks: blocks) + } +} + +func count_markdown_words(blocks: [BlockNode]) -> Int { + return blocks.reduce(0) { words, block in + switch block { + case .paragraph(let content): + return words + count_inline_nodes_words(nodes: content) + case .blockquote, .bulletedList, .numberedList, .taskList, .codeBlock, .htmlBlock, .heading, .table, .thematicBreak: + return words + } + } +} + +func count_words(_ s: String) -> Int { + return s.components(separatedBy: .whitespacesAndNewlines).count +} + +func count_inline_nodes_words(nodes: [InlineNode]) -> Int { + return nodes.reduce(0) { words, node in + switch node { + case .text(let words): + return count_words(words) + case .emphasis(let children): + return words + count_inline_nodes_words(nodes: children) + case .strong(let children): + return words + count_inline_nodes_words(nodes: children) + case .strikethrough(let children): + return words + count_inline_nodes_words(nodes: children) + case .softBreak, .lineBreak, .code, .html, .image, .link: + return words + } + } +} + +enum NoteArtifacts { + case separated(NoteArtifactsSeparated) + case longform(LongformContent) + + var images: [URL] { + switch self { + case .separated(let arts): + return arts.images + case .longform: + return [] + } + } +} + +enum UrlType { + case media(MediaUrl) + case link(URL) + + var url: URL { + switch self { + case .media(let media_url): + switch media_url { + case .image(let url): + return url + case .video(let url): + return url + } + case .link(let url): + return url + } + } + + var is_video: URL? { + switch self { + case .media(let media_url): + switch media_url { + case .image: + return nil + case .video(let url): + return url + } + case .link: + return nil + } + } + + var is_img: URL? { + switch self { + case .media(let media_url): + switch media_url { + case .image(let url): + return url + case .video: + return nil + } + case .link: + return nil + } + } + + var is_link: URL? { + switch self { + case .media: + return nil + case .link(let url): + return url + } + } + + var is_media: MediaUrl? { + switch self { + case .media(let murl): + return murl + case .link: + return nil + } + } +} + +enum MediaUrl { + case image(URL) + case video(URL) + + var url: URL { + switch self { + case .image(let url): + return url + case .video(let url): + return url + } + } +} diff --git a/damus/Models/NotificationsManager.swift b/damus/Models/NotificationsManager.swift new file mode 100644 index 00000000..7415b2e1 --- /dev/null +++ b/damus/Models/NotificationsManager.swift @@ -0,0 +1,125 @@ +// +// NotificationsManager.swift +// damus +// +// Handles several aspects of notification logic (Both local and push notifications) +// +// Created by Daniel D’Aquino on 2023-11-24. +// + +import Foundation +import UIKit + +let EVENT_MAX_AGE_FOR_NOTIFICATION: TimeInterval = 12 * 60 * 60 + +func process_local_notification(ndb: Ndb, settings: UserSettingsStore, contacts: Contacts, muted_threads: MutedThreadsManager, user_keypair: Keypair, profiles: Profiles, event ev: NostrEvent) { + if ev.known_kind == nil { + return + } + + if settings.notification_only_from_following, + contacts.follow_state(ev.pubkey) != .follows + { + return + } + + // Don't show notifications from muted threads. + if muted_threads.isMutedThread(ev, keypair: user_keypair) { + return + } + + // Don't show notifications for old events + guard ev.age < EVENT_MAX_AGE_FOR_NOTIFICATION else { + return + } + + guard let local_notification = generate_local_notification_object( + ndb: ndb, + from: ev, + settings: settings, + user_keypair: user_keypair, + profiles: profiles + ) else { + return + } + create_local_notification(profiles: profiles, notify: local_notification) +} + + +func generate_local_notification_object(ndb: Ndb, from ev: NostrEvent, settings: UserSettingsStore, user_keypair: Keypair, profiles: Profiles) -> LocalNotification? { + guard let type = ev.known_kind else { + return nil + } + + if type == .text, settings.mention_notification { + let blocks = ev.blocks(user_keypair).blocks + for case .mention(let mention) in blocks { + guard case .pubkey(let pk) = mention.ref, pk == user_keypair.pubkey else { + continue + } + let content_preview = render_notification_content_preview(ev: ev, profiles: profiles, keypair: user_keypair) + return LocalNotification(type: .mention, event: ev, target: ev, content: content_preview) + } + } else if type == .boost, + settings.repost_notification, + let inner_ev = ev.get_inner_event() + { + let content_preview = render_notification_content_preview(ev: inner_ev, profiles: profiles, keypair: user_keypair) + return LocalNotification(type: .repost, event: ev, target: inner_ev, content: content_preview) + } else if type == .like, + settings.like_notification, + let evid = ev.referenced_ids.last, + let liked_event = ndb.lookup_note(evid).unsafeUnownedValue // We are only accessing it temporarily to generate notification content + { + let content_preview = render_notification_content_preview(ev: liked_event, profiles: profiles, keypair: user_keypair) + return LocalNotification(type: .like, event: ev, target: liked_event, content: content_preview) + } + + return nil +} + +func create_local_notification(profiles: Profiles, notify: LocalNotification) { + let displayName = event_author_name(profiles: profiles, pubkey: notify.event.pubkey) + + let (content, identifier) = NotificationFormatter.shared.format_message(displayName: displayName, notify: notify) + + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) + + let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger) + + UNUserNotificationCenter.current().add(request) { error in + if let error = error { + print("Error: \(error)") + } else { + print("Local notification scheduled") + } + } +} + +func render_notification_content_preview(ev: NostrEvent, profiles: Profiles, keypair: Keypair) -> String { + + let prefix_len = 300 + let artifacts = render_note_content(ev: ev, profiles: profiles, keypair: keypair) + + // special case for longform events + if ev.known_kind == .longform { + let longform = LongformEvent(event: ev) + return longform.title ?? longform.summary ?? "Longform Event" + } + + switch artifacts { + case .longform: + // we should never hit this until we have more note types built out of parts + // since we handle this case above in known_kind == .longform + return String(ev.content.prefix(prefix_len)) + + case .separated(let artifacts): + return String(NSAttributedString(artifacts.content.attributed).string.prefix(prefix_len)) + } +} + +func event_author_name(profiles: Profiles, pubkey: Pubkey) -> String { + return profiles.lookup(id: pubkey).map({ profile in + Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50) + }).value +} diff --git a/damus/Models/UserSettingsStore.swift b/damus/Models/UserSettingsStore.swift index f4d1b259..191591c3 100644 --- a/damus/Models/UserSettingsStore.swift +++ b/damus/Models/UserSettingsStore.swift @@ -9,6 +9,7 @@ import Foundation import UIKit let fallback_zap_amount = 1000 +let default_emoji_reactions = ["🤣", "🤙", "⚡", "💜", "🔥", "😀", "😃", "😄", "🥶"] func setting_property_key(key: String) -> String { return pk_setting_key(UserSettingsStore.pubkey ?? .empty, key: key) diff --git a/damus/Models/ZapType.swift b/damus/Models/ZapType.swift new file mode 100644 index 00000000..454df17a --- /dev/null +++ b/damus/Models/ZapType.swift @@ -0,0 +1,28 @@ +// +// ZapType.swift +// damus +// +// Created by Daniel D’Aquino on 2023-11-24. +// + +import Foundation + +enum ZapType: String, StringCodable { + case pub + case anon + case priv + case non_zap + + init?(from string: String) { + guard let v = ZapType(rawValue: string) else { + return nil + } + + self = v + } + + func to_string() -> String { + return self.rawValue + } + +} diff --git a/damus/Util/CollectionExtension.swift b/damus/Util/CollectionExtension.swift new file mode 100644 index 00000000..8bd30aad --- /dev/null +++ b/damus/Util/CollectionExtension.swift @@ -0,0 +1,15 @@ +// +// CollectionExtension.swift +// damus +// +// Created by Daniel D’Aquino on 2023-11-25. +// + +import Foundation + +extension Collection { + /// Returns the element at the specified index if it is within bounds, otherwise nil. + subscript (safe index: Index) -> Element? { + return indices.contains(index) ? self[index] : nil + } +} diff --git a/damus/Util/CompatibleAttribute.swift b/damus/Util/CompatibleAttribute.swift index 5a3ecdd0..652eb7f4 100644 --- a/damus/Util/CompatibleAttribute.swift +++ b/damus/Util/CompatibleAttribute.swift @@ -101,3 +101,13 @@ extension CompatibleText { } } } + + +func icon_attributed_string(img: UIImage) -> AttributedString { + let attachment = NSTextAttachment() + attachment.image = img + let attachmentString = NSAttributedString(attachment: attachment) + return AttributedString(attachmentString) +} + + diff --git a/damus/Util/Constants.swift b/damus/Util/Constants.swift index 1fe7d8b3..1a9c819f 100644 --- a/damus/Util/Constants.swift +++ b/damus/Util/Constants.swift @@ -8,9 +8,11 @@ import Foundation class Constants { - static let PURPLE_API_PRODUCTION_BASE_URL: URL = URL(string: "https://purple.damus.io")! - static let PURPLE_API_TEST_BASE_URL: URL = URL(string: "http://127.0.0.1:8989")! + //static let EXAMPLE_DEMOS: DamusState = .empty static let DEVICE_TOKEN_RECEIVER_PRODUCTION_URL: URL = URL(string: "https://notify.damus.io:8000/user-info")! static let DEVICE_TOKEN_RECEIVER_TEST_URL: URL = URL(string: "http://localhost:8000/user-info")! - static let EXAMPLE_DEMOS: DamusState = .empty + static let MAIN_APP_BUNDLE_IDENTIFIER: String = "com.jb55.damus2" + static let NOTIFICATION_EXTENSION_BUNDLE_IDENTIFIER: String = "com.jb55.damus2.DamusNotificationService" + static let PURPLE_API_PRODUCTION_BASE_URL: URL = URL(string: "https://purple.damus.io")! + static let PURPLE_API_TEST_BASE_URL: URL = URL(string: "http://127.0.0.1:8989")! } diff --git a/damus/Util/LNUrls.swift b/damus/Util/LNUrls.swift index 9d00b1a1..c60a4a1e 100644 --- a/damus/Util/LNUrls.swift +++ b/damus/Util/LNUrls.swift @@ -61,3 +61,36 @@ class LNUrls { return self.endpoints[pubkey] ?? .not_fetched } } + +func fetch_static_payreq(_ lnurl: String) async -> LNUrlPayRequest? { + print("fetching static payreq \(lnurl)") + + guard let url = decode_lnurl(lnurl) else { + return nil + } + + guard let ret = try? await URLSession.shared.data(from: url) else { + return nil + } + + let json_str = String(decoding: ret.0, as: UTF8.self) + + guard let endpoint: LNUrlPayRequest = decode_json(json_str) else { + return nil + } + + return endpoint +} + +func decode_lnurl(_ lnurl: String) -> URL? { + guard let decoded = try? bech32_decode(lnurl) else { + return nil + } + guard decoded.hrp == "lnurl" else { + return nil + } + guard let url = URL(string: String(decoding: decoded.data, as: UTF8.self)) else { + return nil + } + return url +} diff --git a/damus/Util/SequenceUtils.swift b/damus/Util/SequenceUtils.swift new file mode 100644 index 00000000..004763e1 --- /dev/null +++ b/damus/Util/SequenceUtils.swift @@ -0,0 +1,23 @@ +// +// SequenceUtils.swift +// damus +// +// Created by Daniel D’Aquino on 2023-11-24. +// + +import Foundation + +extension Sequence { + func just_one() -> Element? { + var got_one = false + var the_x: Element? = nil + for x in self { + guard !got_one else { + return nil + } + the_x = x + got_one = true + } + return the_x + } +} diff --git a/damus/Util/Zap.swift b/damus/Util/Zap.swift index 931810a9..bb57d7ff 100644 --- a/damus/Util/Zap.swift +++ b/damus/Util/Zap.swift @@ -434,39 +434,6 @@ func fetch_zapper_from_lnurl(lnurls: LNUrls, pubkey: Pubkey, lnurl: String) asyn return pk } -func decode_lnurl(_ lnurl: String) -> URL? { - guard let decoded = try? bech32_decode(lnurl) else { - return nil - } - guard decoded.hrp == "lnurl" else { - return nil - } - guard let url = URL(string: String(decoding: decoded.data, as: UTF8.self)) else { - return nil - } - return url -} - -func fetch_static_payreq(_ lnurl: String) async -> LNUrlPayRequest? { - print("fetching static payreq \(lnurl)") - - guard let url = decode_lnurl(lnurl) else { - return nil - } - - guard let ret = try? await URLSession.shared.data(from: url) else { - return nil - } - - let json_str = String(decoding: ret.0, as: UTF8.self) - - guard let endpoint: LNUrlPayRequest = decode_json(json_str) else { - return nil - } - - return endpoint -} - func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent?, msats: Int64, zap_type: ZapType, comment: String?) async -> String? { guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else { return nil diff --git a/damus/Views/AttachMediaUtility.swift b/damus/Views/AttachMediaUtility.swift index 88400b28..ecf28cdd 100644 --- a/damus/Views/AttachMediaUtility.swift +++ b/damus/Views/AttachMediaUtility.swift @@ -92,112 +92,3 @@ extension NSMutableData { append(data) } } - -enum MediaUploader: String, CaseIterable, Identifiable, StringCodable { - var id: String { self.rawValue } - case nostrBuild - case nostrImg - - init?(from string: String) { - guard let mu = MediaUploader(rawValue: string) else { - return nil - } - - self = mu - } - - func to_string() -> String { - return rawValue - } - - var nameParam: String { - switch self { - case .nostrBuild: - return "\"fileToUpload\"" - case .nostrImg: - return "\"image\"" - } - } - - var supportsVideo: Bool { - switch self { - case .nostrBuild: - return true - case .nostrImg: - return false - } - } - - struct Model: Identifiable, Hashable { - var id: String { self.tag } - var index: Int - var tag: String - var displayName : String - } - - var model: Model { - switch self { - case .nostrBuild: - return .init(index: -1, tag: "nostrBuild", displayName: "nostr.build") - case .nostrImg: - return .init(index: 0, tag: "nostrImg", displayName: "nostrimg.com") - } - } - - - var postAPI: String { - switch self { - case .nostrBuild: - return "https://nostr.build/api/v2/upload/files" - case .nostrImg: - return "https://nostrimg.com/api/upload" - } - } - - func getMediaURL(from data: Data) -> String? { - switch self { - case .nostrBuild: - do { - if let jsonObject = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any], - let status = jsonObject["status"] as? String { - - if status == "success", let dataArray = jsonObject["data"] as? [[String: Any]] { - - var urls: [String] = [] - - for dataDict in dataArray { - if let mainUrl = dataDict["url"] as? String { - urls.append(mainUrl) - } - } - - return urls.joined(separator: "\n") - } else if status == "error", let message = jsonObject["message"] as? String { - print("Upload Error: \(message)") - return nil - } - } - } catch { - print("Failed JSONSerialization") - return nil - } - return nil - case .nostrImg: - guard let responseString = String(data: data, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue)) else { - print("Upload failed getting response string") - return nil - } - - guard let startIndex = responseString.range(of: "https://i.nostrimg.com/")?.lowerBound else { - return nil - } - let stringContainingName = responseString[startIndex..(scroller: ScrollViewProxy, id: ID, delay: Dou } } } - -extension Collection { - - /// Returns the element at the specified index if it is within bounds, otherwise nil. - subscript (safe index: Index) -> Element? { - return indices.contains(index) ? self[index] : nil - } -} diff --git a/damus/Views/Events/Longform/LongformView.swift b/damus/Views/Events/Longform/LongformView.swift index 4cd8b909..639371e2 100644 --- a/damus/Views/Events/Longform/LongformView.swift +++ b/damus/Views/Events/Longform/LongformView.swift @@ -7,34 +7,6 @@ import SwiftUI -struct LongformEvent { - let event: NostrEvent - - var title: String? = nil - var image: URL? = nil - var summary: String? = nil - var published_at: Date? = nil - - static func parse(from ev: NostrEvent) -> LongformEvent { - var longform = LongformEvent(event: ev) - - for tag in ev.tags { - guard tag.count >= 2 else { continue } - switch tag[0].string() { - case "title": longform.title = tag[1].string() - case "image": longform.image = URL(string: tag[1].string()) - case "summary": longform.summary = tag[1].string() - case "published_at": - longform.published_at = Double(tag[1].string()).map { d in Date(timeIntervalSince1970: d) } - default: - break - } - } - - return longform - } -} - struct LongformView: View { let state: DamusState let event: LongformEvent diff --git a/damus/Views/NoteContentView.swift b/damus/Views/NoteContentView.swift index eacd8d1b..47fceb7f 100644 --- a/damus/Views/NoteContentView.swift +++ b/damus/Views/NoteContentView.swift @@ -300,70 +300,13 @@ struct NoteContentView: View { } -func attributed_string_attach_icon(_ astr: inout AttributedString, img: UIImage) { - let wrapped = icon_attributed_string(img: img) - astr.append(wrapped) -} - -func icon_attributed_string(img: UIImage) -> AttributedString { - let attachment = NSTextAttachment() - attachment.image = img - let attachmentString = NSAttributedString(attachment: attachment) - return AttributedString(attachmentString) -} - -func url_str(_ url: URL) -> CompatibleText { - var attributedString = AttributedString(stringLiteral: url.absoluteString) - attributedString.link = url - attributedString.foregroundColor = DamusColors.purple +class NoteArtifactsParts { + var parts: [ArtifactPart] + var words: Int - return CompatibleText(attributed: attributedString) - } - -func mention_str(_ m: Mention, profiles: Profiles) -> CompatibleText { - switch m.ref { - case .pubkey(let pk): - let npub = bech32_pubkey(pk) - let profile_txn = profiles.lookup(id: pk) - let profile = profile_txn.unsafeUnownedValue - let disp = Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 50) - var attributedString = AttributedString(stringLiteral: "@\(disp)") - attributedString.link = URL(string: "damus:nostr:\(npub)") - attributedString.foregroundColor = DamusColors.purple - - return CompatibleText(attributed: attributedString) - case .note(let note_id): - let bevid = bech32_note_id(note_id) - var attributedString = AttributedString(stringLiteral: "@\(abbrev_pubkey(bevid))") - attributedString.link = URL(string: "damus:nostr:\(bevid)") - attributedString.foregroundColor = DamusColors.purple - - return CompatibleText(attributed: attributedString) - } -} - -struct LongformContent { - let markdown: MarkdownContent - let words: Int - - init(_ markdown: String) { - let blocks = [BlockNode].init(markdown: markdown) - self.markdown = MarkdownContent(blocks: blocks) - self.words = count_markdown_words(blocks: blocks) - } -} - -enum NoteArtifacts { - case separated(NoteArtifactsSeparated) - case longform(LongformContent) - - var images: [URL] { - switch self { - case .separated(let arts): - return arts.images - case .longform: - return [] - } + init(parts: [ArtifactPart], words: Int) { + self.parts = parts + self.words = words } } @@ -381,83 +324,6 @@ enum ArtifactPart { } } -class NoteArtifactsParts { - var parts: [ArtifactPart] - var words: Int - - init(parts: [ArtifactPart], words: Int) { - self.parts = parts - self.words = words - } -} - -struct NoteArtifactsSeparated: Equatable { - static func == (lhs: NoteArtifactsSeparated, rhs: NoteArtifactsSeparated) -> Bool { - return lhs.content == rhs.content - } - - let content: CompatibleText - let words: Int - let urls: [UrlType] - let invoices: [Invoice] - - var media: [MediaUrl] { - return urls.compactMap { url in url.is_media } - } - - var images: [URL] { - return urls.compactMap { url in url.is_img } - } - - var links: [URL] { - return urls.compactMap { url in url.is_link } - } - - static func just_content(_ content: String) -> NoteArtifactsSeparated { - let txt = CompatibleText(attributed: AttributedString(stringLiteral: content)) - return NoteArtifactsSeparated(content: txt, words: 0, urls: [], invoices: []) - } -} - -enum NoteArtifactState { - case not_loaded - case loading - case loaded(NoteArtifacts) - - var artifacts: NoteArtifacts? { - if case .loaded(let artifacts) = self { - return artifacts - } - - return nil - } - - var should_preload: Bool { - switch self { - case .loaded: - return false - case .loading: - return false - case .not_loaded: - return true - } - } -} - -func note_artifact_is_separated(kind: NostrKind?) -> Bool { - return kind != .longform -} - -func render_note_content(ev: NostrEvent, profiles: Profiles, keypair: Keypair) -> NoteArtifacts { - let blocks = ev.blocks(keypair) - - if ev.known_kind == .longform { - return .longform(LongformContent(ev.content)) - } - - return .separated(render_blocks(blocks: blocks, profiles: profiles)) -} - fileprivate func artifact_part_last_text_ind(parts: [ArtifactPart]) -> (Int, Text)? { let ind = parts.count - 1 if ind < 0 { @@ -471,175 +337,6 @@ fileprivate func artifact_part_last_text_ind(parts: [ArtifactPart]) -> (Int, Tex return (ind, txt) } -func reduce_text_block(blocks: [Block], ind: Int, txt: String, one_note_ref: Bool) -> 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) - } - - 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) - } - } - - return trimmed -} - -func render_blocks(blocks bs: Blocks, profiles: Profiles) -> 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 - } - else { - return false - } - }) - .count == 1 - - var ind: Int = -1 - let txt: CompatibleText = blocks.reduce(CompatibleText()) { str, block in - ind = ind + 1 - - switch block { - case .mention(let m): - if case .note = m.ref, one_note_ref { - return str - } - 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)) - - 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 - 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 NoteArtifactsSeparated(content: txt, words: bs.words, urls: urls, invoices: invoices) -} - -enum MediaUrl { - case image(URL) - case video(URL) - - var url: URL { - switch self { - case .image(let url): - return url - case .video(let url): - return url - } - } -} - -enum UrlType { - case media(MediaUrl) - case link(URL) - - var url: URL { - switch self { - case .media(let media_url): - switch media_url { - case .image(let url): - return url - case .video(let url): - return url - } - case .link(let url): - return url - } - } - - var is_video: URL? { - switch self { - case .media(let media_url): - switch media_url { - case .image: - return nil - case .video(let url): - return url - } - case .link: - return nil - } - } - - var is_img: URL? { - switch self { - case .media(let media_url): - switch media_url { - case .image(let url): - return url - case .video: - return nil - } - case .link: - return nil - } - } - - var is_link: URL? { - switch self { - case .media: - return nil - case .link(let url): - return url - } - } - - var is_media: MediaUrl? { - switch self { - case .media(let murl): - return murl - case .link: - return nil - } - } -} - -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") { - return .media(.image(url)) - } - - if str.hasSuffix(".mp4") || str.hasSuffix(".mov") || str.hasSuffix(".m3u8") { - return .media(.video(url)) - } - - return .link(url) -} - func lookup_cached_preview_size(previews: PreviewCache, evid: NoteId) -> CGFloat? { guard case .value(let cached) = previews.lookup(evid) else { return nil @@ -652,16 +349,6 @@ func lookup_cached_preview_size(previews: PreviewCache, evid: NoteId) -> CGFloat return height } -// trim suffix whitespace and newlines -func trim_suffix(_ str: String) -> String { - return str.replacingOccurrences(of: "\\s+$", with: "", options: .regularExpression) -} - -// trim prefix whitespace and newlines -func trim_prefix(_ str: String) -> String { - return str.replacingOccurrences(of: "^\\s+", with: "", options: .regularExpression) -} - struct NoteContentView_Previews: PreviewProvider { static var previews: some View { let state = test_damus_state @@ -687,39 +374,6 @@ struct NoteContentView_Previews: PreviewProvider { } } - -func count_words(_ s: String) -> Int { - return s.components(separatedBy: .whitespacesAndNewlines).count -} - -func count_inline_nodes_words(nodes: [InlineNode]) -> Int { - return nodes.reduce(0) { words, node in - switch node { - case .text(let words): - return count_words(words) - case .emphasis(let children): - return words + count_inline_nodes_words(nodes: children) - case .strong(let children): - return words + count_inline_nodes_words(nodes: children) - case .strikethrough(let children): - return words + count_inline_nodes_words(nodes: children) - case .softBreak, .lineBreak, .code, .html, .image, .link: - return words - } - } -} - -func count_markdown_words(blocks: [BlockNode]) -> Int { - return blocks.reduce(0) { words, block in - switch block { - case .paragraph(let content): - return words + count_inline_nodes_words(nodes: content) - case .blockquote, .bulletedList, .numberedList, .taskList, .codeBlock, .htmlBlock, .heading, .table, .thematicBreak: - return words - } - } -} - func separate_images(ev: NostrEvent, keypair: Keypair) -> [MediaUrl]? { let urlBlocks: [URL] = ev.blocks(keypair).blocks.reduce(into: []) { urls, block in guard case .url(let url) = block else { diff --git a/damus/Views/Notifications/EventGroupView.swift b/damus/Views/Notifications/EventGroupView.swift index 418793a3..37fb9c7f 100644 --- a/damus/Views/Notifications/EventGroupView.swift +++ b/damus/Views/Notifications/EventGroupView.swift @@ -68,12 +68,6 @@ func determine_reacting_to(our_pubkey: Pubkey, ev: NostrEvent?) -> ReactingTo { return .tagged_in } -func event_author_name(profiles: Profiles, pubkey: Pubkey) -> String { - return profiles.lookup(id: pubkey).map({ profile in - Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50) - }).value -} - func event_group_unique_pubkeys(profiles: Profiles, group: EventGroupType) -> [Pubkey] { var seen = Set() var sorted = [Pubkey]() diff --git a/damus/Views/Notifications/NotificationsView.swift b/damus/Views/Notifications/NotificationsView.swift index 9328ba95..b68d78b8 100644 --- a/damus/Views/Notifications/NotificationsView.swift +++ b/damus/Views/Notifications/NotificationsView.swift @@ -7,32 +7,6 @@ import SwiftUI -enum FriendFilter: String, StringCodable { - case all - case friends - - init?(from string: String) { - guard let ff = FriendFilter(rawValue: string) else { - return nil - } - - self = ff - } - - func to_string() -> String { - self.rawValue - } - - func filter(contacts: Contacts, pubkey: Pubkey) -> Bool { - switch self { - case .all: - return true - case .friends: - return contacts.is_friend_or_self(pubkey) - } - } -} - class NotificationFilter: ObservableObject, Equatable { @Published var state: NotificationFilterState @Published var fine_filter: FriendFilter diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift index ac9adbef..0b9e1a44 100644 --- a/damus/Views/Profile/ProfileView.swift +++ b/damus/Views/Profile/ProfileView.swift @@ -7,13 +7,6 @@ import SwiftUI -enum FollowState { - case follows - case following - case unfollowing - case unfollows -} - func follow_btn_txt(_ fs: FollowState, follows_you: Bool) -> String { switch fs { case .follows: diff --git a/damus/Views/Settings/ReactionsSettingsView.swift b/damus/Views/Settings/ReactionsSettingsView.swift index 232a55bf..6af9aaa6 100644 --- a/damus/Views/Settings/ReactionsSettingsView.swift +++ b/damus/Views/Settings/ReactionsSettingsView.swift @@ -8,8 +8,6 @@ import SwiftUI import Combine -let default_emoji_reactions = ["🤣", "🤙", "⚡", "💜", "🔥", "😀", "😃", "😄", "🥶"] - struct ReactionsSettingsView: View { @ObservedObject var settings: UserSettingsStore diff --git a/damus/Views/Zaps/ZapTypePicker.swift b/damus/Views/Zaps/ZapTypePicker.swift index 03699019..a9416d4d 100644 --- a/damus/Views/Zaps/ZapTypePicker.swift +++ b/damus/Views/Zaps/ZapTypePicker.swift @@ -7,26 +7,6 @@ import SwiftUI -enum ZapType: String, StringCodable { - case pub - case anon - case priv - case non_zap - - init?(from string: String) { - guard let v = ZapType(rawValue: string) else { - return nil - } - - self = v - } - - func to_string() -> String { - return self.rawValue - } - -} - struct ZapTypePicker: View { @Binding var zap_type: ZapType @ObservedObject var settings: UserSettingsStore diff --git a/nostrdb/NdbNote+.swift b/nostrdb/NdbNote+.swift index 7adbec77..ea925f91 100644 --- a/nostrdb/NdbNote+.swift +++ b/nostrdb/NdbNote+.swift @@ -9,12 +9,6 @@ import Foundation // Extension to make NdbNote compatible with NostrEvent's original API extension NdbNote { - private var inner_event: NdbNote? { - get { - return NdbNote.owned_from_json_cstr(json: content_raw, json_len: content_len) - } - } - func get_inner_event(cache: EventCache) -> NdbNote? { guard self.known_kind == .boost else { return nil @@ -25,6 +19,6 @@ extension NdbNote { return cache.lookup(id) } - return self.inner_event + return self.get_inner_event() } } diff --git a/nostrdb/NdbNote.swift b/nostrdb/NdbNote.swift index f7deaa9b..857e03b5 100644 --- a/nostrdb/NdbNote.swift +++ b/nostrdb/NdbNote.swift @@ -48,6 +48,12 @@ class NdbNote: Encodable, Equatable, Hashable { // cached stuff (TODO: remove these) var decrypted_content: String? = nil + + private var inner_event: NdbNote? { + get { + return NdbNote.owned_from_json_cstr(json: content_raw, json_len: content_len) + } + } init(note: UnsafeMutablePointer, size: Int, owned: Bool, key: NoteKey?) { self.note = note @@ -262,6 +268,10 @@ class NdbNote: Encodable, Equatable, Hashable { return NdbNote(note: new_note, size: Int(len), owned: true, key: nil) } + + func get_inner_event() -> NdbNote? { + return self.inner_event + } } // Extension to make NdbNote compatible with NostrEvent's original API