damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit 88f938d11c6141b0cb318c89987c3d64c10e4be6
parent 4171252b18eff75877135fdf363ea1b5c0f28de6
Author: Daniel D’Aquino <daniel@daquino.me>
Date:   Fri,  1 Dec 2023 21:26:06 +0000

Bring local notification logic into the push notification target

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 <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 132+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus.xcodeproj/xcshareddata/xcschemes/DamusNotificationService.xcscheme | 2+-
Adamus/Models/Contacts+.swift | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Models/Contacts.swift | 142-------------------------------------------------------------------------------
Adamus/Models/FollowState.swift | 15+++++++++++++++
Adamus/Models/FriendFilter.swift | 34++++++++++++++++++++++++++++++++++
Mdamus/Models/HomeModel.swift | 138++++++-------------------------------------------------------------------------
Adamus/Models/LongformEvent.swift | 36++++++++++++++++++++++++++++++++++++
Adamus/Models/MediaUploader.swift | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus/Models/NewEventsBits.swift | 23+++++++++++++++++++++++
Adamus/Models/NoteContent.swift | 351+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Adamus/Models/NotificationsManager.swift | 125+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Models/UserSettingsStore.swift | 1+
Adamus/Models/ZapType.swift | 28++++++++++++++++++++++++++++
Adamus/Util/CollectionExtension.swift | 15+++++++++++++++
Mdamus/Util/CompatibleAttribute.swift | 10++++++++++
Mdamus/Util/Constants.swift | 8+++++---
Mdamus/Util/LNUrls.swift | 33+++++++++++++++++++++++++++++++++
Adamus/Util/SequenceUtils.swift | 23+++++++++++++++++++++++
Mdamus/Util/Zap.swift | 33---------------------------------
Mdamus/Views/AttachMediaUtility.swift | 109-------------------------------------------------------------------------------
Mdamus/Views/EventDetailView.swift | 8--------
Mdamus/Views/Events/Longform/LongformView.swift | 28----------------------------
Mdamus/Views/NoteContentView.swift | 358++-----------------------------------------------------------------------------
Mdamus/Views/Notifications/EventGroupView.swift | 6------
Mdamus/Views/Notifications/NotificationsView.swift | 26--------------------------
Mdamus/Views/Profile/ProfileView.swift | 7-------
Mdamus/Views/Settings/ReactionsSettingsView.swift | 2--
Mdamus/Views/Zaps/ZapTypePicker.swift | 20--------------------
Mnostrdb/NdbNote+.swift | 8+-------
Mnostrdb/NdbNote.swift | 10++++++++++
31 files changed, 1129 insertions(+), 872 deletions(-)

diff --git 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 = "<group>"; }; D79C4C1C2AFEB061003A41B4 /* DamusNotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DamusNotificationService.entitlements; sourceTree = "<group>"; }; D7C6787D2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP98AuthenticatedRequest.swift; sourceTree = "<group>"; }; + D7CB5D3D2B116DAD00AD4105 /* NotificationsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsManager.swift; sourceTree = "<group>"; }; + D7CB5D442B116FE800AD4105 /* Contacts+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Contacts+.swift"; sourceTree = "<group>"; }; + D7CB5D4A2B11721600AD4105 /* ZapType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapType.swift; sourceTree = "<group>"; }; + D7CB5D4D2B11728000AD4105 /* NewEventsBits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewEventsBits.swift; sourceTree = "<group>"; }; + D7CB5D502B1174D100AD4105 /* FriendFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FriendFilter.swift; sourceTree = "<group>"; }; + D7CB5D5B2B1176B200AD4105 /* MediaUploader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploader.swift; sourceTree = "<group>"; }; + D7CB5D5E2B11770C00AD4105 /* FollowState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowState.swift; sourceTree = "<group>"; }; D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = "<group>"; }; + D7EDED1B2B1178FE0018B19C /* NoteContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContent.swift; sourceTree = "<group>"; }; + D7EDED1D2B11797D0018B19C /* LongformEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongformEvent.swift; sourceTree = "<group>"; }; + D7EDED202B117DCA0018B19C /* SequenceUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SequenceUtils.swift; sourceTree = "<group>"; }; + D7EDED2D2B128E8A0018B19C /* CollectionExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtension.swift; sourceTree = "<group>"; }; D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayURL.swift; sourceTree = "<group>"; }; E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = "<group>"; }; E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; }; @@ -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 = "<group>"; @@ -1998,6 +2066,8 @@ D2277EE92A089BD5006C3807 /* Router.swift */, 4C2B10272A7B0F5C008AA43E /* Log.swift */, 50C3E0892AA8E3F7006A4BC0 /* AVPlayer+Additions.swift */, + D7EDED202B117DCA0018B19C /* SequenceUtils.swift */, + D7EDED2D2B128E8A0018B19C /* CollectionExtension.swift */, ); path = Util; sourceTree = "<group>"; @@ -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 @@ -59,7 +59,7 @@ <RemoteRunnable runnableDebuggingMode = "1" BundleIdentifier = "com.jb55.damus2" - RemotePath = "/Users/danielnogueira/Library/Developer/CoreSimulator/Devices/99E60B35-CE5D-4B45-AC35-00818C0AF3CB/data/Containers/Bundle/Application/5A083DD0-FDE2-43D7-9172-2F97FAD18F20/damus.app"> + RemotePath = "/Users/danielnogueira/Library/Developer/CoreSimulator/Devices/99E60B35-CE5D-4B45-AC35-00818C0AF3CB/data/Containers/Bundle/Application/12BC3574-F80A-4852-869A-0D826412B040/damus.app"> </RemoteRunnable> <MacroExpansion> <BuildableReference diff --git a/damus/Models/Contacts+.swift b/damus/Models/Contacts+.swift @@ -0,0 +1,153 @@ +// +// Contacts+.swift +// damus +// +// Extra functionality and utilities for `Contacts.swift` +// +// Created by Daniel D’Aquino on 2023-11-24. +// + +import Foundation + +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 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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..<responseString.endIndex] + guard let endIndex = stringContainingName.range(of: "\"")?.lowerBound else { + return nil + } + let nostrBuildImageName = responseString[startIndex..<endIndex] + let nostrBuildURL = "\(nostrBuildImageName)" + return nostrBuildURL + } + } +} diff --git a/damus/Models/NewEventsBits.swift b/damus/Models/NewEventsBits.swift @@ -0,0 +1,23 @@ +// +// NewEventsBits.swift +// damus +// +// Created by Daniel D’Aquino on 2023-11-24. +// + +import Foundation + +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] +} diff --git a/damus/Models/NoteContent.swift b/damus/Models/NoteContent.swift @@ -0,0 +1,351 @@ +// +// NoteContent.swift +// damus +// +// Created by Daniel D’Aquino on 2023-11-24. +// + +import Foundation +import MarkdownUI +import UIKit + +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)) +} + +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<MentionRef>, 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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..<responseString.endIndex] - guard let endIndex = stringContainingName.range(of: "\"")?.lowerBound else { - return nil - } - let nostrBuildImageName = responseString[startIndex..<endIndex] - let nostrBuildURL = "\(nostrBuildImageName)" - return nostrBuildURL - } - } -} diff --git a/damus/Views/EventDetailView.swift b/damus/Views/EventDetailView.swift @@ -32,11 +32,3 @@ func scroll_to_event<ID: Hashable>(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 @@ -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 @@ -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<MentionRef>, 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 @@ -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<Pubkey>() var sorted = [Pubkey]() diff --git 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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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<ndb_note>, 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