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