Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
cf63fdc247
|
@@ -28,7 +28,7 @@ struct NotificationExtensionState: HeadlessDamusState {
|
||||
self.settings = UserSettingsStore()
|
||||
|
||||
self.contacts = Contacts(our_pubkey: keypair.pubkey)
|
||||
self.mutelist_manager = MutelistManager(user_keypair: keypair)
|
||||
self.mutelist_manager = MutelistManager()
|
||||
self.keypair = keypair
|
||||
self.profiles = Profiles(ndb: ndb)
|
||||
self.zaps = Zaps(our_pubkey: keypair.pubkey)
|
||||
|
||||
@@ -40,32 +40,15 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
return
|
||||
}
|
||||
|
||||
// Don't show notification details that match mute list.
|
||||
// TODO: Remove this code block once we get notification suppression entitlement from Apple. It will be covered by the `guard should_display_notification` block
|
||||
if state.mutelist_manager.is_event_muted(nostr_event) {
|
||||
// We cannot really suppress muted notifications until we have the notification supression entitlement.
|
||||
// The best we can do if we ever get those muted notifications (which we generally won't due to server-side processing) is to obscure the details
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = NSLocalizedString("Muted event", comment: "Title for a push notification which has been muted")
|
||||
content.body = NSLocalizedString("This is an event that has been muted according to your mute list rules. We cannot suppress this notification, but we obscured the details to respect your preferences", comment: "Description for a push notification which has been muted, and explanation that we cannot suppress it")
|
||||
content.sound = UNNotificationSound.default
|
||||
contentHandler(content)
|
||||
return
|
||||
}
|
||||
|
||||
guard should_display_notification(state: state, event: nostr_event, mode: .push) else {
|
||||
guard should_display_notification(state: state, event: nostr_event) else {
|
||||
// We should not display notification for this event. Suppress notification.
|
||||
// contentHandler(UNNotificationContent())
|
||||
// TODO: We cannot really suppress until we have the notification supression entitlement. Show the raw notification
|
||||
contentHandler(request.content)
|
||||
contentHandler(UNNotificationContent())
|
||||
return
|
||||
}
|
||||
|
||||
guard let notification_object = generate_local_notification_object(from: nostr_event, state: state) else {
|
||||
// We could not process this notification. Probably an unsupported nostr event kind. Suppress.
|
||||
// contentHandler(UNNotificationContent())
|
||||
// TODO: We cannot really suppress until we have the notification supression entitlement. Show the raw notification
|
||||
contentHandler(request.content)
|
||||
contentHandler(UNNotificationContent())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>1C8F.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>1C8F.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>C617.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
+117
-226
@@ -12,7 +12,6 @@
|
||||
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */; };
|
||||
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.swift */; };
|
||||
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; };
|
||||
3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 3A0A30BA2C21397A00F8C9BC /* EmojiPicker */; };
|
||||
3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */; };
|
||||
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */; };
|
||||
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */; };
|
||||
@@ -21,7 +20,11 @@
|
||||
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; };
|
||||
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */; };
|
||||
3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; };
|
||||
3A5E47C52A4A6CF400C0D090 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5E47C42A4A6CF400C0D090 /* Trie.swift */; };
|
||||
3A5E47C72A4A76C800C0D090 /* TrieTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5E47C62A4A76C800C0D090 /* TrieTests.swift */; };
|
||||
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; };
|
||||
3A90B1812A4EA3AF00000D94 /* UserSearchCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A90B1802A4EA3AF00000D94 /* UserSearchCache.swift */; };
|
||||
3A90B1832A4EA3C600000D94 /* UserSearchCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A90B1822A4EA3C600000D94 /* UserSearchCacheTests.swift */; };
|
||||
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
|
||||
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
|
||||
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA59D1C2999B0400061C48E /* DraftsModel.swift */; };
|
||||
@@ -33,10 +36,8 @@
|
||||
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685D297633BC00C46468 /* Localizable.strings */; };
|
||||
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; };
|
||||
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; };
|
||||
3AFE89C32BD4156F00AD31EF /* MCEmojiPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 3AFE89C22BD4156F00AD31EF /* MCEmojiPicker */; };
|
||||
3CCD1E6A2A874C4E0099A953 /* Nip98HTTPAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCD1E692A874C4E0099A953 /* Nip98HTTPAuth.swift */; };
|
||||
4C011B5E2BD0A56A002F2F9B /* ChatEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C011B5C2BD0A56A002F2F9B /* ChatEventView.swift */; };
|
||||
4C011B5F2BD0A56A002F2F9B /* ChatroomThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C011B5D2BD0A56A002F2F9B /* ChatroomThreadView.swift */; };
|
||||
4C011B612BD0B25C002F2F9B /* ReplyQuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C011B602BD0B25C002F2F9B /* ReplyQuoteView.swift */; };
|
||||
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670028FC7C5900038D2A /* RelayView.swift */; };
|
||||
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 4C06670328FC7EC500038D2A /* Kingfisher */; };
|
||||
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C06670528FCB08600038D2A /* ImageCarousel.swift */; };
|
||||
@@ -97,7 +98,6 @@
|
||||
4C2B10282A7B0F5C008AA43E /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2B10272A7B0F5C008AA43E /* Log.swift */; };
|
||||
4C2B7BF22A71B6540049DEE7 /* Id.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2B7BF12A71B6540049DEE7 /* Id.swift */; };
|
||||
4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */; };
|
||||
4C2D34412BDAF1B300F9FB44 /* NIP10Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2D34402BDAF1B300F9FB44 /* NIP10Tests.swift */; };
|
||||
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */; };
|
||||
4C30AC7429A5680900E2BD5A /* EventGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7329A5680900E2BD5A /* EventGroupView.swift */; };
|
||||
4C30AC7629A5770900E2BD5A /* NotificationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */; };
|
||||
@@ -135,6 +135,7 @@
|
||||
4C363A94282704FA006E126D /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A93282704FA006E126D /* Post.swift */; };
|
||||
4C363A962827096D006E126D /* PostBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A952827096D006E126D /* PostBlock.swift */; };
|
||||
4C363A9A28283854006E126D /* Reply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A9928283854006E126D /* Reply.swift */; };
|
||||
4C363A9C282838B9006E126D /* EventRef.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A9B282838B9006E126D /* EventRef.swift */; };
|
||||
4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A9D2828A822006E126D /* ReplyTests.swift */; };
|
||||
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A9F2828A8DD006E126D /* LikeTests.swift */; };
|
||||
4C363AA228296A7E006E126D /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363AA128296A7E006E126D /* SearchView.swift */; };
|
||||
@@ -173,7 +174,6 @@
|
||||
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67C28FFBBA200C48A62 /* InvoicesView.swift */; };
|
||||
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */; };
|
||||
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C42812B298C848200DBF26F /* TranslateView.swift */; };
|
||||
4C45E5022BED4D000025A428 /* ThreadReply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C45E5012BED4D000025A428 /* ThreadReply.swift */; };
|
||||
4C463CBF2B960B96008A8C36 /* PurpleBackdrop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C463CBE2B960B96008A8C36 /* PurpleBackdrop.swift */; };
|
||||
4C4793012A993CDA00489948 /* mdb.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C4793002A993B9A00489948 /* mdb.c */; settings = {COMPILER_FLAGS = "-w"; }; };
|
||||
4C4793042A993DC000489948 /* midl.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C4793032A993DB900489948 /* midl.c */; settings = {COMPILER_FLAGS = "-w"; }; };
|
||||
@@ -247,7 +247,6 @@
|
||||
4C8D1A6C29F1DFC200ACDF75 /* FriendIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D1A6B29F1DFC200ACDF75 /* FriendIcon.swift */; };
|
||||
4C8D1A6F29F31E5000ACDF75 /* FriendsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */; };
|
||||
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */; };
|
||||
4C8FA7242BED58A900798A6A /* ThreadReply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C45E5012BED4D000025A428 /* ThreadReply.swift */; };
|
||||
4C9054852A6AEAA000811EEC /* NdbTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9054842A6AEAA000811EEC /* NdbTests.swift */; };
|
||||
4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD17283A9EE5008EE7EF /* LoginView.swift */; };
|
||||
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD19283AA67F008EE7EF /* Bech32.swift */; };
|
||||
@@ -283,6 +282,7 @@
|
||||
4CA927632A290EB10098A105 /* EventTop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA927622A290EB10098A105 /* EventTop.swift */; };
|
||||
4CA927652A290F1A0098A105 /* TimeDot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA927642A290F1A0098A105 /* TimeDot.swift */; };
|
||||
4CA927672A290F8B0098A105 /* RelativeTime.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA927662A290F8B0098A105 /* RelativeTime.swift */; };
|
||||
4CA9276A2A290FC00098A105 /* ContextButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA927692A290FC00098A105 /* ContextButton.swift */; };
|
||||
4CA9276C2A2910D10098A105 /* ReplyPart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA9276B2A2910D10098A105 /* ReplyPart.swift */; };
|
||||
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AC298851D000060CEA /* AccountDeletion.swift */; };
|
||||
4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */; };
|
||||
@@ -319,6 +319,7 @@
|
||||
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */; };
|
||||
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; };
|
||||
4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */; };
|
||||
4CCEB7B029B5415A0078AA28 /* SearchingProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */; };
|
||||
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; };
|
||||
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
|
||||
4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128929E9D10C0006FA5A /* SignalView.swift */; };
|
||||
@@ -399,7 +400,6 @@
|
||||
5C14C29D2BBBA40B00079FD2 /* RelayAdminDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29C2BBBA40B00079FD2 /* RelayAdminDetail.swift */; };
|
||||
5C14C29F2BBBA5C600079FD2 /* RelayNipList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29E2BBBA5C600079FD2 /* RelayNipList.swift */; };
|
||||
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; };
|
||||
5C4D9EA72C042FA5005EA0F7 /* HighlightPostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4D9EA62C042FA5005EA0F7 /* HighlightPostView.swift */; };
|
||||
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
|
||||
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
|
||||
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */; };
|
||||
@@ -407,11 +407,6 @@
|
||||
5C7389B12B6EFA7100781E0A /* ProxyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B02B6EFA7100781E0A /* ProxyView.swift */; };
|
||||
5C7389B72B9E692E00781E0A /* MutinyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B62B9E692E00781E0A /* MutinyButton.swift */; };
|
||||
5C7389B92B9E69ED00781E0A /* MutinyGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C7389B82B9E69ED00781E0A /* MutinyGradient.swift */; };
|
||||
5CC8529D2BD741CD0039FFC5 /* HighlightEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */; };
|
||||
5CC8529F2BD744F60039FFC5 /* HighlightView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC8529E2BD744F60039FFC5 /* HighlightView.swift */; };
|
||||
5CC852A22BDED9B90039FFC5 /* HighlightDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */; };
|
||||
5CC852A42BDF3CA10039FFC5 /* HighlightLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC852A32BDF3CA10039FFC5 /* HighlightLink.swift */; };
|
||||
5CC852A62BE00F180039FFC5 /* HighlightEventRef.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC852A52BE00F180039FFC5 /* HighlightEventRef.swift */; };
|
||||
5CC868DD2AA29B3200FB22BA /* NeutralButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CC868DC2AA29B3200FB22BA /* NeutralButtonStyle.swift */; };
|
||||
5CF2DCCC2AA3AF0B00984B8D /* RelayPicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF2DCCB2AA3AF0B00984B8D /* RelayPicView.swift */; };
|
||||
5CF2DCCE2AABE1A500984B8D /* DamusLightGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF2DCCD2AABE1A500984B8D /* DamusLightGradient.swift */; };
|
||||
@@ -469,8 +464,6 @@
|
||||
D72A2D022AD9C136002AFF62 /* EventViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */; };
|
||||
D72A2D052AD9C1B5002AFF62 /* MockDamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */; };
|
||||
D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */; };
|
||||
D72E12782BEED22500F4F781 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72E12772BEED22400F4F781 /* Array.swift */; };
|
||||
D72E127A2BEEEED000F4F781 /* NostrFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */; };
|
||||
D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; };
|
||||
D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; };
|
||||
D7373BA62B688EA300F7783D /* DamusPurpleTranslationSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7373BA52B688EA200F7783D /* DamusPurpleTranslationSetupView.swift */; };
|
||||
@@ -493,7 +486,6 @@
|
||||
D74AAFD62B155F0C006CF0F4 /* WalletConnect+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */; };
|
||||
D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F43092B23F0BE00425B75 /* DamusPurple.swift */; };
|
||||
D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; };
|
||||
D753CEAA2BE9DE04001C3A5D /* MutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D753CEA92BE9DE04001C3A5D /* MutingTests.swift */; };
|
||||
D76556D62B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */; };
|
||||
D76874F32AE3632B00FB0F68 /* ProfileZapLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */; };
|
||||
D77BFA0B2AE3051200621634 /* ProfileActionSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */; };
|
||||
@@ -503,9 +495,6 @@
|
||||
D7870BC32AC47EBC0080BA88 /* EventLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */; };
|
||||
D789D1202AFEFBF20083A7AB /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = D789D11F2AFEFBF20083A7AB /* secp256k1 */; };
|
||||
D78CD5982B8990300014D539 /* DamusAppNotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78CD5972B8990300014D539 /* DamusAppNotificationView.swift */; };
|
||||
D78DB8592C1CE9CA00F0AB12 /* SwipeActions in Frameworks */ = {isa = PBXBuildFile; productRef = D78DB8582C1CE9CA00F0AB12 /* SwipeActions */; };
|
||||
D78DB85B2C20FE5000F0AB12 /* VectorMath.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */; };
|
||||
D78DB85F2C20FED300F0AB12 /* ChatBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78DB85E2C20FED300F0AB12 /* ChatBubbleView.swift */; };
|
||||
D798D21A2B0856CC00234419 /* Mentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FF7D42823313F009601DB /* Mentions.swift */; };
|
||||
D798D21B2B0856F200234419 /* NdbTagsIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDD1AE12A6B3074001CD4DF /* NdbTagsIterator.swift */; };
|
||||
D798D21C2B0857E400234419 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; };
|
||||
@@ -564,6 +553,7 @@
|
||||
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"; }; };
|
||||
D7CCFC0E2B0587C300323D86 /* EventRef.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A9B282838B9006E126D /* EventRef.swift */; };
|
||||
D7CCFC0F2B0587F600323D86 /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8B28398BC6008A31F1 /* Keys.swift */; };
|
||||
D7CCFC102B05880F00323D86 /* Id.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2B7BF12A71B6540049DEE7 /* Id.swift */; };
|
||||
D7CCFC112B05884E00323D86 /* AsciiCharacter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5D5C9C2A6B2CB40024563C /* AsciiCharacter.swift */; };
|
||||
@@ -623,7 +613,6 @@
|
||||
D7CE1B472B0BE719002EDAD4 /* NativeObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B9462A9AD44700DC3548 /* NativeObject.swift */; };
|
||||
D7CE1B482B0BE719002EDAD4 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B93D2A9AD44700DC3548 /* Message.swift */; };
|
||||
D7CE1B492B0BE729002EDAD4 /* DisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */; };
|
||||
D7D2A3812BF815D000E4B42B /* PushNotificationClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D2A3802BF815D000E4B42B /* PushNotificationClient.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 */; };
|
||||
@@ -649,9 +638,6 @@
|
||||
D7EDED332B12ACAE0018B19C /* DamusUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EDED322B12ACAE0018B19C /* DamusUserDefaults.swift */; };
|
||||
D7EDED342B12ACAE0018B19C /* DamusUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EDED322B12ACAE0018B19C /* DamusUserDefaults.swift */; };
|
||||
D7FB10A72B0C371A00FA8D42 /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2B10272A7B0F5C008AA43E /* Log.swift */; };
|
||||
D7FB14222BE5970000398331 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D7FB14212BE5970000398331 /* PrivacyInfo.xcprivacy */; };
|
||||
D7FB14252BE5A9A800398331 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D7FB14242BE5A9A800398331 /* PrivacyInfo.xcprivacy */; };
|
||||
D7FD12262BD345A700CF195B /* FirstAidSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FD12252BD345A700CF195B /* FirstAidSettingsView.swift */; };
|
||||
D7FF94002AC7AC5300FD969D /* RelayURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */; };
|
||||
E02429952B7E97740088B16C /* CameraController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E02429942B7E97740088B16C /* CameraController.swift */; };
|
||||
E02B54182B4DFADA0077FF42 /* Bech32ObjectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E02B54172B4DFADA0077FF42 /* Bech32ObjectTests.swift */; };
|
||||
@@ -753,14 +739,13 @@
|
||||
3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CondensedProfilePicturesView.swift; sourceTree = "<group>"; };
|
||||
3A47CB772BDA05A200728A7C /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3A47CB782BDA05A200728A7C /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A47CB792BDA05A200728A7C /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fi; path = fi.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutedThreadsManager.swift; sourceTree = "<group>"; };
|
||||
3A5C4575296A879E0032D398 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-419"; path = "es-419.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A5CAE1E298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A5CAE1F298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-CN"; path = "zh-CN.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A5E47C42A4A6CF400C0D090 /* Trie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trie.swift; sourceTree = "<group>"; };
|
||||
3A5E47C62A4A76C800C0D090 /* TrieTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrieTests.swift; sourceTree = "<group>"; };
|
||||
3A66D927299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3A66D928299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A66D929299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
@@ -774,6 +759,8 @@
|
||||
3A8624DA299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A8624DB299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringUtil.swift; sourceTree = "<group>"; };
|
||||
3A90B1802A4EA3AF00000D94 /* UserSearchCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserSearchCache.swift; sourceTree = "<group>"; };
|
||||
3A90B1822A4EA3C600000D94 /* UserSearchCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSearchCacheTests.swift; sourceTree = "<group>"; };
|
||||
3A929C20297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A929C21297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A929C22297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "it-IT"; path = "it-IT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
@@ -783,9 +770,6 @@
|
||||
3A96D41A298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3A96D41B298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A96D41C298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3A994C4C2BE5B9370019F632 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = th; path = th.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3A994C4D2BE5B9370019F632 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3A994C4E2BE5B9370019F632 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3AA247FE297E3D900090C62D /* RepostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostsView.swift; sourceTree = "<group>"; };
|
||||
3AA24801297E3DC20090C62D /* RepostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostView.swift; sourceTree = "<group>"; };
|
||||
3AA59D1C2999B0400061C48E /* DraftsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsModel.swift; sourceTree = "<group>"; };
|
||||
@@ -834,9 +818,6 @@
|
||||
3AF6336929884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-PT"; path = "pt-PT.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3AF6336A29884C6B0005672A /* pt-PT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-PT"; path = "pt-PT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3CCD1E692A874C4E0099A953 /* Nip98HTTPAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nip98HTTPAuth.swift; sourceTree = "<group>"; };
|
||||
4C011B5C2BD0A56A002F2F9B /* ChatEventView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatEventView.swift; sourceTree = "<group>"; };
|
||||
4C011B5D2BD0A56A002F2F9B /* ChatroomThreadView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChatroomThreadView.swift; sourceTree = "<group>"; };
|
||||
4C011B602BD0B25C002F2F9B /* ReplyQuoteView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplyQuoteView.swift; sourceTree = "<group>"; };
|
||||
4C06670028FC7C5900038D2A /* RelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayView.swift; sourceTree = "<group>"; };
|
||||
4C06670528FCB08600038D2A /* ImageCarousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCarousel.swift; sourceTree = "<group>"; };
|
||||
4C06670828FDE64700038D2A /* damus-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "damus-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
@@ -900,7 +881,6 @@
|
||||
4C2B10272A7B0F5C008AA43E /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = "<group>"; };
|
||||
4C2B7BF12A71B6540049DEE7 /* Id.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Id.swift; sourceTree = "<group>"; };
|
||||
4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; };
|
||||
4C2D34402BDAF1B300F9FB44 /* NIP10Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP10Tests.swift; sourceTree = "<group>"; };
|
||||
4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
|
||||
4C30AC7329A5680900E2BD5A /* EventGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGroupView.swift; sourceTree = "<group>"; };
|
||||
4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemView.swift; sourceTree = "<group>"; };
|
||||
@@ -936,6 +916,7 @@
|
||||
4C363A93282704FA006E126D /* Post.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = "<group>"; };
|
||||
4C363A952827096D006E126D /* PostBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostBlock.swift; sourceTree = "<group>"; };
|
||||
4C363A9928283854006E126D /* Reply.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reply.swift; sourceTree = "<group>"; };
|
||||
4C363A9B282838B9006E126D /* EventRef.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventRef.swift; sourceTree = "<group>"; };
|
||||
4C363A9D2828A822006E126D /* ReplyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyTests.swift; sourceTree = "<group>"; };
|
||||
4C363A9F2828A8DD006E126D /* LikeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LikeTests.swift; sourceTree = "<group>"; };
|
||||
4C363AA128296A7E006E126D /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
|
||||
@@ -1003,7 +984,6 @@
|
||||
4C3EA67C28FFBBA200C48A62 /* InvoicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoicesView.swift; sourceTree = "<group>"; };
|
||||
4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvoiceView.swift; sourceTree = "<group>"; };
|
||||
4C42812B298C848200DBF26F /* TranslateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslateView.swift; sourceTree = "<group>"; };
|
||||
4C45E5012BED4D000025A428 /* ThreadReply.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadReply.swift; sourceTree = "<group>"; };
|
||||
4C463CBE2B960B96008A8C36 /* PurpleBackdrop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurpleBackdrop.swift; sourceTree = "<group>"; };
|
||||
4C478E242A9932C100489948 /* Ndb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ndb.swift; sourceTree = "<group>"; };
|
||||
4C478E262A99353500489948 /* threadpool.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = threadpool.h; sourceTree = "<group>"; };
|
||||
@@ -1212,6 +1192,7 @@
|
||||
4CA927622A290EB10098A105 /* EventTop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventTop.swift; sourceTree = "<group>"; };
|
||||
4CA927642A290F1A0098A105 /* TimeDot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeDot.swift; sourceTree = "<group>"; };
|
||||
4CA927662A290F8B0098A105 /* RelativeTime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelativeTime.swift; sourceTree = "<group>"; };
|
||||
4CA927692A290FC00098A105 /* ContextButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextButton.swift; sourceTree = "<group>"; };
|
||||
4CA9276B2A2910D10098A105 /* ReplyPart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyPart.swift; sourceTree = "<group>"; };
|
||||
4CA9276D2A2A5D110098A105 /* wasm.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = wasm.h; sourceTree = "<group>"; };
|
||||
4CA9276E2A2A5D110098A105 /* wasm.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = wasm.c; sourceTree = "<group>"; };
|
||||
@@ -1255,6 +1236,7 @@
|
||||
4CC7AAF7297F1CEE00430951 /* EventProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfile.swift; sourceTree = "<group>"; };
|
||||
4CC7AAF9297F64AC00430951 /* EventMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMenu.swift; sourceTree = "<group>"; };
|
||||
4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingEventView.swift; sourceTree = "<group>"; };
|
||||
4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingProfileView.swift; sourceTree = "<group>"; };
|
||||
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploadModel.swift; sourceTree = "<group>"; };
|
||||
4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; };
|
||||
4CDA128929E9D10C0006FA5A /* SignalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalView.swift; sourceTree = "<group>"; };
|
||||
@@ -1340,7 +1322,6 @@
|
||||
5C14C29C2BBBA40B00079FD2 /* RelayAdminDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayAdminDetail.swift; sourceTree = "<group>"; };
|
||||
5C14C29E2BBBA5C600079FD2 /* RelayNipList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayNipList.swift; sourceTree = "<group>"; };
|
||||
5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = "<group>"; };
|
||||
5C4D9EA62C042FA5005EA0F7 /* HighlightPostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightPostView.swift; sourceTree = "<group>"; };
|
||||
5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; };
|
||||
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
|
||||
5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientButtonStyle.swift; sourceTree = "<group>"; };
|
||||
@@ -1348,11 +1329,6 @@
|
||||
5C7389B02B6EFA7100781E0A /* ProxyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyView.swift; sourceTree = "<group>"; };
|
||||
5C7389B62B9E692E00781E0A /* MutinyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutinyButton.swift; sourceTree = "<group>"; };
|
||||
5C7389B82B9E69ED00781E0A /* MutinyGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutinyGradient.swift; sourceTree = "<group>"; };
|
||||
5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightEvent.swift; sourceTree = "<group>"; };
|
||||
5CC8529E2BD744F60039FFC5 /* HighlightView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightView.swift; sourceTree = "<group>"; };
|
||||
5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDescription.swift; sourceTree = "<group>"; };
|
||||
5CC852A32BDF3CA10039FFC5 /* HighlightLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightLink.swift; sourceTree = "<group>"; };
|
||||
5CC852A52BE00F180039FFC5 /* HighlightEventRef.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightEventRef.swift; sourceTree = "<group>"; };
|
||||
5CC868DC2AA29B3200FB22BA /* NeutralButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeutralButtonStyle.swift; sourceTree = "<group>"; };
|
||||
5CF2DCCB2AA3AF0B00984B8D /* RelayPicView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPicView.swift; sourceTree = "<group>"; };
|
||||
5CF2DCCD2AABE1A500984B8D /* DamusLightGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLightGradient.swift; sourceTree = "<group>"; };
|
||||
@@ -1407,8 +1383,6 @@
|
||||
D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventViewTests.swift; sourceTree = "<group>"; };
|
||||
D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDamusState.swift; sourceTree = "<group>"; };
|
||||
D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProfiles.swift; sourceTree = "<group>"; };
|
||||
D72E12772BEED22400F4F781 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; };
|
||||
D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrFilterTests.swift; sourceTree = "<group>"; };
|
||||
D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = "<group>"; };
|
||||
D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = "<group>"; };
|
||||
D7373BA52B688EA200F7783D /* DamusPurpleTranslationSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleTranslationSetupView.swift; sourceTree = "<group>"; };
|
||||
@@ -1422,7 +1396,6 @@
|
||||
D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WalletConnect+.swift"; sourceTree = "<group>"; };
|
||||
D74F43092B23F0BE00425B75 /* DamusPurple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurple.swift; sourceTree = "<group>"; };
|
||||
D74F430B2B23FB9B00425B75 /* StoreObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreObserver.swift; sourceTree = "<group>"; };
|
||||
D753CEA92BE9DE04001C3A5D /* MutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutingTests.swift; sourceTree = "<group>"; };
|
||||
D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleWelcomeView.swift; sourceTree = "<group>"; };
|
||||
D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZapLinkView.swift; sourceTree = "<group>"; };
|
||||
D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileActionSheetView.swift; sourceTree = "<group>"; };
|
||||
@@ -1431,8 +1404,6 @@
|
||||
D7870BC02AC4750B0080BA88 /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = "<group>"; };
|
||||
D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLoaderView.swift; sourceTree = "<group>"; };
|
||||
D78CD5972B8990300014D539 /* DamusAppNotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusAppNotificationView.swift; sourceTree = "<group>"; };
|
||||
D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VectorMath.swift; sourceTree = "<group>"; };
|
||||
D78DB85E2C20FED300F0AB12 /* ChatBubbleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatBubbleView.swift; sourceTree = "<group>"; };
|
||||
D798D21D2B0858BB00234419 /* MigratedTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigratedTypes.swift; sourceTree = "<group>"; };
|
||||
D798D2272B085CDA00234419 /* NdbNote+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NdbNote+.swift"; sourceTree = "<group>"; };
|
||||
D798D22B2B086C7400234419 /* NostrEvent+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NostrEvent+.swift"; sourceTree = "<group>"; };
|
||||
@@ -1453,16 +1424,12 @@
|
||||
D7CB5D5E2B11770C00AD4105 /* FollowState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowState.swift; sourceTree = "<group>"; };
|
||||
D7CBD1D32B8D21DC00BFD889 /* DamusPurpleNotificationManagement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleNotificationManagement.swift; sourceTree = "<group>"; };
|
||||
D7CBD1D52B8D509800BFD889 /* DamusPurpleImpendingExpirationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleImpendingExpirationTests.swift; sourceTree = "<group>"; };
|
||||
D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationClient.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>"; };
|
||||
D7EDED322B12ACAE0018B19C /* DamusUserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusUserDefaults.swift; sourceTree = "<group>"; };
|
||||
D7FB14212BE5970000398331 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
D7FB14242BE5A9A800398331 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
D7FD12252BD345A700CF195B /* FirstAidSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstAidSettingsView.swift; sourceTree = "<group>"; };
|
||||
D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayURL.swift; sourceTree = "<group>"; };
|
||||
E02429942B7E97740088B16C /* CameraController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraController.swift; sourceTree = "<group>"; };
|
||||
E02B54172B4DFADA0077FF42 /* Bech32ObjectTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Bech32ObjectTests.swift; sourceTree = "<group>"; };
|
||||
@@ -1497,10 +1464,9 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
|
||||
3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */,
|
||||
D78DB8592C1CE9CA00F0AB12 /* SwipeActions in Frameworks */,
|
||||
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */,
|
||||
4C27C9322A64766F007DBC75 /* MarkdownUI in Frameworks */,
|
||||
3AFE89C32BD4156F00AD31EF /* MCEmojiPicker in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -1637,6 +1603,7 @@
|
||||
4C363A93282704FA006E126D /* Post.swift */,
|
||||
4C363A952827096D006E126D /* PostBlock.swift */,
|
||||
4C363A9928283854006E126D /* Reply.swift */,
|
||||
4C363A9B282838B9006E126D /* EventRef.swift */,
|
||||
4C363AA328296DEE006E126D /* SearchModel.swift */,
|
||||
0E8A4BB62AE4359200065E81 /* NostrFilter+Hashable.swift */,
|
||||
4C3AC79A28306D7B00E1F516 /* Contacts.swift */,
|
||||
@@ -1664,6 +1631,8 @@
|
||||
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */,
|
||||
4C7D09772A0B0CC900943473 /* WalletModel.swift */,
|
||||
3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */,
|
||||
3A5E47C42A4A6CF400C0D090 /* Trie.swift */,
|
||||
3A90B1802A4EA3AF00000D94 /* UserSearchCache.swift */,
|
||||
D723C38D2AB8D83400065664 /* ContentFilters.swift */,
|
||||
D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */,
|
||||
D7CB5D3D2B116DAD00AD4105 /* NotificationsManager.swift */,
|
||||
@@ -1679,8 +1648,6 @@
|
||||
D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */,
|
||||
B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */,
|
||||
B533694D2B66D791008A805E /* MutelistManager.swift */,
|
||||
D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */,
|
||||
5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
@@ -1753,7 +1720,6 @@
|
||||
4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */,
|
||||
E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */,
|
||||
5053ACA62A56DF3B00851AE3 /* DeveloperSettingsView.swift */,
|
||||
D7FD12252BD345A700CF195B /* FirstAidSettingsView.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
@@ -1809,14 +1775,6 @@
|
||||
path = flatbuffers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C45E5002BED4CE10025A428 /* NIP10 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C45E5012BED4D000025A428 /* ThreadReply.swift */,
|
||||
);
|
||||
path = NIP10;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C478E2A2A9935D300489948 /* bindings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -2021,7 +1979,6 @@
|
||||
4C75EFA227FA576C0006080F /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D78DB85D2C20FE9E00F0AB12 /* Chat */,
|
||||
D71AC4CA2BA8E3320076268E /* Extensions */,
|
||||
BA3759952ABCCF360018D73B /* Camera */,
|
||||
F71694E82A66221E001F4053 /* Onboarding */,
|
||||
@@ -2327,6 +2284,7 @@
|
||||
4CA927622A290EB10098A105 /* EventTop.swift */,
|
||||
4CC7AAF3297F18B400430951 /* ReplyDescription.swift */,
|
||||
4CA927662A290F8B0098A105 /* RelativeTime.swift */,
|
||||
4CA927692A290FC00098A105 /* ContextButton.swift */,
|
||||
4CA9276B2A2910D10098A105 /* ReplyPart.swift */,
|
||||
5C7389B02B6EFA7100781E0A /* ProxyView.swift */,
|
||||
);
|
||||
@@ -2415,7 +2373,6 @@
|
||||
4CC7AAEE297F11B300430951 /* Events */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5CC852A02BDED9970039FFC5 /* Highlight */,
|
||||
4CA927682A290F8F0098A105 /* Components */,
|
||||
4CC7AAEF297F11C700430951 /* SelectedEventView.swift */,
|
||||
4CC7AAF5297F1A6A00430951 /* EventBody.swift */,
|
||||
@@ -2438,6 +2395,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */,
|
||||
4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */,
|
||||
4C9D6D1A2B1D35D7004E5CD9 /* PullDownSearch.swift */,
|
||||
);
|
||||
path = Search;
|
||||
@@ -2486,7 +2444,6 @@
|
||||
4CE6DEDA27F7A08100C66700 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D7FB14212BE5970000398331 /* PrivacyInfo.xcprivacy */,
|
||||
4C32B9362A9AD44700DC3548 /* flatbuffers */,
|
||||
4C9054862A6AEB4500811EEC /* nostrdb */,
|
||||
4C19AE4A2A5CEF7C00C90DB7 /* nostrscript */,
|
||||
@@ -2517,7 +2474,6 @@
|
||||
4CE6DEE527F7A08100C66700 /* damus */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C45E5002BED4CE10025A428 /* NIP10 */,
|
||||
4C1D4FB32A7967990024F453 /* build-git-hash.txt */,
|
||||
4CA3529C2A76AE47003BB08B /* Notify */,
|
||||
4CC14FEC2A73FC9A007AEB17 /* Types */,
|
||||
@@ -2576,6 +2532,8 @@
|
||||
3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */,
|
||||
4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */,
|
||||
501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */,
|
||||
3A5E47C62A4A76C800C0D090 /* TrieTests.swift */,
|
||||
3A90B1822A4EA3C600000D94 /* UserSearchCacheTests.swift */,
|
||||
4C4F14A62A2A61A30045A0B9 /* NostrScriptTests.swift */,
|
||||
4C19AE542A5D977400C90DB7 /* HashtagTests.swift */,
|
||||
3AAC7A012A60FE72002B50DF /* LocalizationUtilTests.swift */,
|
||||
@@ -2591,9 +2549,6 @@
|
||||
E06336A92B75832100A88E6B /* ImageMetadataTest.swift */,
|
||||
D7CBD1D52B8D509800BFD889 /* DamusPurpleImpendingExpirationTests.swift */,
|
||||
D72927AC2BAB515C00F93E90 /* RelayURLTests.swift */,
|
||||
D753CEA92BE9DE04001C3A5D /* MutingTests.swift */,
|
||||
4C2D34402BDAF1B300F9FB44 /* NIP10Tests.swift */,
|
||||
D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */,
|
||||
);
|
||||
path = damusTests;
|
||||
sourceTree = "<group>";
|
||||
@@ -2713,25 +2668,11 @@
|
||||
path = Images;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5CC852A02BDED9970039FFC5 /* Highlight */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5CC8529E2BD744F60039FFC5 /* HighlightView.swift */,
|
||||
5CC852A12BDED9B90039FFC5 /* HighlightDescription.swift */,
|
||||
5CC852A32BDF3CA10039FFC5 /* HighlightLink.swift */,
|
||||
5CC852A52BE00F180039FFC5 /* HighlightEventRef.swift */,
|
||||
5C4D9EA62C042FA5005EA0F7 /* HighlightPostView.swift */,
|
||||
);
|
||||
path = Highlight;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7C0F392D29B57C8F0039859C /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */,
|
||||
4C7D09752A0AF19E00943473 /* FillAndStroke.swift */,
|
||||
D72E12772BEED22400F4F781 /* Array.swift */,
|
||||
D78DB85A2C20FE4F00F0AB12 /* VectorMath.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
@@ -2800,17 +2741,6 @@
|
||||
path = Purple;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D78DB85D2C20FE9E00F0AB12 /* Chat */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C011B5C2BD0A56A002F2F9B /* ChatEventView.swift */,
|
||||
4C011B602BD0B25C002F2F9B /* ReplyQuoteView.swift */,
|
||||
4C011B5D2BD0A56A002F2F9B /* ChatroomThreadView.swift */,
|
||||
D78DB85E2C20FED300F0AB12 /* ChatBubbleView.swift */,
|
||||
);
|
||||
path = Chat;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D79C4C152AFEB061003A41B4 /* DamusNotificationService */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -2819,7 +2749,6 @@
|
||||
D79C4C182AFEB061003A41B4 /* Info.plist */,
|
||||
D70A3B162B02DCE5008BD568 /* NotificationFormatter.swift */,
|
||||
D74AAFC42B1538DE006CF0F4 /* NotificationExtensionState.swift */,
|
||||
D7FB14242BE5A9A800398331 /* PrivacyInfo.xcprivacy */,
|
||||
);
|
||||
path = DamusNotificationService;
|
||||
sourceTree = "<group>";
|
||||
@@ -2892,8 +2821,7 @@
|
||||
4C649880286E0EE300EAE2B3 /* secp256k1 */,
|
||||
4C06670328FC7EC500038D2A /* Kingfisher */,
|
||||
4C27C9312A64766F007DBC75 /* MarkdownUI */,
|
||||
3A0A30BA2C21397A00F8C9BC /* EmojiPicker */,
|
||||
D78DB8582C1CE9CA00F0AB12 /* SwipeActions */,
|
||||
3AFE89C22BD4156F00AD31EF /* MCEmojiPicker */,
|
||||
);
|
||||
productName = damus;
|
||||
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
|
||||
@@ -3003,7 +2931,6 @@
|
||||
"es-419",
|
||||
"es-ES",
|
||||
fa,
|
||||
fi,
|
||||
fr,
|
||||
"hu-HU",
|
||||
id,
|
||||
@@ -3018,7 +2945,6 @@
|
||||
ru,
|
||||
"sv-SE",
|
||||
sw,
|
||||
th,
|
||||
"tr-TR",
|
||||
uk,
|
||||
vi,
|
||||
@@ -3033,8 +2959,7 @@
|
||||
4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */,
|
||||
4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */,
|
||||
D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */,
|
||||
3A0A30B92C21397A00F8C9BC /* XCRemoteSwiftPackageReference "EmojiPicker" */,
|
||||
D78DB8572C1CE9CA00F0AB12 /* XCRemoteSwiftPackageReference "SwipeActions" */,
|
||||
3AFE89C12BD4156F00AD31EF /* XCRemoteSwiftPackageReference "MCEmojiPicker" */,
|
||||
);
|
||||
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -3054,7 +2979,6 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4C1D4FB42A7967990024F453 /* build-git-hash.txt in Resources */,
|
||||
D7FB14222BE5970000398331 /* PrivacyInfo.xcprivacy in Resources */,
|
||||
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */,
|
||||
4CE6DEEE27F7A08200C66700 /* Preview Assets.xcassets in Resources */,
|
||||
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */,
|
||||
@@ -3088,7 +3012,6 @@
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D7FB14252BE5A9A800398331 /* PrivacyInfo.xcprivacy in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -3150,6 +3073,7 @@
|
||||
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 */,
|
||||
@@ -3179,9 +3103,7 @@
|
||||
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
|
||||
D7EDED1E2B11797D0018B19C /* LongformEvent.swift in Sources */,
|
||||
504323A92A3495B6006AE6DC /* RelayModelCache.swift in Sources */,
|
||||
5C4D9EA72C042FA5005EA0F7 /* HighlightPostView.swift in Sources */,
|
||||
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */,
|
||||
D7FD12262BD345A700CF195B /* FirstAidSettingsView.swift in Sources */,
|
||||
D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */,
|
||||
4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */,
|
||||
4C363AA228296A7E006E126D /* SearchView.swift in Sources */,
|
||||
@@ -3210,7 +3132,6 @@
|
||||
4C32B94D2A9AD44700DC3548 /* Offset.swift in Sources */,
|
||||
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */,
|
||||
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */,
|
||||
5CC852A42BDF3CA10039FFC5 /* HighlightLink.swift in Sources */,
|
||||
4C32B9552A9AD44700DC3548 /* ByteBuffer.swift in Sources */,
|
||||
4C32B95B2A9AD44700DC3548 /* NativeObject.swift in Sources */,
|
||||
3AB72AB9298ECF30004BB58C /* Translator.swift in Sources */,
|
||||
@@ -3227,6 +3148,7 @@
|
||||
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
|
||||
D74AAFCF2B155D8C006CF0F4 /* ZapDataModel.swift in Sources */,
|
||||
4C8D1A6F29F31E5000ACDF75 /* FriendsButton.swift in Sources */,
|
||||
3A5E47C52A4A6CF400C0D090 /* Trie.swift in Sources */,
|
||||
D7100C562B76F8E600C59298 /* PurpleViewPrimitives.swift in Sources */,
|
||||
B57B4C642B312BFA00A232C0 /* RelayAuthenticationDetail.swift in Sources */,
|
||||
D7EDED2E2B128E8A0018B19C /* CollectionExtension.swift in Sources */,
|
||||
@@ -3281,7 +3203,6 @@
|
||||
4C86F7C62A76C51100EC0817 /* AttachedWalletNotify.swift in Sources */,
|
||||
4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */,
|
||||
4CB883A82975FC1800DC99E7 /* Zaps.swift in Sources */,
|
||||
4C45E5022BED4D000025A428 /* ThreadReply.swift in Sources */,
|
||||
D74AAFD42B155ECB006CF0F4 /* Zaps+.swift in Sources */,
|
||||
4C75EFB128049D510006080F /* NostrResponse.swift in Sources */,
|
||||
4C7D09592A05BEAD00943473 /* KeyboardVisible.swift in Sources */,
|
||||
@@ -3290,7 +3211,6 @@
|
||||
E02429952B7E97740088B16C /* CameraController.swift in Sources */,
|
||||
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */,
|
||||
5C14C29F2BBBA5C600079FD2 /* RelayNipList.swift in Sources */,
|
||||
D78DB85B2C20FE5000F0AB12 /* VectorMath.swift in Sources */,
|
||||
D7CB5D3E2B116DAD00AD4105 /* NotificationsManager.swift in Sources */,
|
||||
50A16FFF2AA76A0900DFEC1F /* VideoController.swift in Sources */,
|
||||
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */,
|
||||
@@ -3299,12 +3219,10 @@
|
||||
F75BA12D29A1855400E10810 /* BookmarksManager.swift in Sources */,
|
||||
4CC14FEF2A73FCCB007AEB17 /* IdType.swift in Sources */,
|
||||
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */,
|
||||
4C011B612BD0B25C002F2F9B /* ReplyQuoteView.swift in Sources */,
|
||||
D71AC4CC2BA8E3480076268E /* VisibilityTracker.swift in Sources */,
|
||||
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */,
|
||||
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
|
||||
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
|
||||
D72E12782BEED22500F4F781 /* Array.swift in Sources */,
|
||||
4C198DF529F88D2E004C165C /* ImageMetadata.swift in Sources */,
|
||||
4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */,
|
||||
4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */,
|
||||
@@ -3329,6 +3247,7 @@
|
||||
4C2B7BF22A71B6540049DEE7 /* Id.swift in Sources */,
|
||||
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */,
|
||||
4C7D09722A0AEF5E00943473 /* DamusGradient.swift in Sources */,
|
||||
4CCEB7B029B5415A0078AA28 /* SearchingProfileView.swift in Sources */,
|
||||
4C463CBF2B960B96008A8C36 /* PurpleBackdrop.swift in Sources */,
|
||||
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */,
|
||||
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */,
|
||||
@@ -3338,7 +3257,6 @@
|
||||
5CF2DCCC2AA3AF0B00984B8D /* RelayPicView.swift in Sources */,
|
||||
4C687C242A5FA86D0092C550 /* SearchHeaderView.swift in Sources */,
|
||||
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */,
|
||||
D7D2A3812BF815D000E4B42B /* PushNotificationClient.swift in Sources */,
|
||||
4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */,
|
||||
4CA352AC2A76C07F003BB08B /* NewUnmutesNotify.swift in Sources */,
|
||||
4C3EA64928FF597700C48A62 /* bech32.c in Sources */,
|
||||
@@ -3350,7 +3268,6 @@
|
||||
4C3AC7A12835A81400E1F516 /* SetupView.swift in Sources */,
|
||||
4C06670128FC7C5900038D2A /* RelayView.swift in Sources */,
|
||||
4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */,
|
||||
5CC852A22BDED9B90039FFC5 /* HighlightDescription.swift in Sources */,
|
||||
4C94D6432BA5AEFE00C26EFF /* QuoteRepostsView.swift in Sources */,
|
||||
D7EDED332B12ACAE0018B19C /* DamusUserDefaults.swift in Sources */,
|
||||
4CA352AE2A76C1AC003BB08B /* FollowedNotify.swift in Sources */,
|
||||
@@ -3389,14 +3306,11 @@
|
||||
4C64305C2A945AFF00B0C0E9 /* MusicController.swift in Sources */,
|
||||
5053ACA72A56DF3B00851AE3 /* DeveloperSettingsView.swift in Sources */,
|
||||
F79C7FAD29D5E9620000F946 /* EditPictureControl.swift in Sources */,
|
||||
4C011B5F2BD0A56A002F2F9B /* ChatroomThreadView.swift in Sources */,
|
||||
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
|
||||
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */,
|
||||
4C1A9A2A29DDF54400516EAC /* DamusVideoPlayer.swift in Sources */,
|
||||
4CA352A22A76AEC5003BB08B /* LikedNotify.swift in Sources */,
|
||||
5CC8529F2BD744F60039FFC5 /* HighlightView.swift in Sources */,
|
||||
BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */,
|
||||
5CC8529D2BD741CD0039FFC5 /* HighlightEvent.swift in Sources */,
|
||||
4C9146FD2A2A87C200DDEA40 /* wasm.c in Sources */,
|
||||
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
||||
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
|
||||
@@ -3405,6 +3319,7 @@
|
||||
4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */,
|
||||
4CE1399229F0666100AC6A0B /* ShareActionButton.swift in Sources */,
|
||||
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */,
|
||||
4C363A9C282838B9006E126D /* EventRef.swift in Sources */,
|
||||
4C5E54032A9522F600FF6E60 /* UserStatus.swift in Sources */,
|
||||
4C7D095F2A098C5D00943473 /* ConnectWalletView.swift in Sources */,
|
||||
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */,
|
||||
@@ -3451,8 +3366,8 @@
|
||||
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */,
|
||||
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */,
|
||||
5C14C29B2BBBA29C00079FD2 /* RelaySoftwareDetail.swift in Sources */,
|
||||
D78DB85F2C20FED300F0AB12 /* ChatBubbleView.swift in Sources */,
|
||||
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */,
|
||||
3A90B1812A4EA3AF00000D94 /* UserSearchCache.swift in Sources */,
|
||||
4C9D6D162B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift in Sources */,
|
||||
4CC14FF92A741939007AEB17 /* Referenced.swift in Sources */,
|
||||
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
|
||||
@@ -3508,7 +3423,6 @@
|
||||
B51C1CEB2B55A60A00E312A9 /* MuteDurationMenu.swift in Sources */,
|
||||
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
|
||||
4C32B9512A9AD44700DC3548 /* FlatbuffersErrors.swift in Sources */,
|
||||
5CC852A62BE00F180039FFC5 /* HighlightEventRef.swift in Sources */,
|
||||
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
|
||||
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
|
||||
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
|
||||
@@ -3562,7 +3476,6 @@
|
||||
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */,
|
||||
4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */,
|
||||
D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */,
|
||||
4C011B5E2BD0A56A002F2F9B /* ChatEventView.swift in Sources */,
|
||||
4C32B95A2A9AD44700DC3548 /* Verifiable.swift in Sources */,
|
||||
4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */,
|
||||
501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */,
|
||||
@@ -3597,8 +3510,8 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4C2D34412BDAF1B300F9FB44 /* NIP10Tests.swift in Sources */,
|
||||
4C684A572A7FFAE6005E6031 /* UrlTests.swift in Sources */,
|
||||
3A90B1832A4EA3C600000D94 /* UserSearchCacheTests.swift in Sources */,
|
||||
4C9B0DEE2A65A75F00CBDA21 /* AttrStringTestExtensions.swift in Sources */,
|
||||
4C19AE552A5D977400C90DB7 /* HashtagTests.swift in Sources */,
|
||||
D72927AD2BAB515C00F93E90 /* RelayURLTests.swift in Sources */,
|
||||
@@ -3610,7 +3523,6 @@
|
||||
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */,
|
||||
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */,
|
||||
501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */,
|
||||
D72E127A2BEEEED000F4F781 /* NostrFilterTests.swift in Sources */,
|
||||
B5B4D1432B37D47600844320 /* NdbExtensions.swift in Sources */,
|
||||
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */,
|
||||
D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */,
|
||||
@@ -3628,6 +3540,7 @@
|
||||
75AD872B2AA23A460085EF2C /* Block+Tests.swift in Sources */,
|
||||
E0E024112B7C19C20075735D /* TranslationTests.swift in Sources */,
|
||||
F944F56E29EA9CCC0067B3BF /* DamusParseContentTests.swift in Sources */,
|
||||
3A5E47C72A4A76C800C0D090 /* TrieTests.swift in Sources */,
|
||||
B501062D2B363036003874F5 /* AuthIntegrationTests.swift in Sources */,
|
||||
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */,
|
||||
D72A2D052AD9C1B5002AFF62 /* MockDamusState.swift in Sources */,
|
||||
@@ -3636,7 +3549,6 @@
|
||||
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */,
|
||||
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */,
|
||||
4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */,
|
||||
D753CEAA2BE9DE04001C3A5D /* MutingTests.swift in Sources */,
|
||||
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */,
|
||||
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */,
|
||||
4C684A552A7E91FE005E6031 /* LongPostTests.swift in Sources */,
|
||||
@@ -3657,10 +3569,10 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4C8FA7242BED58A900798A6A /* ThreadReply.swift in Sources */,
|
||||
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 */,
|
||||
@@ -3818,37 +3730,35 @@
|
||||
3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
3AC524F0298C000B00693EBF /* ar */,
|
||||
3AA5E70729B9E84A002701ED /* bg */,
|
||||
3A8624DB299E82BE00BD8BE9 /* cs */,
|
||||
3AB5B86C2986D8A3006599D2 /* de */,
|
||||
3A25EF152992DA5D008ABE69 /* el-GR */,
|
||||
3A2B8B0A296A8982009CC16D /* en-US */,
|
||||
3A5C4575296A879E0032D398 /* es-419 */,
|
||||
3A325AC929C9E0CF002BE7ED /* es-ES */,
|
||||
3AD5662C29BD2F5300BF77C5 /* fa */,
|
||||
3A47CB792BDA05A200728A7C /* fi */,
|
||||
3A821C4029E819D500B4BCA7 /* fr */,
|
||||
3AD14EB529C40F38009D2D9C /* hu-HU */,
|
||||
3A41E55B299D52BE001FA465 /* id */,
|
||||
3A929C22297F2CF80090925E /* it-IT */,
|
||||
3A66D929299472FA008B44F4 /* ja */,
|
||||
3AD5663229C0DA4B00BF77C5 /* ko */,
|
||||
3A185A06297F2C3800F4BDC0 /* lv-LV */,
|
||||
3A96D41C298DA94500388A2A /* nl */,
|
||||
3A93342B29884CA600D6A8F3 /* pl-PL */,
|
||||
3AC59CA929CDDB78007E04A6 /* pt-BR */,
|
||||
3AF6336A29884C6B0005672A /* pt-PT */,
|
||||
3A827A1A299FC69D00C4D171 /* ru */,
|
||||
3AD14EB829C40F3F009D2D9C /* sv-SE */,
|
||||
3ABACEC02A5B3ED10037A847 /* sw */,
|
||||
3A994C4C2BE5B9370019F632 /* th */,
|
||||
3A2B8B0A296A8982009CC16D /* en-US */,
|
||||
3AEB8005297CCEA900713A25 /* tr-TR */,
|
||||
3AA5E70429B682B3002701ED /* uk */,
|
||||
3A325AC629C9E0B8002BE7ED /* vi */,
|
||||
3A185A06297F2C3800F4BDC0 /* lv-LV */,
|
||||
3A929C22297F2CF80090925E /* it-IT */,
|
||||
3AB5B86C2986D8A3006599D2 /* de */,
|
||||
3AF6336A29884C6B0005672A /* pt-PT */,
|
||||
3A93342B29884CA600D6A8F3 /* pl-PL */,
|
||||
3AC524F0298C000B00693EBF /* ar */,
|
||||
3A96D41C298DA94500388A2A /* nl */,
|
||||
3A5CAE1F298DC0DB00B5334F /* zh-CN */,
|
||||
3A25EF152992DA5D008ABE69 /* el-GR */,
|
||||
3A66D929299472FA008B44F4 /* ja */,
|
||||
3A41E55B299D52BE001FA465 /* id */,
|
||||
3A8624DB299E82BE00BD8BE9 /* cs */,
|
||||
3A827A1A299FC69D00C4D171 /* ru */,
|
||||
3A3040FB29A91F03008A0F29 /* zh-HK */,
|
||||
3A3040FD29A91F31008A0F29 /* zh-TW */,
|
||||
3AA5E70429B682B3002701ED /* uk */,
|
||||
3AA5E70729B9E84A002701ED /* bg */,
|
||||
3AD5662C29BD2F5300BF77C5 /* fa */,
|
||||
3AD5663229C0DA4B00BF77C5 /* ko */,
|
||||
3AD14EB529C40F38009D2D9C /* hu-HU */,
|
||||
3AD14EB829C40F3F009D2D9C /* sv-SE */,
|
||||
3A325AC629C9E0B8002BE7ED /* vi */,
|
||||
3A325AC929C9E0CF002BE7ED /* es-ES */,
|
||||
3AC59CA929CDDB78007E04A6 /* pt-BR */,
|
||||
3A821C4029E819D500B4BCA7 /* fr */,
|
||||
3ABACEC02A5B3ED10037A847 /* sw */,
|
||||
);
|
||||
name = Localizable.stringsdict;
|
||||
sourceTree = "<group>";
|
||||
@@ -3856,36 +3766,34 @@
|
||||
3ACB685A297633BC00C46468 /* InfoPlist.strings */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
3AC524EE298C000B00693EBF /* ar */,
|
||||
3AA5E70529B9E83E002701ED /* bg */,
|
||||
3A8624D9299E82BE00BD8BE9 /* cs */,
|
||||
3AB5B86A2986D8A3006599D2 /* de */,
|
||||
3A25EF132992DA5D008ABE69 /* el-GR */,
|
||||
3ACB685B297633BC00C46468 /* es-419 */,
|
||||
3A325AC829C9E0CF002BE7ED /* es-ES */,
|
||||
3AD5662B29BD2F5300BF77C5 /* fa */,
|
||||
3A47CB772BDA05A200728A7C /* fi */,
|
||||
3A821C3F29E819D500B4BCA7 /* fr */,
|
||||
3AD14EB629C40F38009D2D9C /* hu-HU */,
|
||||
3A41E559299D52BE001FA465 /* id */,
|
||||
3A929C20297F2CF80090925E /* it-IT */,
|
||||
3A66D927299472FA008B44F4 /* ja */,
|
||||
3AD5663329C0DA4B00BF77C5 /* ko */,
|
||||
3A96D41A298DA94500388A2A /* nl */,
|
||||
3A185A04297F2C3800F4BDC0 /* lv-LV */,
|
||||
3A93342929884CA600D6A8F3 /* pl-PL */,
|
||||
3AC59CA829CDDB78007E04A6 /* pt-BR */,
|
||||
3AF6336829884C6B0005672A /* pt-PT */,
|
||||
3A827A18299FC69D00C4D171 /* ru */,
|
||||
3AD14EB929C40F3F009D2D9C /* sv-SE */,
|
||||
3ABACEBF2A5B3ED10037A847 /* sw */,
|
||||
3A994C4D2BE5B9370019F632 /* th */,
|
||||
3AEB8003297CCEA800713A25 /* tr-TR */,
|
||||
3AA5E70329B682AD002701ED /* uk */,
|
||||
3A325AC529C9E0B8002BE7ED /* vi */,
|
||||
3A185A04297F2C3800F4BDC0 /* lv-LV */,
|
||||
3A929C20297F2CF80090925E /* it-IT */,
|
||||
3AB5B86A2986D8A3006599D2 /* de */,
|
||||
3AF6336829884C6B0005672A /* pt-PT */,
|
||||
3A93342929884CA600D6A8F3 /* pl-PL */,
|
||||
3AC524EE298C000B00693EBF /* ar */,
|
||||
3A96D41A298DA94500388A2A /* nl */,
|
||||
3A5CAE1D298DC0DB00B5334F /* zh-CN */,
|
||||
3A25EF132992DA5D008ABE69 /* el-GR */,
|
||||
3A66D927299472FA008B44F4 /* ja */,
|
||||
3A41E559299D52BE001FA465 /* id */,
|
||||
3A8624D9299E82BE00BD8BE9 /* cs */,
|
||||
3A827A18299FC69D00C4D171 /* ru */,
|
||||
3A3040F929A91ED6008A0F29 /* zh-HK */,
|
||||
3A3040FC29A91F31008A0F29 /* zh-TW */,
|
||||
3AA5E70329B682AD002701ED /* uk */,
|
||||
3AA5E70529B9E83E002701ED /* bg */,
|
||||
3AD5662B29BD2F5300BF77C5 /* fa */,
|
||||
3AD5663329C0DA4B00BF77C5 /* ko */,
|
||||
3AD14EB629C40F38009D2D9C /* hu-HU */,
|
||||
3AD14EB929C40F3F009D2D9C /* sv-SE */,
|
||||
3A325AC529C9E0B8002BE7ED /* vi */,
|
||||
3A325AC829C9E0CF002BE7ED /* es-ES */,
|
||||
3AC59CA829CDDB78007E04A6 /* pt-BR */,
|
||||
3A821C3F29E819D500B4BCA7 /* fr */,
|
||||
3ABACEBF2A5B3ED10037A847 /* sw */,
|
||||
);
|
||||
name = InfoPlist.strings;
|
||||
sourceTree = "<group>";
|
||||
@@ -3893,37 +3801,35 @@
|
||||
3ACB685D297633BC00C46468 /* Localizable.strings */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
3AC524EF298C000B00693EBF /* ar */,
|
||||
3AA5E70629B9E844002701ED /* bg */,
|
||||
3A8624DA299E82BE00BD8BE9 /* cs */,
|
||||
3AB5B86B2986D8A3006599D2 /* de */,
|
||||
3A25EF142992DA5D008ABE69 /* el-GR */,
|
||||
3A3040FF29AB02D1008A0F29 /* en-US */,
|
||||
3ACB685E297633BC00C46468 /* es-419 */,
|
||||
3A325AC729C9E0CF002BE7ED /* es-ES */,
|
||||
3AD5662D29BD2F5300BF77C5 /* fa */,
|
||||
3A47CB782BDA05A200728A7C /* fi */,
|
||||
3A821C3E29E819D500B4BCA7 /* fr */,
|
||||
3A41E55A299D52BE001FA465 /* id */,
|
||||
3AD14EB729C40F38009D2D9C /* hu-HU */,
|
||||
3A929C21297F2CF80090925E /* it-IT */,
|
||||
3A66D928299472FA008B44F4 /* ja */,
|
||||
3AD5663129C0DA4B00BF77C5 /* ko */,
|
||||
3A185A05297F2C3800F4BDC0 /* lv-LV */,
|
||||
3A96D41B298DA94500388A2A /* nl */,
|
||||
3A93342A29884CA600D6A8F3 /* pl-PL */,
|
||||
3AC59CA729CDDB78007E04A6 /* pt-BR */,
|
||||
3AF6336929884C6B0005672A /* pt-PT */,
|
||||
3A827A19299FC69D00C4D171 /* ru */,
|
||||
3AD14EBA29C40F3F009D2D9C /* sv-SE */,
|
||||
3ABACEC12A5B3ED10037A847 /* sw */,
|
||||
3A994C4E2BE5B9370019F632 /* th */,
|
||||
3AEB8004297CCEA800713A25 /* tr-TR */,
|
||||
3AA5E70229B682A5002701ED /* uk */,
|
||||
3A325AC429C9E0B8002BE7ED /* vi */,
|
||||
3A185A05297F2C3800F4BDC0 /* lv-LV */,
|
||||
3A929C21297F2CF80090925E /* it-IT */,
|
||||
3AB5B86B2986D8A3006599D2 /* de */,
|
||||
3AF6336929884C6B0005672A /* pt-PT */,
|
||||
3A93342A29884CA600D6A8F3 /* pl-PL */,
|
||||
3AC524EF298C000B00693EBF /* ar */,
|
||||
3A96D41B298DA94500388A2A /* nl */,
|
||||
3A5CAE1E298DC0DB00B5334F /* zh-CN */,
|
||||
3A25EF142992DA5D008ABE69 /* el-GR */,
|
||||
3A66D928299472FA008B44F4 /* ja */,
|
||||
3A41E55A299D52BE001FA465 /* id */,
|
||||
3A8624DA299E82BE00BD8BE9 /* cs */,
|
||||
3A827A19299FC69D00C4D171 /* ru */,
|
||||
3A3040FA29A91EFC008A0F29 /* zh-HK */,
|
||||
3A3040FE29A91F31008A0F29 /* zh-TW */,
|
||||
3A3040FF29AB02D1008A0F29 /* en-US */,
|
||||
3AA5E70229B682A5002701ED /* uk */,
|
||||
3AA5E70629B9E844002701ED /* bg */,
|
||||
3AD5662D29BD2F5300BF77C5 /* fa */,
|
||||
3AD5663129C0DA4B00BF77C5 /* ko */,
|
||||
3AD14EB729C40F38009D2D9C /* hu-HU */,
|
||||
3AD14EBA29C40F3F009D2D9C /* sv-SE */,
|
||||
3A325AC429C9E0B8002BE7ED /* vi */,
|
||||
3A325AC729C9E0CF002BE7ED /* es-ES */,
|
||||
3AC59CA729CDDB78007E04A6 /* pt-BR */,
|
||||
3A821C3E29E819D500B4BCA7 /* fr */,
|
||||
3ABACEC12A5B3ED10037A847 /* sw */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
@@ -3966,7 +3872,7 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
@@ -3987,7 +3893,7 @@
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.3;
|
||||
MARKETING_VERSION = 1.9;
|
||||
MARKETING_VERSION = 1.8;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
@@ -4033,7 +3939,7 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 4;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -4049,7 +3955,7 @@
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 12.3;
|
||||
MARKETING_VERSION = 1.9;
|
||||
MARKETING_VERSION = 1.8;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -4069,7 +3975,6 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -4096,7 +4001,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 1.10;
|
||||
MARKETING_VERSION = 1.9;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
@@ -4120,7 +4025,6 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -4147,7 +4051,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 1.10;
|
||||
MARKETING_VERSION = 1.9;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
@@ -4352,12 +4256,12 @@
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCRemoteSwiftPackageReference section */
|
||||
3A0A30B92C21397A00F8C9BC /* XCRemoteSwiftPackageReference "EmojiPicker" */ = {
|
||||
3AFE89C12BD4156F00AD31EF /* XCRemoteSwiftPackageReference "MCEmojiPicker" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/tyiu/EmojiPicker.git";
|
||||
repositoryURL = "https://github.com/izyumkin/MCEmojiPicker";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 0.1.1;
|
||||
minimumVersion = 1.2.3;
|
||||
};
|
||||
};
|
||||
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
|
||||
@@ -4392,14 +4296,6 @@
|
||||
minimumVersion = 0.2.26;
|
||||
};
|
||||
};
|
||||
D78DB8572C1CE9CA00F0AB12 /* XCRemoteSwiftPackageReference "SwipeActions" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/aheze/SwipeActions";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = 1.1.0;
|
||||
};
|
||||
};
|
||||
D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/pointfreeco/swift-snapshot-testing";
|
||||
@@ -4411,10 +4307,10 @@
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
3A0A30BA2C21397A00F8C9BC /* EmojiPicker */ = {
|
||||
3AFE89C22BD4156F00AD31EF /* MCEmojiPicker */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 3A0A30B92C21397A00F8C9BC /* XCRemoteSwiftPackageReference "EmojiPicker" */;
|
||||
productName = EmojiPicker;
|
||||
package = 3AFE89C12BD4156F00AD31EF /* XCRemoteSwiftPackageReference "MCEmojiPicker" */;
|
||||
productName = MCEmojiPicker;
|
||||
};
|
||||
4C06670328FC7EC500038D2A /* Kingfisher */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
@@ -4436,11 +4332,6 @@
|
||||
package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */;
|
||||
productName = secp256k1;
|
||||
};
|
||||
D78DB8582C1CE9CA00F0AB12 /* SwipeActions */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D78DB8572C1CE9CA00F0AB12 /* XCRemoteSwiftPackageReference "SwipeActions" */;
|
||||
productName = SwipeActions;
|
||||
};
|
||||
D7A343ED2AD0D77C00CED48B /* InlineSnapshotTesting */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D7A343EC2AD0D77C00CED48B /* XCRemoteSwiftPackageReference "swift-snapshot-testing" */;
|
||||
|
||||
@@ -1,24 +1,6 @@
|
||||
{
|
||||
"originHash" : "babaf4d5748afecf49bbb702530d8e9576460692f478b0a50ee43195dd4440e2",
|
||||
"originHash" : "c627e27ffbf9762282eabbfa1118e0c13a337c2492a58f81531aa396bcf2d440",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "emojikit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/tyiu/EmojiKit",
|
||||
"state" : {
|
||||
"revision" : "05805f72d63a6d6a2d7dc7fe14abd37c1317b11a",
|
||||
"version" : "0.1.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "emojipicker",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/tyiu/EmojiPicker.git",
|
||||
"state" : {
|
||||
"revision" : "0c28b4a1a6b8840cf2580bda59517f6d0a733dc8",
|
||||
"version" : "0.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "gsplayer",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -37,6 +19,15 @@
|
||||
"version" : "7.6.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "mcemojipicker",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/izyumkin/MCEmojiPicker",
|
||||
"state" : {
|
||||
"revision" : "e0b4903b75ae1cc418d276d84d1cb946b8a1d73c",
|
||||
"version" : "1.2.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "secp256k1.swift",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -45,15 +36,6 @@
|
||||
"revision" : "40b4b38b3b1c83f7088c76189a742870e0ca06a9"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-collections",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-collections.git",
|
||||
"state" : {
|
||||
"revision" : "ee97538f5b81ae89698fd95938896dec5217b148",
|
||||
"version" : "1.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-markdown-ui",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -79,24 +61,6 @@
|
||||
"revision" : "74203046135342e4a4a627476dd6caf8b28fe11b",
|
||||
"version" : "509.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-trie",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/tyiu/swift-trie",
|
||||
"state" : {
|
||||
"revision" : "4c50bff6c168f74425f70476be62a072980d2da7",
|
||||
"version" : "0.1.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swipeactions",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/aheze/SwipeActions",
|
||||
"state" : {
|
||||
"revision" : "41e6f6dce02d8cfa164f8c5461a41340850ca3ab",
|
||||
"version" : "1.1.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 3
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xD7",
|
||||
"green" : "0xD1",
|
||||
"red" : "0xD1"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x13",
|
||||
"green" : "0x11",
|
||||
"red" : "0x11"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xF9",
|
||||
"green" : "0xF3",
|
||||
"red" : "0xF3"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x25",
|
||||
"green" : "0x22",
|
||||
"red" : "0x22"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "244",
|
||||
"green" : "218",
|
||||
"red" : "244"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "92",
|
||||
"green" : "45",
|
||||
"red" : "93"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "236",
|
||||
"green" : "194",
|
||||
"red" : "238"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "109",
|
||||
"green" : "49",
|
||||
"red" : "111"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "197",
|
||||
"green" : "67",
|
||||
"red" : "204"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "255",
|
||||
"green" : "194",
|
||||
"red" : "255"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xF2",
|
||||
"green" : "0xD8",
|
||||
"red" : "0xF4"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x45",
|
||||
"green" : "0x17",
|
||||
"red" : "0x47"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -10,11 +10,6 @@ import SwiftUI
|
||||
|
||||
class DamusColors {
|
||||
static let adaptableGrey = Color("DamusAdaptableGrey")
|
||||
static let adaptableGrey2 = Color("DamusAdaptableGrey 2")
|
||||
static let adaptableLighterGrey = Color("DamusAdaptableLighterGrey")
|
||||
static let adaptablePurpleBackground = Color("DamusAdaptablePurpleBackground 1")
|
||||
static let adaptablePurpleBackground2 = Color("DamusAdaptablePurpleBackground 2")
|
||||
static let adaptablePurpleForeground = Color("DamusAdaptablePurpleForeground")
|
||||
static let adaptableBlack = Color("DamusAdaptableBlack")
|
||||
static let adaptableWhite = Color("DamusAdaptableWhite")
|
||||
static let white = Color("DamusWhite")
|
||||
@@ -28,7 +23,6 @@ class DamusColors {
|
||||
static let green = Color("DamusGreen")
|
||||
static let purple = Color("DamusPurple")
|
||||
static let deepPurple = Color("DamusDeepPurple")
|
||||
static let highlight = Color("DamusHighlight")
|
||||
static let blue = Color("DamusBlue")
|
||||
static let bitcoin = Color("Bitcoin")
|
||||
static let success = Color("DamusSuccessPrimary")
|
||||
|
||||
@@ -35,7 +35,7 @@ struct SearchHeaderView: View {
|
||||
}
|
||||
|
||||
var SearchText: Text {
|
||||
Text(described.description)
|
||||
Text(verbatim: described.description)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -83,9 +83,9 @@ struct SingleCharacterAvatar: View {
|
||||
|
||||
var body: some View {
|
||||
NonImageAvatar {
|
||||
Text(character)
|
||||
Text(verbatim: character)
|
||||
.font(.largeTitle.bold())
|
||||
.mask(Text(character)
|
||||
.mask(Text(verbatim: character)
|
||||
.font(.largeTitle.bold()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,20 +9,16 @@ import UIKit
|
||||
import SwiftUI
|
||||
|
||||
struct SelectableText: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent?
|
||||
|
||||
let attributedString: AttributedString
|
||||
let textAlignment: NSTextAlignment
|
||||
@State private var showHighlightPost = false
|
||||
@State private var selectedText = ""
|
||||
|
||||
@State private var selectedTextHeight: CGFloat = .zero
|
||||
@State private var selectedTextWidth: CGFloat = .zero
|
||||
|
||||
|
||||
let size: EventViewKind
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent?, attributedString: AttributedString, textAlignment: NSTextAlignment? = nil, size: EventViewKind) {
|
||||
self.damus_state = damus_state
|
||||
self.event = event
|
||||
|
||||
init(attributedString: AttributedString, textAlignment: NSTextAlignment? = nil, size: EventViewKind) {
|
||||
self.attributedString = attributedString
|
||||
self.textAlignment = textAlignment ?? NSTextAlignment.natural
|
||||
self.size = size
|
||||
@@ -36,9 +32,6 @@ struct SelectableText: View {
|
||||
font: eventviewsize_to_uifont(size),
|
||||
fixedWidth: selectedTextWidth,
|
||||
textAlignment: self.textAlignment,
|
||||
enableHighlighting: self.enableHighlighting(),
|
||||
showHighlightPost: $showHighlightPost,
|
||||
selectedText: $selectedText,
|
||||
height: $selectedTextHeight
|
||||
)
|
||||
.padding([.leading, .trailing], -1.0)
|
||||
@@ -53,48 +46,8 @@ struct SelectableText: View {
|
||||
self.selectedTextWidth = newSize.width
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showHighlightPost) {
|
||||
if let event {
|
||||
HighlightPostView(damus_state: damus_state, event: event, selectedText: $selectedText)
|
||||
.presentationDragIndicator(.visible)
|
||||
.presentationDetents([.height(selectedTextHeight + 150), .medium, .large])
|
||||
}
|
||||
}
|
||||
.frame(height: selectedTextHeight)
|
||||
}
|
||||
|
||||
func enableHighlighting() -> Bool {
|
||||
self.event != nil
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class TextView: UITextView {
|
||||
@Binding var showHighlightPost: Bool
|
||||
@Binding var selectedText: String
|
||||
|
||||
init(frame: CGRect, textContainer: NSTextContainer?, showHighlightPost: Binding<Bool>, selectedText: Binding<String>) {
|
||||
self._showHighlightPost = showHighlightPost
|
||||
self._selectedText = selectedText
|
||||
super.init(frame: frame, textContainer: textContainer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
||||
if action == #selector(highlightText(_:)) {
|
||||
return true
|
||||
}
|
||||
return super.canPerformAction(action, withSender: sender)
|
||||
}
|
||||
|
||||
@objc public func highlightText(_ sender: Any?) {
|
||||
guard let selectedRange = self.selectedTextRange else { return }
|
||||
selectedText = self.text(in: selectedRange) ?? ""
|
||||
showHighlightPost.toggle()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fileprivate struct TextViewRepresentable: UIViewRepresentable {
|
||||
@@ -104,13 +57,11 @@ fileprivate class TextView: UITextView {
|
||||
let font: UIFont
|
||||
let fixedWidth: CGFloat
|
||||
let textAlignment: NSTextAlignment
|
||||
let enableHighlighting: Bool
|
||||
@Binding var showHighlightPost: Bool
|
||||
@Binding var selectedText: String
|
||||
|
||||
@Binding var height: CGFloat
|
||||
|
||||
func makeUIView(context: UIViewRepresentableContext<Self>) -> TextView {
|
||||
let view = TextView(frame: .zero, textContainer: nil, showHighlightPost: $showHighlightPost, selectedText: $selectedText)
|
||||
func makeUIView(context: UIViewRepresentableContext<Self>) -> UITextView {
|
||||
let view = UITextView()
|
||||
view.isEditable = false
|
||||
view.dataDetectorTypes = .all
|
||||
view.isSelectable = true
|
||||
@@ -120,15 +71,10 @@ fileprivate class TextView: UITextView {
|
||||
view.textContainerInset.left = 1.0
|
||||
view.textContainerInset.right = 1.0
|
||||
view.textAlignment = textAlignment
|
||||
|
||||
let menuController = UIMenuController.shared
|
||||
let highlightItem = UIMenuItem(title: "Highlight", action: #selector(view.highlightText(_:)))
|
||||
menuController.menuItems = self.enableHighlighting ? [highlightItem] : []
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: TextView, context: UIViewRepresentableContext<Self>) {
|
||||
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<Self>) {
|
||||
let mutableAttributedString = createNSAttributedString()
|
||||
uiView.attributedText = mutableAttributedString
|
||||
uiView.textAlignment = self.textAlignment
|
||||
|
||||
@@ -192,7 +192,7 @@ struct UserStatusSheet: View {
|
||||
|
||||
Picker(NSLocalizedString("Duration", comment: "Label for profile status expiration duration picker."), selection: $duration) {
|
||||
ForEach(StatusDuration.allCases, id: \.self) { d in
|
||||
Text(d.description)
|
||||
Text(verbatim: d.description)
|
||||
.tag(d)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,8 +29,7 @@ struct SupporterBadge: View {
|
||||
.frame(width:size, height:size)
|
||||
.foregroundStyle(GoldGradient)
|
||||
if self.style == .full {
|
||||
let date = format_date(date: purple_account.created_at, time_style: .none)
|
||||
Text(date)
|
||||
Text(verbatim: format_date(date: purple_account.created_at, time_style: .none))
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
@@ -51,9 +51,9 @@ struct TranslateView: View {
|
||||
.foregroundColor(.gray)
|
||||
.font(.footnote)
|
||||
.padding([.top, .bottom], 10)
|
||||
|
||||
|
||||
if self.size == .selected {
|
||||
SelectableText(damus_state: damus_state, event: event, attributedString: artifacts.content.attributed, size: self.size)
|
||||
SelectableText(attributedString: artifacts.content.attributed, size: self.size)
|
||||
} else {
|
||||
artifacts.content.text
|
||||
.font(eventviewsize_to_font(self.size, font_size: font_size))
|
||||
|
||||
@@ -10,12 +10,10 @@ import SwiftUI
|
||||
struct TruncatedText: View {
|
||||
let text: CompatibleText
|
||||
let maxChars: Int
|
||||
let show_show_more_button: Bool
|
||||
|
||||
init(text: CompatibleText, maxChars: Int = 280, show_show_more_button: Bool) {
|
||||
init(text: CompatibleText, maxChars: Int = 280) {
|
||||
self.text = text
|
||||
self.maxChars = maxChars
|
||||
self.show_show_more_button = show_show_more_button
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -31,10 +29,8 @@ struct TruncatedText: View {
|
||||
|
||||
if truncatedAttributedString != nil {
|
||||
Spacer()
|
||||
if self.show_show_more_button {
|
||||
Button(NSLocalizedString("Show more", comment: "Button to show entire note.")) { }
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
Button(NSLocalizedString("Show more", comment: "Button to show entire note.")) { }
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,10 +38,10 @@ struct TruncatedText: View {
|
||||
struct TruncatedText_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack(spacing: 100) {
|
||||
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven"), show_show_more_button: true)
|
||||
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven"))
|
||||
.frame(width: 200, height: 200)
|
||||
|
||||
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour"), show_show_more_button: true)
|
||||
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour"))
|
||||
.frame(width: 200, height: 200)
|
||||
}
|
||||
}
|
||||
|
||||
+50
-41
@@ -59,57 +59,66 @@ func parse_note_content(content: NoteContent) -> Blocks {
|
||||
}
|
||||
}
|
||||
|
||||
func interpret_event_refs(tags: TagsSequence) -> ThreadReply? {
|
||||
// migration is long over, lets just do this to fix tests
|
||||
return interpret_event_refs_ndb(tags: tags)
|
||||
}
|
||||
|
||||
func interpret_event_refs_ndb(tags: TagsSequence) -> ThreadReply? {
|
||||
func interpret_event_refs_ndb(blocks: [Block], tags: TagsSequence) -> [EventRef] {
|
||||
if tags.count == 0 {
|
||||
return nil
|
||||
return []
|
||||
}
|
||||
|
||||
/// build a set of indices for each event mention
|
||||
let mention_indices = build_mention_indices(blocks, type: .e)
|
||||
|
||||
return interp_event_refs_without_mentions_ndb(References<NoteRef>(tags: tags))
|
||||
/// simpler case with no mentions
|
||||
if mention_indices.count == 0 {
|
||||
return interp_event_refs_without_mentions_ndb(tags.note.referenced_noterefs)
|
||||
}
|
||||
|
||||
return interp_event_refs_with_mentions_ndb(tags: tags, mention_indices: mention_indices)
|
||||
}
|
||||
|
||||
func interp_event_refs_without_mentions_ndb(_ ev_tags: References<NoteRef>) -> ThreadReply? {
|
||||
func interp_event_refs_without_mentions_ndb(_ ev_tags: References<NoteRef>) -> [EventRef] {
|
||||
|
||||
var count = 0
|
||||
var evrefs: [EventRef] = []
|
||||
var first: Bool = true
|
||||
var root_id: NoteRef? = nil
|
||||
var reply_id: NoteRef? = nil
|
||||
var mention: NoteRef? = nil
|
||||
var any_marker: Bool = false
|
||||
var first_ref: NoteRef? = nil
|
||||
|
||||
for ref in ev_tags {
|
||||
if let marker = ref.marker {
|
||||
any_marker = true
|
||||
switch marker {
|
||||
case .root: root_id = ref
|
||||
case .reply: reply_id = ref
|
||||
case .mention: mention = ref
|
||||
}
|
||||
// deprecated form, only activate if we don't have any markers set
|
||||
} else if !any_marker {
|
||||
if first {
|
||||
root_id = ref
|
||||
first = false
|
||||
if first {
|
||||
first_ref = ref
|
||||
evrefs.append(.thread_id(ref))
|
||||
first = false
|
||||
} else {
|
||||
|
||||
evrefs.append(.reply(ref))
|
||||
}
|
||||
count += 1
|
||||
}
|
||||
|
||||
if let first_ref, count == 1 {
|
||||
let r = first_ref
|
||||
return [.reply_to_root(r)]
|
||||
}
|
||||
|
||||
return evrefs
|
||||
}
|
||||
|
||||
func interp_event_refs_with_mentions_ndb(tags: TagsSequence, mention_indices: Set<Int>) -> [EventRef] {
|
||||
var mentions: [EventRef] = []
|
||||
var ev_refs: [NoteRef] = []
|
||||
var i: Int = 0
|
||||
|
||||
for tag in tags {
|
||||
if let note_id = NoteRef.from_tag(tag: tag) {
|
||||
if mention_indices.contains(i) {
|
||||
mentions.append(.mention(.noteref(note_id, index: i)))
|
||||
} else {
|
||||
reply_id = ref
|
||||
ev_refs.append(note_id)
|
||||
}
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
|
||||
// If either reply or root_id is blank while the other is not, then this is
|
||||
// considered reply-to-root. We should always have a root and reply tag, if they
|
||||
// are equal this is reply-to-root
|
||||
if reply_id == nil && root_id != nil {
|
||||
reply_id = root_id
|
||||
} else if root_id == nil && reply_id != nil {
|
||||
root_id = reply_id
|
||||
}
|
||||
|
||||
guard let reply_id, let root_id else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ThreadReply(root: root_id, reply: reply_id, mention: mention.map { m in .noteref(m) })
|
||||
|
||||
var replies = interp_event_refs_without_mentions(ev_refs)
|
||||
replies.append(contentsOf: mentions)
|
||||
return replies
|
||||
}
|
||||
|
||||
+7
-12
@@ -8,7 +8,6 @@
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
import MediaPlayer
|
||||
import EmojiPicker
|
||||
|
||||
struct ZapSheet {
|
||||
let target: ZapTarget
|
||||
@@ -309,7 +308,7 @@ struct ContentView: View {
|
||||
active_sheet = .onboardingSuggestions
|
||||
hasSeenOnboardingSuggestions = true
|
||||
}
|
||||
self.appDelegate?.state = damus_state
|
||||
self.appDelegate?.settings = damus_state?.settings
|
||||
}
|
||||
.sheet(item: $active_sheet) { item in
|
||||
switch item {
|
||||
@@ -680,7 +679,10 @@ struct ContentView: View {
|
||||
let relay_filters = RelayFilters(our_pubkey: pubkey)
|
||||
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
|
||||
|
||||
let settings = UserSettingsStore.globally_load_for(pubkey: pubkey)
|
||||
// dumb stuff needed for property wrappers
|
||||
UserSettingsStore.pubkey = pubkey
|
||||
let settings = UserSettingsStore()
|
||||
UserSettingsStore.shared = settings
|
||||
|
||||
let new_relay_filters = load_relay_filters(pubkey) == nil
|
||||
for relay in bootstrap_relays {
|
||||
@@ -700,7 +702,7 @@ struct ContentView: View {
|
||||
likes: EventCounter(our_pubkey: pubkey),
|
||||
boosts: EventCounter(our_pubkey: pubkey),
|
||||
contacts: Contacts(our_pubkey: pubkey),
|
||||
mutelist_manager: MutelistManager(user_keypair: keypair),
|
||||
mutelist_manager: MutelistManager(),
|
||||
profiles: Profiles(ndb: ndb),
|
||||
dms: home.dms,
|
||||
previews: PreviewCache(),
|
||||
@@ -720,8 +722,7 @@ struct ContentView: View {
|
||||
music: MusicController(onChange: music_changed),
|
||||
video: VideoController(),
|
||||
ndb: ndb,
|
||||
quote_reposts: .init(our_pubkey: pubkey),
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
|
||||
quote_reposts: .init(our_pubkey: pubkey)
|
||||
)
|
||||
|
||||
home.damus_state = self.damus_state!
|
||||
@@ -834,12 +835,6 @@ func save_last_event(_ ev: NostrEvent, timeline: Timeline) {
|
||||
UserDefaults.standard.set(String(ev.created_at), forKey: "last_\(str)_time")
|
||||
}
|
||||
|
||||
func save_last_event(_ ev_id: NoteId, created_at: UInt32, timeline: Timeline) {
|
||||
let str = timeline.rawValue
|
||||
UserDefaults.standard.set(ev_id.hex(), forKey: "last_\(str)")
|
||||
UserDefaults.standard.set(String(created_at), forKey: "last_\(str)_time")
|
||||
}
|
||||
|
||||
func update_filters_with_since(last_of_kind: [UInt32: NostrEvent], filters: [NostrFilter]) -> [NostrFilter] {
|
||||
|
||||
return filters.map { filter in
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
class Contacts {
|
||||
private var friends: Set<Pubkey> = Set()
|
||||
private var friend_of_friends: Set<Pubkey> = Set()
|
||||
@@ -14,13 +15,7 @@ class Contacts {
|
||||
private var pubkey_to_our_friends = [Pubkey : Set<Pubkey>]()
|
||||
|
||||
let our_pubkey: Pubkey
|
||||
var delegate: ContactsDelegate? = nil
|
||||
var event: NostrEvent? {
|
||||
didSet {
|
||||
guard let event else { return }
|
||||
self.delegate?.latest_contact_event_changed(new_event: event)
|
||||
}
|
||||
}
|
||||
var event: NostrEvent?
|
||||
|
||||
init(our_pubkey: Pubkey) {
|
||||
self.our_pubkey = our_pubkey
|
||||
@@ -93,8 +88,3 @@ class Contacts {
|
||||
return Array((pubkey_to_our_friends[pubkey] ?? Set()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Delegate protocol for `Contacts`. Use this to listen to significant updates from a `Contacts` instance
|
||||
protocol ContactsDelegate {
|
||||
func latest_contact_event_changed(new_event: NostrEvent)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ enum FilterState : Int {
|
||||
func filter(ev: NostrEvent) -> Bool {
|
||||
switch self {
|
||||
case .posts:
|
||||
return ev.known_kind == .boost || !ev.is_reply()
|
||||
return ev.known_kind == .boost || !ev.is_reply(.empty)
|
||||
case .posts_and_replies:
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -9,31 +9,31 @@ import Foundation
|
||||
|
||||
|
||||
class CreateAccountModel: ObservableObject {
|
||||
@Published var display_name: String = ""
|
||||
@Published var name: String = ""
|
||||
@Published var real_name: String = ""
|
||||
@Published var nick_name: String = ""
|
||||
@Published var about: String = ""
|
||||
@Published var pubkey: Pubkey = .empty
|
||||
@Published var privkey: Privkey = .empty
|
||||
@Published var profile_image: URL? = nil
|
||||
|
||||
var rendered_name: String {
|
||||
if display_name.isEmpty {
|
||||
return name
|
||||
if real_name.isEmpty {
|
||||
return nick_name
|
||||
}
|
||||
return display_name
|
||||
return real_name
|
||||
}
|
||||
|
||||
var keypair: Keypair {
|
||||
return Keypair(pubkey: self.pubkey, privkey: self.privkey)
|
||||
}
|
||||
|
||||
init(display_name: String = "", name: String = "", about: String = "") {
|
||||
init(real: String = "", nick: String = "", about: String = "") {
|
||||
let keypair = generate_new_keypair()
|
||||
self.pubkey = keypair.pubkey
|
||||
self.privkey = keypair.privkey
|
||||
|
||||
self.display_name = display_name
|
||||
self.name = name
|
||||
self.real_name = real
|
||||
self.nick_name = nick
|
||||
self.about = about
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
import Foundation
|
||||
import LinkPresentation
|
||||
import EmojiPicker
|
||||
|
||||
class DamusState: HeadlessDamusState {
|
||||
let pool: RelayPool
|
||||
@@ -37,10 +36,8 @@ class DamusState: HeadlessDamusState {
|
||||
let video: VideoController
|
||||
let ndb: Ndb
|
||||
var purple: DamusPurple
|
||||
var push_notification_client: PushNotificationClient
|
||||
let emoji_provider: EmojiProvider
|
||||
|
||||
init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [RelayURL], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider) {
|
||||
init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [RelayURL], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter) {
|
||||
self.pool = pool
|
||||
self.keypair = keypair
|
||||
self.likes = likes
|
||||
@@ -71,8 +68,6 @@ class DamusState: HeadlessDamusState {
|
||||
keypair: keypair
|
||||
)
|
||||
self.quote_reposts = quote_reposts
|
||||
self.push_notification_client = PushNotificationClient(keypair: keypair, settings: settings)
|
||||
self.emoji_provider = emoji_provider
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
@@ -102,7 +97,6 @@ class DamusState: HeadlessDamusState {
|
||||
|
||||
func close() {
|
||||
print("txn: damus close")
|
||||
wallet.disconnect()
|
||||
pool.close()
|
||||
ndb.close()
|
||||
}
|
||||
@@ -118,7 +112,7 @@ class DamusState: HeadlessDamusState {
|
||||
likes: EventCounter(our_pubkey: empty_pub),
|
||||
boosts: EventCounter(our_pubkey: empty_pub),
|
||||
contacts: Contacts(our_pubkey: empty_pub),
|
||||
mutelist_manager: MutelistManager(user_keypair: kp),
|
||||
mutelist_manager: MutelistManager(),
|
||||
profiles: Profiles(ndb: .empty),
|
||||
dms: DirectMessagesModel(our_pubkey: empty_pub),
|
||||
previews: PreviewCache(),
|
||||
@@ -138,8 +132,7 @@ class DamusState: HeadlessDamusState {
|
||||
music: nil,
|
||||
video: VideoController(),
|
||||
ndb: .empty,
|
||||
quote_reposts: .init(our_pubkey: empty_pub),
|
||||
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
|
||||
quote_reposts: .init(our_pubkey: empty_pub)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
//
|
||||
// EventRef.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2022-05-08.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum EventRef: Equatable {
|
||||
case mention(Mention<NoteRef>)
|
||||
case thread_id(NoteRef)
|
||||
case reply(NoteRef)
|
||||
case reply_to_root(NoteRef)
|
||||
|
||||
var is_mention: NoteRef? {
|
||||
if case .mention(let m) = self { return m.ref }
|
||||
return nil
|
||||
}
|
||||
|
||||
var is_direct_reply: NoteRef? {
|
||||
switch self {
|
||||
case .mention:
|
||||
return nil
|
||||
case .thread_id:
|
||||
return nil
|
||||
case .reply(let refid):
|
||||
return refid
|
||||
case .reply_to_root(let refid):
|
||||
return refid
|
||||
}
|
||||
}
|
||||
|
||||
var is_thread_id: NoteRef? {
|
||||
switch self {
|
||||
case .mention:
|
||||
return nil
|
||||
case .thread_id(let referencedId):
|
||||
return referencedId
|
||||
case .reply:
|
||||
return nil
|
||||
case .reply_to_root(let referencedId):
|
||||
return referencedId
|
||||
}
|
||||
}
|
||||
|
||||
var is_reply: NoteRef? {
|
||||
switch self {
|
||||
case .mention:
|
||||
return nil
|
||||
case .thread_id:
|
||||
return nil
|
||||
case .reply(let refid):
|
||||
return refid
|
||||
case .reply_to_root(let refid):
|
||||
return refid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func build_mention_indices(_ blocks: [Block], type: MentionType) -> Set<Int> {
|
||||
return blocks.reduce(into: []) { acc, block in
|
||||
switch block {
|
||||
case .mention(let m):
|
||||
if m.ref.key == type, let idx = m.index {
|
||||
acc.insert(idx)
|
||||
}
|
||||
case .relay:
|
||||
return
|
||||
case .text:
|
||||
return
|
||||
case .hashtag:
|
||||
return
|
||||
case .url:
|
||||
return
|
||||
case .invoice:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func interp_event_refs_without_mentions(_ refs: [NoteRef]) -> [EventRef] {
|
||||
if refs.count == 0 {
|
||||
return []
|
||||
}
|
||||
|
||||
if refs.count == 1 {
|
||||
return [.reply_to_root(refs[0])]
|
||||
}
|
||||
|
||||
var evrefs: [EventRef] = []
|
||||
var first: Bool = true
|
||||
for ref in refs {
|
||||
if first {
|
||||
evrefs.append(.thread_id(ref))
|
||||
first = false
|
||||
} else {
|
||||
evrefs.append(.reply(ref))
|
||||
}
|
||||
}
|
||||
return evrefs
|
||||
}
|
||||
|
||||
func interp_event_refs_with_mentions(tags: Tags, mention_indices: Set<Int>) -> [EventRef] {
|
||||
var mentions: [EventRef] = []
|
||||
var ev_refs: [NoteRef] = []
|
||||
var i: Int = 0
|
||||
|
||||
for tag in tags {
|
||||
if let ref = NoteRef.from_tag(tag: tag) {
|
||||
if mention_indices.contains(i) {
|
||||
let mention = Mention<NoteRef>(index: i, ref: ref)
|
||||
mentions.append(.mention(mention))
|
||||
} else {
|
||||
ev_refs.append(ref)
|
||||
}
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
|
||||
var replies = interp_event_refs_without_mentions(ev_refs)
|
||||
replies.append(contentsOf: mentions)
|
||||
return replies
|
||||
}
|
||||
|
||||
func interpret_event_refs(blocks: [Block], tags: Tags) -> [EventRef] {
|
||||
if tags.count == 0 {
|
||||
return []
|
||||
}
|
||||
|
||||
/// build a set of indices for each event mention
|
||||
let mention_indices = build_mention_indices(blocks, type: .e)
|
||||
|
||||
/// simpler case with no mentions
|
||||
if mention_indices.count == 0 {
|
||||
return interp_event_refs_without_mentions_ndb(References<NoteRef>(tags: tags))
|
||||
}
|
||||
|
||||
return interp_event_refs_with_mentions(tags: tags, mention_indices: mention_indices)
|
||||
}
|
||||
|
||||
|
||||
func event_is_reply(_ refs: [EventRef]) -> Bool {
|
||||
return refs.contains { evref in
|
||||
return evref.is_reply != nil
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
//
|
||||
// HighlightEvent.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 4/22/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct HighlightEvent {
|
||||
let event: NostrEvent
|
||||
|
||||
var event_ref: String? = nil
|
||||
var url_ref: URL? = nil
|
||||
var context: String? = nil
|
||||
|
||||
static func parse(from ev: NostrEvent) -> HighlightEvent {
|
||||
var highlight = HighlightEvent(event: ev)
|
||||
|
||||
for tag in ev.tags {
|
||||
guard tag.count >= 2 else { continue }
|
||||
switch tag[0].string() {
|
||||
case "e": highlight.event_ref = tag[1].string()
|
||||
case "a": highlight.event_ref = tag[1].string()
|
||||
case "r": highlight.url_ref = URL(string: tag[1].string())
|
||||
case "context": highlight.context = tag[1].string()
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return highlight
|
||||
}
|
||||
}
|
||||
@@ -41,19 +41,11 @@ enum HomeResubFilter {
|
||||
}
|
||||
}
|
||||
|
||||
class HomeModel: ContactsDelegate {
|
||||
// The maximum amount of contacts placed on a home feed subscription filter.
|
||||
// If the user has more contacts, chunking or other techniques will be used to avoid sending huge filters
|
||||
let MAX_CONTACTS_ON_FILTER = 500
|
||||
|
||||
class HomeModel {
|
||||
// Don't trigger a user notification for events older than a certain age
|
||||
static let event_max_age_for_notification: TimeInterval = EVENT_MAX_AGE_FOR_NOTIFICATION
|
||||
|
||||
var damus_state: DamusState {
|
||||
didSet {
|
||||
self.load_our_stuff_from_damus_state()
|
||||
}
|
||||
}
|
||||
var damus_state: DamusState
|
||||
|
||||
// NDBTODO: let's get rid of this entirely, let nostrdb handle it
|
||||
var has_event: [String: Set<NoteId>] = [:]
|
||||
@@ -116,32 +108,6 @@ class HomeModel: ContactsDelegate {
|
||||
self.should_debounce_dms = false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Loading items from DamusState
|
||||
|
||||
/// This is called whenever DamusState gets set. This function is used to load or setup anything we need from the new DamusState
|
||||
func load_our_stuff_from_damus_state() {
|
||||
self.load_latest_contact_event_from_damus_state()
|
||||
}
|
||||
|
||||
/// This loads the latest contact event we have on file from NostrDB. This should be called as soon as we get the new DamusState
|
||||
/// Loading the latest contact list event into our `Contacts` instance from storage is important to avoid getting into weird states when the network is unreliable or when relays delete such information
|
||||
func load_latest_contact_event_from_damus_state() {
|
||||
guard let latest_contact_event_id_hex = damus_state.settings.latest_contact_event_id_hex else { return }
|
||||
guard let latest_contact_event_id = NoteId(hex: latest_contact_event_id_hex) else { return }
|
||||
guard let latest_contact_event: NdbNote = damus_state.ndb.lookup_note( latest_contact_event_id)?.unsafeUnownedValue?.to_owned() else { return }
|
||||
process_contact_event(state: damus_state, ev: latest_contact_event)
|
||||
damus_state.contacts.delegate = self
|
||||
}
|
||||
|
||||
// MARK: - ContactsDelegate functions
|
||||
|
||||
func latest_contact_event_changed(new_event: NostrEvent) {
|
||||
// When the latest user contact event has changed, save its ID so we know exactly where to find it next time
|
||||
damus_state.settings.latest_contact_event_id_hex = new_event.id.hex()
|
||||
}
|
||||
|
||||
// MARK: - Nostr event and subscription handling
|
||||
|
||||
func resubscribe(_ resubbing: Resubscribe) {
|
||||
if self.should_debounce_dms {
|
||||
@@ -184,7 +150,7 @@ class HomeModel: ContactsDelegate {
|
||||
}
|
||||
|
||||
switch kind {
|
||||
case .chat, .longform, .text, .highlight:
|
||||
case .chat, .longform, .text:
|
||||
handle_text_event(sub_id: sub_id, ev)
|
||||
case .contacts:
|
||||
handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
|
||||
@@ -313,14 +279,9 @@ class HomeModel: ContactsDelegate {
|
||||
@MainActor
|
||||
func handle_damus_app_notification(_ notification: DamusAppNotification) async {
|
||||
if self.notifications.insert_app_notification(notification: notification) {
|
||||
let last_notification = get_last_event(.notifications)
|
||||
if last_notification == nil || last_notification!.created_at < notification.last_event_at {
|
||||
save_last_event(NoteId.empty, created_at: notification.last_event_at, timeline: .notifications)
|
||||
// If we successfully inserted a new Damus App notification, switch ON the Damus App notification bit on our NewsEventsBits
|
||||
// This will cause the bell icon on the tab bar to display the purple dot indicating there is an unread notification
|
||||
self.notification_status.new_events = NewEventsBits(rawValue: self.notification_status.new_events.rawValue | NewEventsBits.damus_app_notifications.rawValue)
|
||||
}
|
||||
return
|
||||
// If we successfully inserted a new Damus App notification, switch ON the Damus App notification bit on our NewsEventsBits
|
||||
// This will cause the bell icon on the tab bar to display the purple dot indicating there is an unread notification
|
||||
self.notification_status.new_events = NewEventsBits(rawValue: self.notification_status.new_events.rawValue | NewEventsBits.damus_app_notifications.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,8 +510,7 @@ class HomeModel: ContactsDelegate {
|
||||
notifications_filter.limit = 500
|
||||
|
||||
var notifications_filters = [notifications_filter]
|
||||
let contacts_filter_chunks = contacts_filter.chunked(on: .authors, into: MAX_CONTACTS_ON_FILTER)
|
||||
var contacts_filters = contacts_filter_chunks + [our_contacts_filter, our_blocklist_filter, our_old_blocklist_filter]
|
||||
var contacts_filters = [contacts_filter, our_contacts_filter, our_blocklist_filter, our_old_blocklist_filter]
|
||||
var dms_filters = [dms_filter, our_dms_filter]
|
||||
let last_of_kind = get_last_of_kind(relay_id: relay_id)
|
||||
|
||||
@@ -586,7 +546,7 @@ class HomeModel: ContactsDelegate {
|
||||
func subscribe_to_home_filters(friends fs: [Pubkey]? = nil, relay_id: RelayURL? = nil) {
|
||||
// TODO: separate likes?
|
||||
var home_filter_kinds: [NostrKind] = [
|
||||
.text, .longform, .boost, .highlight
|
||||
.text, .longform, .boost
|
||||
]
|
||||
if !damus_state.settings.onlyzaps_mode {
|
||||
home_filter_kinds.append(.like)
|
||||
@@ -603,7 +563,7 @@ class HomeModel: ContactsDelegate {
|
||||
home_filter.authors = friends
|
||||
home_filter.limit = 500
|
||||
|
||||
var home_filters = home_filter.chunked(on: .authors, into: MAX_CONTACTS_ON_FILTER)
|
||||
var home_filters = [home_filter]
|
||||
|
||||
let followed_hashtags = Array(damus_state.contacts.get_followed_hashtags())
|
||||
if followed_hashtags.count != 0 {
|
||||
@@ -733,7 +693,7 @@ class HomeModel: ContactsDelegate {
|
||||
func got_new_dm(notifs: NewEventsBits, ev: NostrEvent) {
|
||||
notification_status.new_events = notifs
|
||||
|
||||
guard should_display_notification(state: damus_state, event: ev, mode: .local),
|
||||
guard should_display_notification(state: damus_state, event: ev),
|
||||
let notification_object = generate_local_notification_object(from: ev, state: damus_state)
|
||||
else {
|
||||
return
|
||||
@@ -1153,8 +1113,8 @@ func should_show_event(event: NostrEvent, damus_state: DamusState) -> Bool {
|
||||
)
|
||||
}
|
||||
|
||||
func should_show_event(state: DamusState, ev: NostrEvent) -> Bool {
|
||||
let event_muted = state.mutelist_manager.is_event_muted(ev)
|
||||
func should_show_event(state: DamusState, ev: NostrEvent, keypair: Keypair? = nil) -> Bool {
|
||||
let event_muted = state.mutelist_manager.is_event_muted(ev, keypair: keypair)
|
||||
if event_muted {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -49,32 +49,6 @@ enum MediaUpload {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
var mime_type: String {
|
||||
switch self.file_extension {
|
||||
case "jpg", "jpeg":
|
||||
return "image/jpg"
|
||||
case "png":
|
||||
return "image/png"
|
||||
case "gif":
|
||||
return "image/gif"
|
||||
case "tiff", "tif":
|
||||
return "image/tiff"
|
||||
case "mp4":
|
||||
return "video/mp4"
|
||||
case "ogg":
|
||||
return "video/ogg"
|
||||
case "webm":
|
||||
return "video/webm"
|
||||
default:
|
||||
switch self {
|
||||
case .image:
|
||||
return "image/jpg"
|
||||
case .video:
|
||||
return "video/mp4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ImageUploadModel: NSObject, URLSessionTaskDelegate, ObservableObject {
|
||||
|
||||
@@ -14,7 +14,6 @@ struct LongformEvent {
|
||||
var image: URL? = nil
|
||||
var summary: String? = nil
|
||||
var published_at: Date? = nil
|
||||
var labels: [String]? = nil
|
||||
|
||||
static func parse(from ev: NostrEvent) -> LongformEvent {
|
||||
var longform = LongformEvent(event: ev)
|
||||
@@ -27,10 +26,6 @@ struct LongformEvent {
|
||||
case "summary": longform.summary = tag[1].string()
|
||||
case "published_at":
|
||||
longform.published_at = Double(tag[1].string()).map { d in Date(timeIntervalSince1970: d) }
|
||||
case "t":
|
||||
if (longform.labels?.append(tag[1].string())) == nil {
|
||||
longform.labels = [tag[1].string()]
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
@@ -292,8 +292,9 @@ func make_post_tags(post_blocks: [Block], tags: [[String]]) -> PostTags {
|
||||
}
|
||||
|
||||
func post_to_event(post: NostrPost, keypair: FullKeypair) -> NostrEvent? {
|
||||
let tags = post.references.map({ r in r.tag }) + post.tags
|
||||
let post_blocks = parse_post_blocks(content: post.content)
|
||||
let post_tags = make_post_tags(post_blocks: post_blocks, tags: post.tags)
|
||||
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags)
|
||||
let content = post_tags.blocks
|
||||
.map(\.asString)
|
||||
.joined(separator: "")
|
||||
|
||||
@@ -8,27 +8,12 @@
|
||||
import Foundation
|
||||
|
||||
class MutelistManager {
|
||||
let user_keypair: Keypair
|
||||
private(set) var event: NostrEvent? = nil
|
||||
|
||||
var users: Set<MuteItem> = [] {
|
||||
didSet { self.reset_cache() }
|
||||
}
|
||||
var hashtags: Set<MuteItem> = [] {
|
||||
didSet { self.reset_cache() }
|
||||
}
|
||||
var threads: Set<MuteItem> = [] {
|
||||
didSet { self.reset_cache() }
|
||||
}
|
||||
var words: Set<MuteItem> = [] {
|
||||
didSet { self.reset_cache() }
|
||||
}
|
||||
|
||||
var muted_notes_cache: [NoteId: EventMuteStatus] = [:]
|
||||
|
||||
init(user_keypair: Keypair) {
|
||||
self.user_keypair = user_keypair
|
||||
}
|
||||
var users: Set<MuteItem> = []
|
||||
var hashtags: Set<MuteItem> = []
|
||||
var threads: Set<MuteItem> = []
|
||||
var words: Set<MuteItem> = []
|
||||
|
||||
func refresh_sets() {
|
||||
guard let referenced_mute_items = event?.referenced_mute_items else { return }
|
||||
@@ -56,10 +41,6 @@ class MutelistManager {
|
||||
threads = new_threads
|
||||
words = new_words
|
||||
}
|
||||
|
||||
func reset_cache() {
|
||||
self.muted_notes_cache = [:]
|
||||
}
|
||||
|
||||
func is_muted(_ item: MuteItem) -> Bool {
|
||||
switch item {
|
||||
@@ -74,8 +55,8 @@ class MutelistManager {
|
||||
}
|
||||
}
|
||||
|
||||
func is_event_muted(_ ev: NostrEvent) -> Bool {
|
||||
return self.event_muted_reason(ev) != nil
|
||||
func is_event_muted(_ ev: NostrEvent, keypair: Keypair? = nil) -> Bool {
|
||||
return event_muted_reason(ev, keypair: keypair) != nil
|
||||
}
|
||||
|
||||
func set_mutelist(_ ev: NostrEvent) {
|
||||
@@ -133,27 +114,15 @@ class MutelistManager {
|
||||
threads.remove(item)
|
||||
}
|
||||
}
|
||||
|
||||
func event_muted_reason(_ ev: NostrEvent) -> MuteItem? {
|
||||
if let cached_mute_status = self.muted_notes_cache[ev.id] {
|
||||
return cached_mute_status.mute_reason()
|
||||
}
|
||||
if let reason = self.compute_event_muted_reason(ev) {
|
||||
self.muted_notes_cache[ev.id] = .muted(reason: reason)
|
||||
return reason
|
||||
}
|
||||
self.muted_notes_cache[ev.id] = .not_muted
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
/// Check if an event is muted given a collection of ``MutedItem``.
|
||||
///
|
||||
/// - Parameter ev: The ``NostrEvent`` that you want to check the muted reason for.
|
||||
/// - Returns: The ``MuteItem`` that matched the event. Or `nil` if the event is not muted.
|
||||
func compute_event_muted_reason(_ ev: NostrEvent) -> MuteItem? {
|
||||
func event_muted_reason(_ ev: NostrEvent, keypair: Keypair? = nil) -> MuteItem? {
|
||||
// Events from the current user should not be muted.
|
||||
guard self.user_keypair.pubkey != ev.pubkey else { return nil }
|
||||
guard keypair?.pubkey != ev.pubkey else { return nil }
|
||||
|
||||
// Check if user is muted
|
||||
let check_user_item = MuteItem.user(ev.pubkey, nil)
|
||||
@@ -178,7 +147,7 @@ class MutelistManager {
|
||||
}
|
||||
|
||||
// Check if word is muted
|
||||
if let content: String = ev.maybe_get_content(self.user_keypair)?.lowercased() {
|
||||
if let keypair, let content: String = ev.maybe_get_content(keypair)?.lowercased() {
|
||||
for word in words {
|
||||
if case .word(let string, _) = word {
|
||||
if content.contains(string.lowercased()) {
|
||||
@@ -190,18 +159,4 @@ class MutelistManager {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
enum EventMuteStatus {
|
||||
case muted(reason: MuteItem)
|
||||
case not_muted
|
||||
|
||||
func mute_reason() -> MuteItem? {
|
||||
switch self {
|
||||
case .muted(reason: let reason):
|
||||
return reason
|
||||
case .not_muted:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import UIKit
|
||||
let EVENT_MAX_AGE_FOR_NOTIFICATION: TimeInterval = 12 * 60 * 60
|
||||
|
||||
func process_local_notification(state: HeadlessDamusState, event ev: NostrEvent) {
|
||||
guard should_display_notification(state: state, event: ev, mode: .local) else {
|
||||
guard should_display_notification(state: state, event: ev) else {
|
||||
// We should not display notification. Exit.
|
||||
return
|
||||
}
|
||||
@@ -25,12 +25,7 @@ func process_local_notification(state: HeadlessDamusState, event ev: NostrEvent)
|
||||
create_local_notification(profiles: state.profiles, notify: local_notification)
|
||||
}
|
||||
|
||||
func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent, mode: UserSettingsStore.NotificationsMode) -> Bool {
|
||||
// Do not show notification if it's coming from a mode different from the one selected by our user
|
||||
guard state.settings.notifications_mode == mode else {
|
||||
return false
|
||||
}
|
||||
|
||||
func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent) -> Bool {
|
||||
if ev.known_kind == nil {
|
||||
return false
|
||||
}
|
||||
@@ -42,7 +37,7 @@ func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent
|
||||
}
|
||||
|
||||
// Don't show notifications that match mute list.
|
||||
if state.mutelist_manager.is_event_muted(ev) {
|
||||
if state.mutelist_manager.is_event_muted(ev, keypair: state.keypair) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,12 @@ import Foundation
|
||||
struct NostrPost {
|
||||
let kind: NostrKind
|
||||
let content: String
|
||||
let references: [RefId]
|
||||
let tags: [[String]]
|
||||
|
||||
init(content: String, kind: NostrKind = .text, tags: [[String]] = []) {
|
||||
init(content: String, references: [RefId], kind: NostrKind = .text, tags: [[String]] = []) {
|
||||
self.content = content
|
||||
self.references = references
|
||||
self.kind = kind
|
||||
self.tags = tags
|
||||
}
|
||||
|
||||
@@ -60,11 +60,11 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
damus.pool.unsubscribe(sub_id: sub_id)
|
||||
damus.pool.unsubscribe(sub_id: prof_subid)
|
||||
}
|
||||
|
||||
|
||||
func subscribe() {
|
||||
var text_filter = NostrFilter(kinds: [.text, .longform, .highlight])
|
||||
var text_filter = NostrFilter(kinds: [.text, .longform])
|
||||
var profile_filter = NostrFilter(kinds: [.contacts, .metadata, .boost])
|
||||
|
||||
|
||||
profile_filter.authors = [pubkey]
|
||||
|
||||
text_filter.authors = [pubkey]
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
//
|
||||
// PushNotificationClient.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2024-05-17.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct PushNotificationClient {
|
||||
let keypair: Keypair
|
||||
let settings: UserSettingsStore
|
||||
private(set) var device_token: Data? = nil
|
||||
|
||||
mutating func set_device_token(new_device_token: Data) async throws {
|
||||
self.device_token = new_device_token
|
||||
if settings.enable_experimental_push_notifications && settings.notifications_mode == .push {
|
||||
try await self.send_token()
|
||||
}
|
||||
}
|
||||
|
||||
func send_token() async throws {
|
||||
guard let device_token else { return }
|
||||
// Send the device token and pubkey to the server
|
||||
let token = device_token.map { String(format: "%02.2hhx", $0) }.joined()
|
||||
|
||||
Log.info("Sending device token to server: %s", for: .push_notifications, token)
|
||||
|
||||
let pubkey = self.keypair.pubkey
|
||||
|
||||
// Send those as JSON to the server
|
||||
let json: [String: Any] = ["deviceToken": token, "pubkey": pubkey.hex()]
|
||||
|
||||
// create post request
|
||||
let url = self.settings.send_device_token_to_localhost ? Constants.DEVICE_TOKEN_RECEIVER_TEST_URL : Constants.DEVICE_TOKEN_RECEIVER_PRODUCTION_URL
|
||||
let json_data = try JSONSerialization.data(withJSONObject: json)
|
||||
|
||||
|
||||
let (data, response) = try await make_nip98_authenticated_request(
|
||||
method: .post,
|
||||
url: url,
|
||||
payload: json_data,
|
||||
payload_type: .json,
|
||||
auth_keypair: self.keypair
|
||||
)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
switch httpResponse.statusCode {
|
||||
case 200:
|
||||
Log.info("Sent device token to Damus push notification server successfully", for: .push_notifications)
|
||||
default:
|
||||
Log.error("Error in sending device_token to Damus push notification server. HTTP status code: %d; Response: %s", for: .push_notifications, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
|
||||
throw ClientError.http_response_error(status_code: httpResponse.statusCode, response: data)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func revoke_token() async throws {
|
||||
guard let device_token else { return }
|
||||
// Send the device token and pubkey to the server
|
||||
let token = device_token.map { String(format: "%02.2hhx", $0) }.joined()
|
||||
|
||||
Log.info("Revoking device token from server: %s", for: .push_notifications, token)
|
||||
|
||||
let pubkey = self.keypair.pubkey
|
||||
|
||||
// Send those as JSON to the server
|
||||
let json: [String: Any] = ["deviceToken": token, "pubkey": pubkey.hex()]
|
||||
|
||||
// create post request
|
||||
let url = self.settings.send_device_token_to_localhost ? Constants.DEVICE_TOKEN_REVOKER_TEST_URL : Constants.DEVICE_TOKEN_REVOKER_PRODUCTION_URL
|
||||
let json_data = try JSONSerialization.data(withJSONObject: json)
|
||||
|
||||
|
||||
let (data, response) = try await make_nip98_authenticated_request(
|
||||
method: .post,
|
||||
url: url,
|
||||
payload: json_data,
|
||||
payload_type: .json,
|
||||
auth_keypair: self.keypair
|
||||
)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
switch httpResponse.statusCode {
|
||||
case 200:
|
||||
Log.info("Sent device token removal request to Damus push notification server successfully", for: .push_notifications)
|
||||
default:
|
||||
Log.error("Error in sending device_token removal to Damus push notification server. HTTP status code: %d; Response: %s", for: .push_notifications, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
|
||||
throw ClientError.http_response_error(status_code: httpResponse.statusCode, response: data)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Helper structures
|
||||
|
||||
extension PushNotificationClient {
|
||||
enum ClientError: Error {
|
||||
case http_response_error(status_code: Int, response: Data)
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ class SearchHomeModel: ObservableObject {
|
||||
guard sub_id == self.base_subid || sub_id == self.profiles_subid else {
|
||||
return
|
||||
}
|
||||
if ev.is_textlike && should_show_event(state: damus_state, ev: ev) && !ev.is_reply()
|
||||
if ev.is_textlike && should_show_event(state: damus_state, ev: ev) && !ev.is_reply(damus_state.keypair)
|
||||
{
|
||||
if !damus_state.settings.multiple_events_per_pubkey && seen_pubkey.contains(ev.pubkey) {
|
||||
return
|
||||
|
||||
@@ -36,7 +36,7 @@ class SearchModel: ObservableObject {
|
||||
func subscribe() {
|
||||
// since 1 month
|
||||
search.limit = self.limit
|
||||
search.kinds = [.text, .like, .longform, .highlight]
|
||||
search.kinds = [.text, .like, .longform]
|
||||
|
||||
//likes_filter.ids = ref_events.referenced_ids!
|
||||
|
||||
|
||||
@@ -20,13 +20,7 @@ class ThreadModel: ObservableObject {
|
||||
self.original_event = event
|
||||
add_event(event, keypair: damus_state.keypair)
|
||||
}
|
||||
|
||||
func events() -> [NostrEvent] {
|
||||
return Array(event_map).sorted(by: { a, b in
|
||||
return a.created_at < b.created_at
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
var is_original: Bool {
|
||||
return original_event.id == event.id
|
||||
}
|
||||
@@ -66,7 +60,7 @@ class ThreadModel: ObservableObject {
|
||||
var event_filter = NostrFilter()
|
||||
var ref_events = NostrFilter()
|
||||
|
||||
let thread_id = event.thread_id()
|
||||
let thread_id = event.thread_id(keypair: .empty)
|
||||
|
||||
ref_events.referenced_ids = [thread_id, event.id]
|
||||
ref_events.kinds = [.text]
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
//
|
||||
// Trie.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 6/26/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Tree data structure of all the substring permutations of a collection of strings optimized for searching for values of type V.
|
||||
///
|
||||
/// Each node in the tree can have child nodes.
|
||||
/// Each node represents a single character in substrings, and each of its child nodes represent the subsequent character in those substrings.
|
||||
///
|
||||
/// A node that has no children mean that there are no substrings with any additional characters beyond the branch of letters leading up to that node.
|
||||
///
|
||||
/// A node that has values mean that there are strings that end in the character represented by the node and contain the substring represented by the branch of letters leading up to that node.
|
||||
///
|
||||
/// https://en.wikipedia.org/wiki/Trie
|
||||
class Trie<V: Hashable> {
|
||||
private var children: [Character : Trie] = [:]
|
||||
|
||||
/// Separate exact matches from strict substrings so that exact matches appear first in returned results.
|
||||
private var exactMatchValues = Set<V>()
|
||||
private var substringMatchValues = Set<V>()
|
||||
|
||||
private var parent: Trie? = nil
|
||||
}
|
||||
|
||||
extension Trie {
|
||||
var hasChildren: Bool {
|
||||
return !self.children.isEmpty
|
||||
}
|
||||
|
||||
var hasValues: Bool {
|
||||
return !self.exactMatchValues.isEmpty || !self.substringMatchValues.isEmpty
|
||||
}
|
||||
|
||||
/// Finds the branch that matches the specified key and returns the values from all of its descendant nodes.
|
||||
func find(key: String) -> [V] {
|
||||
var currentNode = self
|
||||
|
||||
// Find branch with matching prefix.
|
||||
for char in key {
|
||||
if let child = currentNode.children[char] {
|
||||
currentNode = child
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Perform breadth-first search from matching branch and collect values from all descendants.
|
||||
var substringMatches = Set<V>(currentNode.substringMatchValues)
|
||||
var queue = Array(currentNode.children.values)
|
||||
|
||||
while !queue.isEmpty {
|
||||
let node = queue.removeFirst()
|
||||
substringMatches.formUnion(node.exactMatchValues)
|
||||
substringMatches.formUnion(node.substringMatchValues)
|
||||
queue.append(contentsOf: node.children.values)
|
||||
}
|
||||
|
||||
// Prioritize exact matches to be returned first, and then remove exact matches from the set of partial substring matches that are appended afterward.
|
||||
return Array(currentNode.exactMatchValues) + (substringMatches.subtracting(currentNode.exactMatchValues))
|
||||
}
|
||||
|
||||
/// Inserts value of type V into this trie for the specified key. This function stores all substring endings of the key, not only the key itself.
|
||||
/// Runtime performance is O(n^2) and storage cost is O(n), where n is the number of characters in the key.
|
||||
func insert(key: String, value: V) {
|
||||
// Create root branches for each character of the key to enable substring searches instead of only just prefix searches.
|
||||
// Hence the nested loop.
|
||||
for i in 0..<key.count {
|
||||
var currentNode = self
|
||||
|
||||
// Find branch with matching prefix.
|
||||
for char in key[key.index(key.startIndex, offsetBy: i)...] {
|
||||
if let child = currentNode.children[char] {
|
||||
currentNode = child
|
||||
} else {
|
||||
let child = Trie()
|
||||
child.parent = currentNode
|
||||
currentNode.children[char] = child
|
||||
currentNode = child
|
||||
}
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
currentNode.exactMatchValues.insert(value)
|
||||
} else {
|
||||
currentNode.substringMatchValues.insert(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes value of type V from this trie for the specified key.
|
||||
func remove(key: String, value: V) {
|
||||
for i in 0..<key.count {
|
||||
var currentNode = self
|
||||
|
||||
var foundLeafNode = true
|
||||
|
||||
// Find branch with matching prefix.
|
||||
for j in i..<key.count {
|
||||
let char = key[key.index(key.startIndex, offsetBy: j)]
|
||||
|
||||
if let child = currentNode.children[char] {
|
||||
currentNode = child
|
||||
} else {
|
||||
foundLeafNode = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if foundLeafNode {
|
||||
currentNode.exactMatchValues.remove(value)
|
||||
currentNode.substringMatchValues.remove(value)
|
||||
|
||||
// Clean up the tree if this leaf node no longer holds values or children.
|
||||
for j in (i..<key.count).reversed() {
|
||||
if let parent = currentNode.parent, !currentNode.hasValues && !currentNode.hasChildren {
|
||||
currentNode = parent
|
||||
let char = key[key.index(key.startIndex, offsetBy: j)]
|
||||
currentNode.children.removeValue(forKey: char)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// UserSearchCache.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 6/27/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Cache of searchable users by name, display_name, NIP-05 identifier, or own contact list petname.
|
||||
/// Optimized for fast searches of substrings by using a Trie.
|
||||
/// Optimal for performing user searches that could be initiated by typing quickly on a keyboard into a text input field.
|
||||
|
||||
// TODO: replace with lmdb (the b tree should handle this just fine ?)
|
||||
// we just need a name to profile index
|
||||
class UserSearchCache {
|
||||
private let trie = Trie<Pubkey>()
|
||||
|
||||
func search(key: String) -> [Pubkey] {
|
||||
let results = trie.find(key: key)
|
||||
return results
|
||||
}
|
||||
|
||||
/// Computes the differences between an old profile, if it exists, and a new profile, and updates the user search cache accordingly.
|
||||
@MainActor
|
||||
func updateProfile(id: Pubkey, profiles: Profiles, oldProfile: Profile?, newProfile: Profile) {
|
||||
// Remove searchable keys tied to the old profile if they differ from the new profile
|
||||
// to keep the trie clean without empty nodes while avoiding excessive graph searching.
|
||||
if let oldProfile {
|
||||
if let oldName = oldProfile.name, newProfile.name?.caseInsensitiveCompare(oldName) != .orderedSame {
|
||||
trie.remove(key: oldName.lowercased(), value: id)
|
||||
}
|
||||
if let oldDisplayName = oldProfile.display_name, newProfile.display_name?.caseInsensitiveCompare(oldDisplayName) != .orderedSame {
|
||||
trie.remove(key: oldDisplayName.lowercased(), value: id)
|
||||
}
|
||||
if let oldNip05 = oldProfile.nip05, newProfile.nip05?.caseInsensitiveCompare(oldNip05) != .orderedSame {
|
||||
trie.remove(key: oldNip05.lowercased(), value: id)
|
||||
}
|
||||
}
|
||||
|
||||
addProfile(id: id, profiles: profiles, profile: newProfile)
|
||||
}
|
||||
|
||||
/// Adds a profile to the user search cache.
|
||||
@MainActor
|
||||
private func addProfile(id: Pubkey, profiles: Profiles, profile: Profile) {
|
||||
// Searchable by name.
|
||||
if let name = profile.name {
|
||||
trie.insert(key: name.lowercased(), value: id)
|
||||
}
|
||||
|
||||
// Searchable by display name.
|
||||
if let displayName = profile.display_name {
|
||||
trie.insert(key: displayName.lowercased(), value: id)
|
||||
}
|
||||
|
||||
// Searchable by NIP-05 identifier.
|
||||
if let nip05 = profiles.is_validated(id) {
|
||||
trie.insert(key: "\(nip05.username.lowercased())@\(nip05.host.lowercased())", value: id)
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the diffences between an old contacts event and a new contacts event for our own user, and updates the search cache accordingly.
|
||||
func updateOwnContactsPetnames(id: Pubkey, oldEvent: NostrEvent?, newEvent: NostrEvent) {
|
||||
guard newEvent.known_kind == .contacts && newEvent.pubkey == id else {
|
||||
return
|
||||
}
|
||||
|
||||
var petnames: [Pubkey: String] = [:]
|
||||
for tag in newEvent.tags {
|
||||
guard tag.count > 3,
|
||||
let chr = tag[0].single_char, chr == "p",
|
||||
let id = tag[1].id()
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
let pubkey = Pubkey(id)
|
||||
|
||||
petnames[pubkey] = tag[3].string()
|
||||
}
|
||||
|
||||
// Compute the diff with the old contacts list, if it exists,
|
||||
// mark the ones that are the same to not be removed from the user search cache,
|
||||
// and remove the old ones that are different from the user search cache.
|
||||
if let oldEvent, oldEvent.known_kind == .contacts, oldEvent.pubkey == id {
|
||||
for tag in oldEvent.tags {
|
||||
guard tag.count >= 4,
|
||||
tag[0].matches_char("p"),
|
||||
let id = tag[1].id()
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
let pubkey = Pubkey(id)
|
||||
|
||||
let oldPetname = tag[3].string()
|
||||
|
||||
if let newPetname = petnames[pubkey] {
|
||||
if newPetname.caseInsensitiveCompare(oldPetname) == .orderedSame {
|
||||
petnames.removeValue(forKey: pubkey)
|
||||
} else {
|
||||
trie.remove(key: oldPetname, value: pubkey)
|
||||
}
|
||||
} else {
|
||||
trie.remove(key: oldPetname, value: pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the new petnames to the user search cache.
|
||||
for (pubkey, petname) in petnames {
|
||||
trie.insert(key: petname, value: pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,14 +96,6 @@ class UserSettingsStore: ObservableObject {
|
||||
static var shared: UserSettingsStore? = nil
|
||||
static var bool_options = Set<String>()
|
||||
|
||||
static func globally_load_for(pubkey: Pubkey) -> UserSettingsStore {
|
||||
// dumb stuff needed for property wrappers
|
||||
UserSettingsStore.pubkey = pubkey
|
||||
let settings = UserSettingsStore()
|
||||
UserSettingsStore.shared = settings
|
||||
return settings
|
||||
}
|
||||
|
||||
@StringSetting(key: "default_wallet", default_value: .system_default_wallet)
|
||||
var default_wallet: Wallet
|
||||
|
||||
@@ -155,9 +147,6 @@ class UserSettingsStore: ObservableObject {
|
||||
@Setting(key: "like_notification", default_value: true)
|
||||
var like_notification: Bool
|
||||
|
||||
@StringSetting(key: "notifications_mode", default_value: .local)
|
||||
var notifications_mode: NotificationsMode
|
||||
|
||||
@Setting(key: "notification_only_from_following", default_value: false)
|
||||
var notification_only_from_following: Bool
|
||||
|
||||
@@ -323,42 +312,6 @@ class UserSettingsStore: ObservableObject {
|
||||
return internal_winetranslate_api_key != nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Internal, hidden settings
|
||||
|
||||
@Setting(key: "latest_contact_event_id", default_value: nil)
|
||||
var latest_contact_event_id_hex: String?
|
||||
|
||||
|
||||
// MARK: Helper types
|
||||
|
||||
enum NotificationsMode: String, CaseIterable, Identifiable, StringCodable, Equatable {
|
||||
var id: String { self.rawValue }
|
||||
|
||||
func to_string() -> String {
|
||||
return rawValue
|
||||
}
|
||||
|
||||
init?(from string: String) {
|
||||
guard let notifications_mode = NotificationsMode(rawValue: string) else {
|
||||
return nil
|
||||
}
|
||||
self = notifications_mode
|
||||
}
|
||||
|
||||
func text_description() -> String {
|
||||
switch self {
|
||||
case .local:
|
||||
NSLocalizedString("Local", comment: "Option for notification mode setting: Local notification mode")
|
||||
case .push:
|
||||
NSLocalizedString("Push", comment: "Option for notification mode setting: Push notification mode")
|
||||
}
|
||||
}
|
||||
|
||||
case local
|
||||
case push
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func pk_setting_key(_ pubkey: Pubkey, key: String) -> String {
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
//
|
||||
// VideoCache.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D'Aquino on 2024-04-01.
|
||||
//
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
// Default expiry time of only 1 day to prevent using too much storage
|
||||
fileprivate let DEFAULT_EXPIRY_TIME: TimeInterval = 60*60*24
|
||||
// Default cache directory is in the system-provided caches directory, so that the operating system can delete files when it needs storage space
|
||||
// (https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/FileSystemOverview/FileSystemOverview.html)
|
||||
fileprivate let DEFAULT_CACHE_DIRECTORY_PATH: URL? = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("video_cache")
|
||||
|
||||
struct VideoCache {
|
||||
private let cache_url: URL
|
||||
private let expiry_time: TimeInterval
|
||||
static let standard: VideoCache? = try? VideoCache()
|
||||
|
||||
init?(cache_url: URL? = nil, expiry_time: TimeInterval = DEFAULT_EXPIRY_TIME) throws {
|
||||
guard let cache_url_to_apply = cache_url ?? DEFAULT_CACHE_DIRECTORY_PATH else { return nil }
|
||||
self.cache_url = cache_url_to_apply
|
||||
self.expiry_time = expiry_time
|
||||
|
||||
// Create the cache directory if it doesn't exist
|
||||
do {
|
||||
try FileManager.default.createDirectory(at: self.cache_url, withIntermediateDirectories: true, attributes: nil)
|
||||
} catch {
|
||||
Log.error("Could not create cache directory: %s", for: .storage, error.localizedDescription)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks for a cached video and returns its URL if available, otherwise downloads and caches the video.
|
||||
func maybe_cached_url_for(video_url: URL) throws -> URL {
|
||||
let cached_url = url_to_cached_url(url: video_url)
|
||||
|
||||
if FileManager.default.fileExists(atPath: cached_url.path) {
|
||||
// Check if the cached video has expired
|
||||
let file_attributes = try FileManager.default.attributesOfItem(atPath: cached_url.path)
|
||||
if let modification_date = file_attributes[.modificationDate] as? Date, Date().timeIntervalSince(modification_date) <= expiry_time {
|
||||
// Video is not expired
|
||||
return cached_url
|
||||
} else {
|
||||
Task {
|
||||
// Video is expired, delete and re-download on the background
|
||||
try FileManager.default.removeItem(at: cached_url)
|
||||
return try await download_and_cache_video(from: video_url)
|
||||
}
|
||||
return video_url
|
||||
}
|
||||
} else {
|
||||
Task {
|
||||
// Video is not cached, download and cache on the background
|
||||
return try await download_and_cache_video(from: video_url)
|
||||
}
|
||||
return video_url
|
||||
}
|
||||
}
|
||||
|
||||
/// Downloads video content using URLSession and caches it to disk.
|
||||
private func download_and_cache_video(from url: URL) async throws -> URL {
|
||||
let (data, response) = try await URLSession.shared.data(from: url)
|
||||
|
||||
guard let http_response = response as? HTTPURLResponse,
|
||||
200..<300 ~= http_response.statusCode else {
|
||||
throw URLError(.badServerResponse)
|
||||
}
|
||||
|
||||
let destination_url = url_to_cached_url(url: url)
|
||||
|
||||
try data.write(to: destination_url)
|
||||
return destination_url
|
||||
}
|
||||
|
||||
func url_to_cached_url(url: URL) -> URL {
|
||||
let hashed_url = hash_url(url)
|
||||
let file_extension = url.pathExtension
|
||||
return self.cache_url.appendingPathComponent(hashed_url + "." + file_extension)
|
||||
}
|
||||
|
||||
/// Deletes all cached videos older than the expiry time.
|
||||
func periodic_purge(completion: ((Error?) -> Void)? = nil) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
Log.info("Starting periodic video cache purge", for: .storage)
|
||||
let file_manager = FileManager.default
|
||||
do {
|
||||
let cached_files = try file_manager.contentsOfDirectory(at: self.cache_url, includingPropertiesForKeys: [.contentModificationDateKey], options: .skipsHiddenFiles)
|
||||
|
||||
for file in cached_files {
|
||||
let attributes = try file.resourceValues(forKeys: [.contentModificationDateKey])
|
||||
if let modification_date = attributes.contentModificationDate, Date().timeIntervalSince(modification_date) > self.expiry_time {
|
||||
try file_manager.removeItem(at: file)
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
completion?(nil)
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
completion?(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hashes the URL using SHA-256
|
||||
private func hash_url(_ url: URL) -> String {
|
||||
let data = Data(url.absoluteString.utf8)
|
||||
let hashed_data = SHA256.hash(data: data)
|
||||
return hashed_data.compactMap { String(format: "%02x", $0) }.joined()
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
//
|
||||
// ThreadReply.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2024-05-09.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
struct ThreadReply {
|
||||
let root: NoteRef
|
||||
let reply: NoteRef
|
||||
let mention: Mention<NoteRef>?
|
||||
|
||||
var is_reply_to_root: Bool {
|
||||
return root.id == reply.id
|
||||
}
|
||||
|
||||
init(root: NoteRef, reply: NoteRef, mention: Mention<NoteRef>?) {
|
||||
self.root = root
|
||||
self.reply = reply
|
||||
self.mention = mention
|
||||
}
|
||||
|
||||
init?(tags: TagsSequence) {
|
||||
guard let tr = interpret_event_refs_ndb(tags: tags) else {
|
||||
return nil
|
||||
}
|
||||
self = tr
|
||||
}
|
||||
}
|
||||
@@ -54,68 +54,4 @@ struct NostrFilter: Codable, Equatable {
|
||||
public static func filter_hashtag(_ htags: [String]) -> NostrFilter {
|
||||
NostrFilter(hashtag: htags.map { $0.lowercased() })
|
||||
}
|
||||
|
||||
/// Splits the filter on a given filter path/axis into chunked filters
|
||||
///
|
||||
/// - Parameter path: The path where chunking should be done
|
||||
/// - Parameter chunk_size: The maximum size of each chunk.
|
||||
/// - Returns: An array of arrays, where each contained array is a chunk of the original array with up to `size` elements.
|
||||
func chunked(on path: ChunkPath, into chunk_size: Int) -> [Self] {
|
||||
let chunked_slices = self.get_slice(from: path).chunked(into: chunk_size)
|
||||
var chunked_filters: [NostrFilter] = []
|
||||
for chunked_slice in chunked_slices {
|
||||
var chunked_filter = self
|
||||
chunked_filter.apply_slice(chunked_slice)
|
||||
chunked_filters.append(chunked_filter)
|
||||
}
|
||||
return chunked_filters
|
||||
}
|
||||
|
||||
/// Gets a slice from a NostrFilter on a given path/axis
|
||||
///
|
||||
/// - Parameter path: The path where chunking should be done
|
||||
/// - Parameter chunk_size: The maximum size of each chunk.
|
||||
/// - Returns: An array of arrays, where each contained array is a chunk of the original array with up to `size` elements.
|
||||
func get_slice(from path: ChunkPath) -> Slice {
|
||||
switch path {
|
||||
case .pubkeys:
|
||||
return .pubkeys(self.pubkeys)
|
||||
case .authors:
|
||||
return .authors(self.authors)
|
||||
}
|
||||
}
|
||||
|
||||
/// Overrides one member/axis of a NostrFilter using a specific slice
|
||||
/// - Parameter slice: The slice to be applied on this NostrFilter
|
||||
mutating func apply_slice(_ slice: Slice) {
|
||||
switch slice {
|
||||
case .pubkeys(let pubkeys):
|
||||
self.pubkeys = pubkeys
|
||||
case .authors(let authors):
|
||||
self.authors = authors
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// A path to one of the axes of a NostrFilter.
|
||||
enum ChunkPath {
|
||||
case pubkeys
|
||||
case authors
|
||||
// Other paths/axes not supported yet
|
||||
}
|
||||
|
||||
/// Represents the value of a single axis of a NostrFilter
|
||||
enum Slice {
|
||||
case pubkeys([Pubkey]?)
|
||||
case authors([Pubkey]?)
|
||||
|
||||
func chunked(into chunk_size: Int) -> [Slice] {
|
||||
switch self {
|
||||
case .pubkeys(let array):
|
||||
return (array ?? []).chunked(into: chunk_size).map({ .pubkeys($0) })
|
||||
case .authors(let array):
|
||||
return (array ?? []).chunked(into: chunk_size).map({ .authors($0) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ enum NostrKind: UInt32, Codable {
|
||||
case longform = 30023
|
||||
case zap = 9735
|
||||
case zap_request = 9734
|
||||
case highlight = 9802
|
||||
case nwc_request = 23194
|
||||
case nwc_response = 23195
|
||||
case http_auth = 27235
|
||||
|
||||
@@ -226,23 +226,19 @@ class RelayPool {
|
||||
print("queueing request for \(relay)")
|
||||
request_queue.append(QueuedRequest(req: r, relay: relay, skip_ephemeral: skip_ephemeral))
|
||||
}
|
||||
|
||||
func send_raw_to_local_ndb(_ req: NostrRequestType) {
|
||||
// send to local relay (nostrdb)
|
||||
switch req {
|
||||
case .typical(let r):
|
||||
if case .event = r, let rstr = make_nostr_req(r) {
|
||||
let _ = ndb.process_client_event(rstr)
|
||||
}
|
||||
case .custom(let string):
|
||||
let _ = ndb.process_client_event(string)
|
||||
}
|
||||
}
|
||||
|
||||
func send_raw(_ req: NostrRequestType, to: [RelayURL]? = nil, skip_ephemeral: Bool = true) {
|
||||
let relays = to.map{ get_relays($0) } ?? self.relays
|
||||
|
||||
self.send_raw_to_local_ndb(req)
|
||||
// send to local relay (nostrdb)
|
||||
switch req {
|
||||
case .typical(let r):
|
||||
if case .event = r, let rstr = make_nostr_req(r) {
|
||||
let _ = ndb.process_client_event(rstr)
|
||||
}
|
||||
case .custom(let string):
|
||||
let _ = ndb.process_client_event(string)
|
||||
}
|
||||
|
||||
for relay in relays {
|
||||
if req.is_read && !(relay.descriptor.info.read ?? true) {
|
||||
|
||||
+2
-20
File diff suppressed because one or more lines are too long
@@ -17,7 +17,7 @@ class CompatibleText: Equatable {
|
||||
return AnyView(
|
||||
VStack {
|
||||
Image("warning")
|
||||
Text("This note contains too many items and cannot be rendered", comment: "Error message indicating that a note is too big and cannot be rendered")
|
||||
Text(NSLocalizedString("This note contains too many items and cannot be rendered", comment: "Error message indicating that a note is too big and cannot be rendered"))
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
@@ -10,10 +10,8 @@ import Foundation
|
||||
class Constants {
|
||||
//static let EXAMPLE_DEMOS: DamusState = .empty
|
||||
static let DAMUS_APP_GROUP_IDENTIFIER: String = "group.com.damus"
|
||||
static let DEVICE_TOKEN_RECEIVER_PRODUCTION_URL: URL = URL(string: "http://45.33.32.5:8000/user-info")!
|
||||
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 DEVICE_TOKEN_REVOKER_PRODUCTION_URL: URL = URL(string: "http://45.33.32.5:8000/user-info/remove")!
|
||||
static let DEVICE_TOKEN_REVOKER_TEST_URL: URL = URL(string: "http://localhost:8000/user-info/remove")!
|
||||
static let MAIN_APP_BUNDLE_IDENTIFIER: String = "com.jb55.damus2"
|
||||
static let NOTIFICATION_EXTENSION_BUNDLE_IDENTIFIER: String = "com.jb55.damus2.DamusNotificationService"
|
||||
|
||||
|
||||
@@ -97,13 +97,13 @@ class EventCache {
|
||||
// TODO: remove me and change code to use ndb directly
|
||||
private let ndb: Ndb
|
||||
private var events: [NoteId: NostrEvent] = [:]
|
||||
private var replies = ReplyMap()
|
||||
private var cancellable: AnyCancellable?
|
||||
private var image_metadata: [String: ImageMetadataState] = [:] // lowercased URL key
|
||||
private var event_data: [NoteId: EventData] = [:]
|
||||
var replies = ReplyMap()
|
||||
|
||||
//private var thread_latest: [String: Int64]
|
||||
|
||||
|
||||
init(ndb: Ndb) {
|
||||
self.ndb = ndb
|
||||
cancellable = NotificationCenter.default.publisher(
|
||||
@@ -169,7 +169,7 @@ class EventCache {
|
||||
var ev = event
|
||||
|
||||
while true {
|
||||
guard let direct_reply = ev.direct_replies(),
|
||||
guard let direct_reply = ev.direct_replies(keypair).last,
|
||||
let next_ev = lookup(direct_reply), next_ev != ev
|
||||
else {
|
||||
break
|
||||
@@ -183,11 +183,11 @@ class EventCache {
|
||||
}
|
||||
|
||||
func add_replies(ev: NostrEvent, keypair: Keypair) {
|
||||
if let reply = ev.direct_replies() {
|
||||
for reply in ev.direct_replies(keypair) {
|
||||
replies.add(id: reply, reply_id: ev.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func child_events(event: NostrEvent) -> [NostrEvent] {
|
||||
guard let xs = replies.lookup(event.id) else {
|
||||
return []
|
||||
@@ -218,16 +218,7 @@ class EventCache {
|
||||
*/
|
||||
|
||||
func lookup(_ evid: NoteId) -> NostrEvent? {
|
||||
if let ev = events[evid] {
|
||||
return ev
|
||||
}
|
||||
|
||||
if let ev = self.ndb.lookup_note(evid)?.unsafeUnownedValue?.to_owned() {
|
||||
events[ev.id] = ev
|
||||
return ev
|
||||
}
|
||||
|
||||
return nil
|
||||
return events[evid]
|
||||
}
|
||||
|
||||
func insert(_ ev: NostrEvent) {
|
||||
@@ -248,11 +239,6 @@ func should_translate(event: NostrEvent, our_keypair: Keypair, settings: UserSet
|
||||
guard settings.can_translate else {
|
||||
return false
|
||||
}
|
||||
|
||||
// don't translate reposts, longform, etc
|
||||
if event.kind != 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Do not translate self-authored notes if logged in with a private key
|
||||
// as we can assume the user can understand their own notes.
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
//
|
||||
// Array.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2024-05-10.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Array {
|
||||
/// Splits the array into chunks of the specified size.
|
||||
/// - Parameter size: The maximum size of each chunk.
|
||||
/// - Returns: An array of arrays, where each contained array is a chunk of the original array with up to `size` elements.
|
||||
func chunked(into size: Int) -> [[Element]] {
|
||||
guard size > 0 else { return [self] }
|
||||
return stride(from: 0, to: count, by: size).map {
|
||||
Array(self[$0..<Swift.min($0 + size, count)])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element: Equatable {
|
||||
mutating func removeAll(equalTo item: Element) {
|
||||
self.removeAll(where: { $0 == item })
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
//
|
||||
// VectorMath.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2024-06-17.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension CGPoint {
|
||||
/// Summing a vector to a point
|
||||
static func +(lhs: CGPoint, rhs: CGVector) -> CGPoint {
|
||||
return CGPoint(x: lhs.x + rhs.dx, y: lhs.y + rhs.dy)
|
||||
}
|
||||
|
||||
/// Subtracting a vector from a point
|
||||
static func -(lhs: CGPoint, rhs: CGVector) -> CGPoint {
|
||||
return CGPoint(x: lhs.x - rhs.dx, y: lhs.y - rhs.dy)
|
||||
}
|
||||
}
|
||||
|
||||
extension CGVector {
|
||||
/// Multiplying a vector by a scalar
|
||||
static func *(lhs: CGVector, rhs: CGFloat) -> CGVector {
|
||||
return CGVector(dx: lhs.dx * rhs, dy: lhs.dy * rhs)
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,7 @@ func processImage(image: UIImage) -> URL? {
|
||||
}
|
||||
|
||||
fileprivate func processImage(source: CGImageSource, fileExtension: String) -> URL? {
|
||||
let destinationURL = generateUniqueTemporaryMediaURL(fileExtension: fileExtension)
|
||||
let destinationURL = createMediaURL(fileExtension: fileExtension)
|
||||
|
||||
guard let destination = removeGPSDataFromImage(source: source, url: destinationURL) else { return nil }
|
||||
|
||||
@@ -45,7 +45,7 @@ func processVideo(videoURL: URL) -> URL? {
|
||||
}
|
||||
|
||||
fileprivate func saveVideoToTemporaryFolder(videoURL: URL) -> URL? {
|
||||
let destinationURL = generateUniqueTemporaryMediaURL(fileExtension: videoURL.pathExtension)
|
||||
let destinationURL = createMediaURL(fileExtension: videoURL.pathExtension)
|
||||
|
||||
do {
|
||||
try FileManager.default.copyItem(at: videoURL, to: destinationURL)
|
||||
@@ -57,7 +57,7 @@ fileprivate func saveVideoToTemporaryFolder(videoURL: URL) -> URL? {
|
||||
}
|
||||
|
||||
/// Generate a temporary URL with a unique filename
|
||||
func generateUniqueTemporaryMediaURL(fileExtension: String) -> URL {
|
||||
fileprivate func createMediaURL(fileExtension: String) -> URL {
|
||||
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
||||
let uniqueMediaName = "\(UUID().uuidString).\(fileExtension)"
|
||||
let temporaryMediaURL = temporaryDirectoryURL.appendingPathComponent(uniqueMediaName)
|
||||
|
||||
@@ -15,7 +15,6 @@ enum LogCategory: String {
|
||||
case storage
|
||||
case push_notifications
|
||||
case damus_purple
|
||||
case image_uploading
|
||||
}
|
||||
|
||||
/// Damus structured logger
|
||||
|
||||
@@ -58,7 +58,7 @@ func load_bootstrap_relays(pubkey: Pubkey) -> [RelayURL] {
|
||||
|
||||
let relay_urls = relays.compactMap({ RelayURL($0) })
|
||||
|
||||
let loaded_relays = Array(Set(relay_urls))
|
||||
let loaded_relays = Array(Set(relay_urls + get_default_bootstrap_relays()))
|
||||
print("Loading custom bootstrap relays: \(loaded_relays)")
|
||||
return loaded_relays
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class ReplyCounter {
|
||||
|
||||
counted.insert(event.id)
|
||||
|
||||
if let reply = event.direct_replies() {
|
||||
for reply in event.direct_replies(keypair) {
|
||||
if event.pubkey == our_pubkey {
|
||||
self.our_replies[reply] = event
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ enum Route: Hashable {
|
||||
case ReactionsSettings(settings: UserSettingsStore)
|
||||
case SearchSettings(settings: UserSettingsStore)
|
||||
case DeveloperSettings(settings: UserSettingsStore)
|
||||
case FirstAidSettings(settings: UserSettingsStore)
|
||||
case Thread(thread: ThreadModel)
|
||||
case Reposts(reposts: EventsModel)
|
||||
case QuoteReposts(quotes: EventsModel)
|
||||
@@ -79,22 +78,19 @@ enum Route: Hashable {
|
||||
case .AppearanceSettings(let settings):
|
||||
AppearanceSettingsView(damus_state: damusState, settings: settings)
|
||||
case .NotificationSettings(let settings):
|
||||
NotificationSettingsView(damus_state: damusState, settings: settings)
|
||||
NotificationSettingsView(settings: settings)
|
||||
case .ZapSettings(let settings):
|
||||
ZapSettingsView(settings: settings)
|
||||
case .TranslationSettings(let settings):
|
||||
TranslationSettingsView(settings: settings, damus_state: damusState)
|
||||
case .ReactionsSettings(let settings):
|
||||
ReactionsSettingsView(settings: settings, damus_state: damusState)
|
||||
ReactionsSettingsView(settings: settings)
|
||||
case .SearchSettings(let settings):
|
||||
SearchSettingsView(settings: settings)
|
||||
case .DeveloperSettings(let settings):
|
||||
DeveloperSettingsView(settings: settings)
|
||||
case .FirstAidSettings(settings: let settings):
|
||||
FirstAidSettingsView(damus_state: damusState, settings: settings)
|
||||
case .Thread(let thread):
|
||||
ChatroomThreadView(damus: damusState, thread: thread)
|
||||
//ThreadView(state: damusState, thread: thread)
|
||||
ThreadView(state: damusState, thread: thread)
|
||||
case .Reposts(let reposts):
|
||||
RepostsView(damus_state: damusState, model: reposts)
|
||||
case .QuoteReposts(let quote_reposts):
|
||||
@@ -179,8 +175,6 @@ enum Route: Hashable {
|
||||
hasher.combine("searchSettings")
|
||||
case .DeveloperSettings:
|
||||
hasher.combine("developerSettings")
|
||||
case .FirstAidSettings:
|
||||
hasher.combine("firstAidSettings")
|
||||
case .Thread(let threadModel):
|
||||
hasher.combine("thread")
|
||||
hasher.combine(threadModel.event.id)
|
||||
|
||||
@@ -411,6 +411,9 @@ func get_zap_description(_ ev: NostrEvent, inv_desc: InvoiceDescription) -> Stri
|
||||
guard let data = desc.data(using: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
guard sha256(data) == deschash else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return desc
|
||||
}
|
||||
|
||||
@@ -6,34 +6,29 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import EmojiPicker
|
||||
import EmojiKit
|
||||
import SwipeActions
|
||||
import UIKit
|
||||
import MCEmojiPicker
|
||||
|
||||
struct EventActionBar: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
let generator = UIImpactFeedbackGenerator(style: .medium)
|
||||
let userProfile : ProfileModel
|
||||
let swipe_context: SwipeContext?
|
||||
let options: Options
|
||||
|
||||
// just used for previews
|
||||
@State var show_share_sheet: Bool = false
|
||||
@State var show_share_action: Bool = false
|
||||
@State var show_repost_action: Bool = false
|
||||
|
||||
@State private var selectedEmoji: Emoji? = nil
|
||||
@State private var isOnTopHalfOfScreen: Bool = false
|
||||
|
||||
@ObservedObject var bar: ActionBarModel
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel? = nil, options: Options = [], swipe_context: SwipeContext? = nil) {
|
||||
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel? = nil) {
|
||||
self.damus_state = damus_state
|
||||
self.event = event
|
||||
_bar = ObservedObject(wrappedValue: bar ?? make_actionbar_model(ev: event.id, damus: damus_state))
|
||||
self.userProfile = ProfileModel(pubkey: event.pubkey, damus: damus_state)
|
||||
self.options = options
|
||||
self.swipe_context = swipe_context
|
||||
}
|
||||
|
||||
var lnurl: String? {
|
||||
@@ -50,176 +45,60 @@ struct EventActionBar: View {
|
||||
return true
|
||||
}
|
||||
|
||||
var space_if_spread: AnyView {
|
||||
if options.contains(.no_spread) {
|
||||
return AnyView(EmptyView())
|
||||
}
|
||||
else {
|
||||
return AnyView(Spacer())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Swipe action menu buttons
|
||||
|
||||
var reply_swipe_button: some View {
|
||||
SwipeAction(systemImage: "arrowshape.turn.up.left.fill", backgroundColor: DamusColors.adaptableGrey) {
|
||||
notify(.compose(.replying_to(event)))
|
||||
self.swipe_context?.state.wrappedValue = .closed
|
||||
}
|
||||
.allowSwipeToTrigger()
|
||||
.swipeButtonStyle()
|
||||
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
|
||||
}
|
||||
|
||||
var repost_swipe_button: some View {
|
||||
SwipeAction(image: "repost", backgroundColor: DamusColors.adaptableGrey) {
|
||||
self.show_repost_action = true
|
||||
self.swipe_context?.state.wrappedValue = .closed
|
||||
}
|
||||
.swipeButtonStyle()
|
||||
.accessibilityLabel(NSLocalizedString("Repost or quote this note", comment: "Accessibility label for repost/quote button"))
|
||||
}
|
||||
|
||||
var like_swipe_button: some View {
|
||||
SwipeAction(image: "shaka", backgroundColor: DamusColors.adaptableGrey) {
|
||||
send_like(emoji: damus_state.settings.default_emoji_reaction)
|
||||
self.swipe_context?.state.wrappedValue = .closed
|
||||
}
|
||||
.swipeButtonStyle()
|
||||
.accessibilityLabel(NSLocalizedString("React with default reaction emoji", comment: "Accessibility label for react button"))
|
||||
}
|
||||
|
||||
var share_swipe_button: some View {
|
||||
SwipeAction(image: "upload", backgroundColor: DamusColors.adaptableGrey) {
|
||||
show_share_action = true
|
||||
self.swipe_context?.state.wrappedValue = .closed
|
||||
}
|
||||
.swipeButtonStyle()
|
||||
.accessibilityLabel(NSLocalizedString("Share externally", comment: "Accessibility label for external share button"))
|
||||
}
|
||||
|
||||
// MARK: Bar buttons
|
||||
|
||||
var reply_button: some View {
|
||||
HStack(spacing: 4) {
|
||||
EventActionButton(img: "bubble2", col: bar.replied ? DamusColors.purple : Color.gray) {
|
||||
notify(.compose(.replying_to(event)))
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
|
||||
Text(verbatim: "\(bar.replies > 0 ? "\(bar.replies)" : "")")
|
||||
.font(.footnote.weight(.medium))
|
||||
.foregroundColor(bar.replied ? DamusColors.purple : Color.gray)
|
||||
}
|
||||
}
|
||||
|
||||
var repost_button: some View {
|
||||
HStack(spacing: 4) {
|
||||
|
||||
EventActionButton(img: "repost", col: bar.boosted ? Color.green : nil) {
|
||||
self.show_repost_action = true
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Reposts", comment: "Accessibility label for boosts button"))
|
||||
Text(verbatim: "\(bar.boosts > 0 ? "\(bar.boosts)" : "")")
|
||||
.font(.footnote.weight(.medium))
|
||||
.foregroundColor(bar.boosted ? Color.green : Color.gray)
|
||||
}
|
||||
}
|
||||
|
||||
var like_button: some View {
|
||||
HStack(spacing: 4) {
|
||||
LikeButton(damus_state: damus_state, liked: bar.liked, liked_emoji: bar.our_like != nil ? to_reaction_emoji(ev: bar.our_like!) : nil) { emoji in
|
||||
if bar.liked {
|
||||
//notify(.delete, bar.our_like)
|
||||
} else {
|
||||
send_like(emoji: emoji)
|
||||
var body: some View {
|
||||
HStack {
|
||||
if damus_state.keypair.privkey != nil {
|
||||
HStack(spacing: 4) {
|
||||
EventActionButton(img: "bubble2", col: bar.replied ? DamusColors.purple : Color.gray) {
|
||||
notify(.compose(.replying_to(event)))
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
|
||||
Text(verbatim: "\(bar.replies > 0 ? "\(bar.replies)" : "")")
|
||||
.font(.footnote.weight(.medium))
|
||||
.foregroundColor(bar.replied ? DamusColors.purple : Color.gray)
|
||||
}
|
||||
}
|
||||
|
||||
Text(verbatim: "\(bar.likes > 0 ? "\(bar.likes)" : "")")
|
||||
.font(.footnote.weight(.medium))
|
||||
.nip05_colorized(gradient: bar.liked)
|
||||
}
|
||||
}
|
||||
|
||||
var share_button: some View {
|
||||
EventActionButton(img: "upload", col: Color.gray) {
|
||||
show_share_action = true
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Share", comment: "Button to share a note"))
|
||||
}
|
||||
|
||||
// MARK: Main views
|
||||
|
||||
var swipe_action_menu_content: some View {
|
||||
Group {
|
||||
self.reply_swipe_button
|
||||
self.repost_swipe_button
|
||||
if show_like {
|
||||
self.like_swipe_button
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var swipe_action_menu_reverse_content: some View {
|
||||
Group {
|
||||
if show_like {
|
||||
self.like_swipe_button
|
||||
}
|
||||
self.repost_swipe_button
|
||||
self.reply_swipe_button
|
||||
}
|
||||
}
|
||||
|
||||
var action_bar_content: some View {
|
||||
let hide_items_without_activity = options.contains(.hide_items_without_activity)
|
||||
let should_hide_chat_bubble = hide_items_without_activity && bar.replies == 0
|
||||
let should_hide_repost = hide_items_without_activity && bar.boosts == 0
|
||||
let should_hide_reactions = hide_items_without_activity && bar.likes == 0
|
||||
let zap_model = self.damus_state.events.get_cache_data(self.event.id).zaps_model
|
||||
let should_hide_zap = hide_items_without_activity && zap_model.zap_total > 0
|
||||
let should_hide_share_button = hide_items_without_activity
|
||||
|
||||
return HStack(spacing: options.contains(.no_spread) ? 10 : 0) {
|
||||
if damus_state.keypair.privkey != nil && !should_hide_chat_bubble {
|
||||
self.reply_button
|
||||
}
|
||||
|
||||
if !should_hide_repost {
|
||||
self.space_if_spread
|
||||
self.repost_button
|
||||
}
|
||||
|
||||
if show_like && !should_hide_reactions {
|
||||
self.space_if_spread
|
||||
self.like_button
|
||||
}
|
||||
Spacer()
|
||||
HStack(spacing: 4) {
|
||||
|
||||
if let lnurl = self.lnurl, !should_hide_zap {
|
||||
self.space_if_spread
|
||||
NoteZapButton(damus_state: damus_state, target: ZapTarget.note(id: event.id, author: event.pubkey), lnurl: lnurl, zaps: zap_model)
|
||||
EventActionButton(img: "repost", col: bar.boosted ? Color.green : nil) {
|
||||
self.show_repost_action = true
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Reposts", comment: "Accessibility label for boosts button"))
|
||||
Text(verbatim: "\(bar.boosts > 0 ? "\(bar.boosts)" : "")")
|
||||
.font(.footnote.weight(.medium))
|
||||
.foregroundColor(bar.boosted ? Color.green : Color.gray)
|
||||
}
|
||||
|
||||
if !should_hide_share_button {
|
||||
self.space_if_spread
|
||||
self.share_button
|
||||
|
||||
if show_like {
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 4) {
|
||||
LikeButton(damus_state: damus_state, liked: bar.liked, liked_emoji: bar.our_like != nil ? to_reaction_emoji(ev: bar.our_like!) : nil, isOnTopHalfOfScreen: $isOnTopHalfOfScreen) { emoji in
|
||||
if bar.liked {
|
||||
//notify(.delete, bar.our_like)
|
||||
} else {
|
||||
send_like(emoji: emoji)
|
||||
}
|
||||
}
|
||||
|
||||
Text(verbatim: "\(bar.likes > 0 ? "\(bar.likes)" : "")")
|
||||
.font(.footnote.weight(.medium))
|
||||
.nip05_colorized(gradient: bar.liked)
|
||||
}
|
||||
}
|
||||
|
||||
if let lnurl = self.lnurl {
|
||||
Spacer()
|
||||
NoteZapButton(damus_state: damus_state, target: ZapTarget.note(id: event.id, author: event.pubkey), lnurl: lnurl, zaps: self.damus_state.events.get_cache_data(self.event.id).zaps_model)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
EventActionButton(img: "upload", col: Color.gray) {
|
||||
show_share_action = true
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Share", comment: "Button to share a note"))
|
||||
}
|
||||
}
|
||||
|
||||
var content: some View {
|
||||
if options.contains(.swipe_action_menu) {
|
||||
AnyView(self.swipe_action_menu_content)
|
||||
}
|
||||
else if options.contains(.swipe_action_menu_reverse) {
|
||||
AnyView(self.swipe_action_menu_reverse_content)
|
||||
}
|
||||
else {
|
||||
AnyView(self.action_bar_content)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
self.content
|
||||
.onAppear {
|
||||
self.bar.update(damus: damus_state, evid: self.event.id)
|
||||
}
|
||||
@@ -258,6 +137,20 @@ struct EventActionBar: View {
|
||||
self.bar.our_like = liked.event
|
||||
}
|
||||
}
|
||||
.background(
|
||||
GeometryReader { geometry in
|
||||
EmptyView()
|
||||
.onAppear {
|
||||
let eventActionBarY = geometry.frame(in: .global).midY
|
||||
let screenMidY = UIScreen.main.bounds.midY
|
||||
self.isOnTopHalfOfScreen = eventActionBarY > screenMidY
|
||||
}
|
||||
.onChange(of: geometry.frame(in: .global).midY) { newY in
|
||||
let screenMidY = UIScreen.main.bounds.midY
|
||||
self.isOnTopHalfOfScreen = newY > screenMidY
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func send_like(emoji: String) {
|
||||
@@ -272,17 +165,6 @@ struct EventActionBar: View {
|
||||
|
||||
damus_state.postbox.send(like_ev)
|
||||
}
|
||||
|
||||
// MARK: Helper structures
|
||||
|
||||
struct Options: OptionSet {
|
||||
let rawValue: UInt32
|
||||
|
||||
static let no_spread = Options(rawValue: 1 << 0)
|
||||
static let hide_items_without_activity = Options(rawValue: 1 << 1)
|
||||
static let swipe_action_menu = Options(rawValue: 1 << 2)
|
||||
static let swipe_action_menu_reverse = Options(rawValue: 1 << 3)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -302,6 +184,7 @@ struct LikeButton: View {
|
||||
let damus_state: DamusState
|
||||
let liked: Bool
|
||||
let liked_emoji: String?
|
||||
@Binding var isOnTopHalfOfScreen: Bool
|
||||
let action: (_ emoji: String) -> Void
|
||||
|
||||
// For reactions background
|
||||
@@ -310,7 +193,7 @@ struct LikeButton: View {
|
||||
|
||||
@State private var isReactionsVisible = false
|
||||
|
||||
@State private var selectedEmoji: Emoji?
|
||||
@State private var selectedEmoji: String = ""
|
||||
|
||||
// Following four are Shaka animation properties
|
||||
let timer = Timer.publish(every: 0.10, on: .main, in: .common).autoconnect()
|
||||
@@ -349,11 +232,6 @@ struct LikeButton: View {
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isReactionsVisible) {
|
||||
NavigationView {
|
||||
EmojiPickerView(selectedEmoji: $selectedEmoji, emojiProvider: damus_state.emoji_provider)
|
||||
}.presentationDetents([.medium, .large])
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Like", comment: "Accessibility Label for Like button"))
|
||||
.rotationEffect(Angle(degrees: shouldAnimate ? rotationAngle : 0))
|
||||
.onReceive(self.timer) { _ in
|
||||
@@ -368,10 +246,14 @@ struct LikeButton: View {
|
||||
amountOfAngleIncrease = 20.0
|
||||
}
|
||||
})
|
||||
.emojiPicker(
|
||||
isPresented: $isReactionsVisible,
|
||||
selectedEmoji: $selectedEmoji,
|
||||
arrowDirection: isOnTopHalfOfScreen ? .down : .up,
|
||||
isDismissAfterChoosing: true
|
||||
)
|
||||
.onChange(of: selectedEmoji) { newSelectedEmoji in
|
||||
if let newSelectedEmoji {
|
||||
self.action(newSelectedEmoji.value)
|
||||
}
|
||||
self.action(newSelectedEmoji)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,6 +300,7 @@ struct LikeButton: View {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct EventActionBar_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let ds = test_damus_state
|
||||
@@ -442,44 +325,7 @@ struct EventActionBar_Previews: PreviewProvider {
|
||||
EventActionBar(damus_state: ds, event: ev, bar: extra_max_bar)
|
||||
|
||||
EventActionBar(damus_state: ds, event: ev, bar: mega_max_bar)
|
||||
|
||||
EventActionBar(damus_state: ds, event: ev, bar: bar, options: [.no_spread])
|
||||
}
|
||||
.padding(20)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Helpers
|
||||
|
||||
fileprivate struct SwipeButtonStyle: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.frame(width: 50, height: 50)
|
||||
.clipShape(Circle())
|
||||
.overlay(Circle().stroke(Color.damusAdaptableGrey2, lineWidth: 2))
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension View {
|
||||
func swipeButtonStyle() -> some View {
|
||||
modifier(SwipeButtonStyle())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Needed extensions for SwipeAction
|
||||
|
||||
public extension SwipeAction where Label == Image, Background == Color {
|
||||
init(
|
||||
image: String,
|
||||
backgroundColor: Color = Color.primary.opacity(0.1),
|
||||
highlightOpacity: Double = 0.5,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.init(action: action) { highlight in
|
||||
Image(image)
|
||||
} background: { highlight in
|
||||
backgroundColor
|
||||
.opacity(highlight ? highlightOpacity : 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ struct ShareActionButton: View {
|
||||
.frame(width: 55.0, height: 55.0)
|
||||
}
|
||||
.frame(height: 25)
|
||||
Text(text)
|
||||
Text(verbatim: text)
|
||||
.foregroundColor(col)
|
||||
.font(.footnote)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
@@ -121,7 +121,7 @@ struct AddRelayView: View {
|
||||
dismiss()
|
||||
}) {
|
||||
HStack {
|
||||
Text("Add relay", comment: "Button to add a relay.")
|
||||
Text(verbatim: "Add relay")
|
||||
.bold()
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
|
||||
|
||||
@@ -17,7 +17,7 @@ enum ImageUploadResult {
|
||||
|
||||
fileprivate func create_upload_body(mediaData: Data, boundary: String, mediaUploader: MediaUploader, mediaToUpload: MediaUpload) -> Data {
|
||||
let body = NSMutableData();
|
||||
let contentType = mediaToUpload.mime_type
|
||||
let contentType = mediaToUpload.is_image ? "image/jpg" : "video/mp4"
|
||||
body.appendString(string: "Content-Type: multipart/form-data; boundary=\(boundary)\r\n\r\n")
|
||||
body.appendString(string: "--\(boundary)\r\n")
|
||||
body.appendString(string: "Content-Disposition: form-data; name=\(mediaUploader.nameParam); filename=\(mediaToUpload.genericFileName)\r\n")
|
||||
|
||||
@@ -33,7 +33,7 @@ struct BookmarksView: View {
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 32.0, height: 32.0)
|
||||
Text("You have no bookmarks yet, add them in the context menu", comment: "Text indicating that there are no bookmarks to be viewed")
|
||||
Text(NSLocalizedString("You have no bookmarks yet, add them in the context menu", comment: "Text indicating that there are no bookmarks to be viewed"))
|
||||
}
|
||||
} else {
|
||||
ScrollView {
|
||||
|
||||
@@ -22,8 +22,7 @@ struct GradientFollowButton: View {
|
||||
Button(action: {
|
||||
follow_state = perform_follow_btn_action(follow_state, target: target)
|
||||
}) {
|
||||
let followButtonText = follow_btn_txt(follow_state, follows_you: follows_you)
|
||||
Text(followButtonText)
|
||||
Text(follow_btn_txt(follow_state, follows_you: follows_you))
|
||||
.foregroundColor(follow_state == .unfollows ? .white : grayTextColor)
|
||||
.font(.callout)
|
||||
.fontWeight(.medium)
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
//
|
||||
// ChatBubbleView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2024-06-17.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
/// Use this view to display content inside of a custom-designed chat bubble shape.
|
||||
struct ChatBubble<T: View, U: ShapeStyle, V: View>: View {
|
||||
/// The direction at which the chat bubble tip will be pointing towards
|
||||
let direction: Direction
|
||||
let stroke_content: U
|
||||
let stroke_style: StrokeStyle
|
||||
let background_style: V
|
||||
@ViewBuilder let content: T
|
||||
|
||||
// Constants, which are loosely tied to `OFFSET_X` and `OFFSET_Y`
|
||||
let OFFSET_X_PADDING: CGFloat = 6
|
||||
let OFFSET_Y_BOTTOM_PADDING: CGFloat = 3
|
||||
|
||||
var body: some View {
|
||||
self.content
|
||||
.padding(direction == .left ? .leading : .trailing, OFFSET_X_PADDING)
|
||||
.padding(.bottom, OFFSET_Y_BOTTOM_PADDING)
|
||||
.background(self.background_style)
|
||||
.clipShape(
|
||||
BubbleShape(direction: self.direction)
|
||||
)
|
||||
.overlay(
|
||||
BubbleShape(direction: self.direction)
|
||||
.stroke(self.stroke_content, style: self.stroke_style)
|
||||
)
|
||||
.padding(direction == .left ? .leading : .trailing, -OFFSET_X_PADDING)
|
||||
.padding(.bottom, -OFFSET_Y_BOTTOM_PADDING)
|
||||
}
|
||||
|
||||
enum Direction {
|
||||
case right
|
||||
case left
|
||||
}
|
||||
|
||||
struct BubbleShape: Shape {
|
||||
/// The direction at which the chat bubble tip will be pointing towards
|
||||
let direction: Direction
|
||||
|
||||
// MARK: Constant parameters that defines the shape and look of the chat bubbles
|
||||
|
||||
/// The corner radius of the round edges
|
||||
let CORNER_RADIUS: CGFloat = 10
|
||||
/// The height of the chat bubble tip detail
|
||||
let DETAIL_HEIGHT: CGFloat = 10
|
||||
/// The horizontal distance between the chat bubble tip and the vertical edge of the bubble
|
||||
let OFFSET_X: CGFloat = 7
|
||||
/// The vertical distance between the chat bubble tip and the bottom edge of the bubble
|
||||
let OFFSET_Y: CGFloat = 5
|
||||
/// Value between 0 and 1 that determines curvature of the upper chat bubble curve detail
|
||||
let DETAIL_CURVE_FACTOR: CGFloat = 0.75
|
||||
/// Value between 0 and 1 that determines curvature of the lower chat bubble curve detail
|
||||
let LOWER_DETAIL_CURVE_FACTOR: CGFloat = 0.4
|
||||
/// The horizontal distance between the chat bubble tip and the point at which the lower chat bubble curve detail attaches to the bottom of the chat bubble
|
||||
let LOWER_DETAIL_ATTACHMENT_OFFSET_X: CGFloat = 20
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
return self.direction == .left ? self.draw_left_bubble(in: rect) : self.draw_right_bubble(in: rect)
|
||||
}
|
||||
|
||||
func draw_left_bubble(in rect: CGRect) -> Path {
|
||||
return Path { p in
|
||||
// Start at the top left, just below the end of the corner radius
|
||||
let start = CGPoint(x: OFFSET_X, y: CORNER_RADIUS)
|
||||
// Left edge
|
||||
p.move(to: start)
|
||||
p.addLine(to: CGPoint(x: OFFSET_X, y: rect.height - DETAIL_HEIGHT))
|
||||
// Draw the chat bubble tip
|
||||
p.addLine(to: CGPoint(x: OFFSET_X, y: rect.height - DETAIL_HEIGHT))
|
||||
let tip_of_bubble = CGPoint(x: 0, y: rect.height)
|
||||
p.addQuadCurve(
|
||||
to: tip_of_bubble,
|
||||
control: CGPoint(x: 0, y: rect.height - DETAIL_HEIGHT) + CGVector(dx: OFFSET_X, dy: DETAIL_HEIGHT) * DETAIL_CURVE_FACTOR
|
||||
)
|
||||
let lower_detail_attachment = CGPoint(x: LOWER_DETAIL_ATTACHMENT_OFFSET_X, y: rect.height - OFFSET_Y)
|
||||
p.addCurve(
|
||||
to: lower_detail_attachment,
|
||||
control1: tip_of_bubble + CGVector(dx: LOWER_DETAIL_ATTACHMENT_OFFSET_X, dy: 0) * LOWER_DETAIL_CURVE_FACTOR,
|
||||
control2: lower_detail_attachment - CGVector(dx: LOWER_DETAIL_ATTACHMENT_OFFSET_X, dy: 0) * LOWER_DETAIL_CURVE_FACTOR
|
||||
)
|
||||
// Draw the bottom edge
|
||||
p.addLine(to: CGPoint(x: rect.width - CORNER_RADIUS, y: rect.height - OFFSET_Y))
|
||||
// Draw the bottom right round corner
|
||||
p.addQuadCurve(
|
||||
to: CGPoint(x: rect.width, y: rect.height - OFFSET_Y - CORNER_RADIUS),
|
||||
control: CGPoint(x: rect.width, y: rect.height - OFFSET_Y)
|
||||
)
|
||||
// Draw right edge
|
||||
p.addLine(to: CGPoint(x: rect.width, y: CORNER_RADIUS))
|
||||
// Draw top right round corner
|
||||
p.addQuadCurve(
|
||||
to: CGPoint(x: rect.width - CORNER_RADIUS, y: 0),
|
||||
control: CGPoint(x: rect.width, y: 0)
|
||||
)
|
||||
// Draw top edge
|
||||
p.addLine(to: CGPoint(x: CORNER_RADIUS + OFFSET_X, y: 0))
|
||||
// Draw top left round corner
|
||||
p.addQuadCurve(
|
||||
to: start,
|
||||
control: CGPoint(x: OFFSET_X, y: 0)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func draw_right_bubble(in rect: CGRect) -> Path {
|
||||
return Path { p in
|
||||
// Start at the top right, just below the end of the corner radius
|
||||
let right_edge = rect.width - OFFSET_X
|
||||
let start = CGPoint(x: right_edge, y: CORNER_RADIUS)
|
||||
p.move(to: start)
|
||||
// Right edge
|
||||
p.addLine(to: CGPoint(x: right_edge, y: rect.height - DETAIL_HEIGHT))
|
||||
// Draw the chat bubble tip
|
||||
let tip_of_bubble = CGPoint(x: rect.width, y: rect.height)
|
||||
p.addQuadCurve(
|
||||
to: tip_of_bubble,
|
||||
control: CGPoint(x: rect.width, y: rect.height - DETAIL_HEIGHT) + CGVector(dx: -OFFSET_X, dy: DETAIL_HEIGHT) * DETAIL_CURVE_FACTOR
|
||||
)
|
||||
let lower_detail_attachment = CGPoint(x: rect.width - LOWER_DETAIL_ATTACHMENT_OFFSET_X, y: rect.height - OFFSET_Y)
|
||||
p.addCurve(
|
||||
to: lower_detail_attachment,
|
||||
control1: tip_of_bubble - CGVector(dx: LOWER_DETAIL_ATTACHMENT_OFFSET_X, dy: 0) * LOWER_DETAIL_CURVE_FACTOR,
|
||||
control2: lower_detail_attachment + CGVector(dx: LOWER_DETAIL_ATTACHMENT_OFFSET_X, dy: 0) * LOWER_DETAIL_CURVE_FACTOR
|
||||
)
|
||||
// Draw the bottom edge
|
||||
p.addLine(to: CGPoint(x: CORNER_RADIUS, y: rect.height - OFFSET_Y))
|
||||
// Draw the bottom left round corner
|
||||
p.addQuadCurve(
|
||||
to: CGPoint(x: 0, y: rect.height - OFFSET_Y - CORNER_RADIUS),
|
||||
control: CGPoint(x: 0, y: rect.height - OFFSET_Y)
|
||||
)
|
||||
// Draw left edge
|
||||
p.addLine(to: CGPoint(x: 0, y: CORNER_RADIUS))
|
||||
// Draw top right round corner
|
||||
p.addQuadCurve(
|
||||
to: CGPoint(x: CORNER_RADIUS, y: 0),
|
||||
control: CGPoint(x: 0, y: 0)
|
||||
)
|
||||
// Draw top edge
|
||||
p.addLine(to: CGPoint(x: rect.width - CORNER_RADIUS - OFFSET_X, y: 0))
|
||||
// Draw top left round corner
|
||||
p.addQuadCurve(
|
||||
to: start,
|
||||
control: CGPoint(x: rect.width - OFFSET_X, y: 0)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
VStack {
|
||||
ChatBubble(
|
||||
direction: .left,
|
||||
stroke_content: Color.accentColor.opacity(0),
|
||||
stroke_style: .init(lineWidth: 4),
|
||||
background_style: Color.accentColor
|
||||
) {
|
||||
Text("Hello there")
|
||||
.padding()
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
|
||||
ChatBubble(
|
||||
direction: .right,
|
||||
stroke_content: Color.accentColor.opacity(0),
|
||||
stroke_style: .init(lineWidth: 4),
|
||||
background_style: Color.accentColor
|
||||
) {
|
||||
Text("Hello there")
|
||||
.padding()
|
||||
}
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
@@ -1,324 +0,0 @@
|
||||
//
|
||||
// ChatView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2022-04-19.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import EmojiKit
|
||||
import EmojiPicker
|
||||
import SwipeActions
|
||||
|
||||
fileprivate let CORNER_RADIUS: CGFloat = 10
|
||||
|
||||
struct ChatEventView: View {
|
||||
// MARK: Parameters
|
||||
let event: NostrEvent
|
||||
let selected_event: NostrEvent
|
||||
let prev_ev: NostrEvent?
|
||||
let next_ev: NostrEvent?
|
||||
let damus_state: DamusState
|
||||
var thread: ThreadModel
|
||||
let scroll_to_event: ((_ id: NoteId) -> Void)?
|
||||
let focus_event: (() -> Void)?
|
||||
let highlight_bubble: Bool
|
||||
|
||||
// MARK: long-press reaction control objects
|
||||
/// Whether the user is actively pressing the view
|
||||
@State var is_pressing = false
|
||||
/// The dispatched work item scheduled by a timer to bounce the event bubble and show the emoji selector
|
||||
@State var long_press_bounce_work_item: DispatchWorkItem?
|
||||
@State var popover_state: PopoverState = .closed {
|
||||
didSet {
|
||||
let generator = UIImpactFeedbackGenerator(style: popover_state == .open_emoji_selector ? .heavy : .light)
|
||||
generator.impactOccurred()
|
||||
}
|
||||
}
|
||||
@State var selected_emoji: Emoji?
|
||||
|
||||
@State private var isOnTopHalfOfScreen: Bool = false
|
||||
@ObservedObject var bar: ActionBarModel
|
||||
|
||||
enum PopoverState: String {
|
||||
case closed
|
||||
case open_emoji_selector
|
||||
}
|
||||
|
||||
var just_started: Bool {
|
||||
return prev_ev == nil || prev_ev!.pubkey != event.pubkey
|
||||
}
|
||||
|
||||
func next_replies_to_this() -> Bool {
|
||||
guard let next = next_ev else {
|
||||
return false
|
||||
}
|
||||
|
||||
return damus_state.events.replies.lookup(next.id) != nil
|
||||
}
|
||||
|
||||
func is_reply_to_prev(ref_id: NoteId) -> Bool {
|
||||
guard let prev = prev_ev else {
|
||||
return true
|
||||
}
|
||||
|
||||
if let rep = damus_state.events.replies.lookup(event.id) {
|
||||
return rep.contains(prev.id)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
var disable_animation: Bool {
|
||||
self.damus_state.settings.disable_animation
|
||||
}
|
||||
|
||||
var reply_quote_options: EventViewOptions {
|
||||
return [.no_previews, .no_action_bar, .truncate_content_very_short, .no_show_more, .no_translate, .no_media]
|
||||
}
|
||||
|
||||
var profile_picture_view: some View {
|
||||
VStack {
|
||||
ProfilePicView(pubkey: event.pubkey, size: 32, highlight: .none, profiles: damus_state.profiles, disable_animation: disable_animation)
|
||||
.onTapGesture {
|
||||
show_profile_action_sheet_if_enabled(damus_state: damus_state, pubkey: event.pubkey)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: 32)
|
||||
}
|
||||
|
||||
var by_other_user: Bool {
|
||||
return event.pubkey != damus_state.pubkey
|
||||
}
|
||||
|
||||
var is_ours: Bool { return !by_other_user }
|
||||
|
||||
var event_bubble: some View {
|
||||
ChatBubble(
|
||||
direction: is_ours ? .right : .left,
|
||||
stroke_content: Color.accentColor.opacity(highlight_bubble ? 1 : 0),
|
||||
stroke_style: .init(lineWidth: 4),
|
||||
background_style: by_other_user ? DamusColors.adaptableGrey : DamusColors.adaptablePurpleBackground
|
||||
) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
if by_other_user {
|
||||
HStack {
|
||||
ProfileName(pubkey: event.pubkey, damus: damus_state)
|
||||
.onTapGesture {
|
||||
show_profile_action_sheet_if_enabled(damus_state: damus_state, pubkey: event.pubkey)
|
||||
}
|
||||
Text(verbatim: "\(format_relative_time(event.created_at))")
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
|
||||
if let replying_to = event.direct_replies(),
|
||||
replying_to != selected_event.id {
|
||||
ReplyQuoteView(keypair: damus_state.keypair, quoter: event, event_id: replying_to, state: damus_state, thread: thread, options: reply_quote_options)
|
||||
.background(is_ours ? DamusColors.adaptablePurpleBackground2 : DamusColors.adaptableGrey2)
|
||||
.foregroundColor(is_ours ? Color.damusAdaptablePurpleForeground : Color.damusAdaptableBlack)
|
||||
.cornerRadius(5)
|
||||
.onTapGesture {
|
||||
self.scroll_to_event?(replying_to)
|
||||
}
|
||||
}
|
||||
|
||||
let blur_images = should_blur_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
|
||||
NoteContentView(damus_state: damus_state, event: event, blur_images: blur_images, size: .normal, options: [])
|
||||
.padding(2)
|
||||
}
|
||||
.frame(minWidth: 150, alignment: is_ours ? .trailing : .leading)
|
||||
.padding(10)
|
||||
}
|
||||
.tint(is_ours ? Color.white : Color.accentColor)
|
||||
.overlay(
|
||||
ZStack(alignment: is_ours ? .bottomLeading : .bottomTrailing) {
|
||||
VStack {
|
||||
Spacer()
|
||||
self.action_bar
|
||||
.padding(.horizontal, 5)
|
||||
}
|
||||
}
|
||||
)
|
||||
.onTapGesture {
|
||||
if popover_state == .closed {
|
||||
focus_event?()
|
||||
}
|
||||
else {
|
||||
popover_state = .closed
|
||||
let generator = UIImpactFeedbackGenerator(style: .light)
|
||||
generator.impactOccurred()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var event_bubble_with_long_press_interaction: some View {
|
||||
ZStack(alignment: is_ours ? .bottomLeading : .bottomTrailing) {
|
||||
self.event_bubble
|
||||
.sheet(isPresented: Binding(get: { popover_state == .open_emoji_selector }, set: { new_state in
|
||||
withAnimation(new_state == true ? .easeIn(duration: 0.5) : .easeOut(duration: 0.1)) {
|
||||
popover_state = new_state == true ? .open_emoji_selector : .closed
|
||||
}
|
||||
})) {
|
||||
NavigationView {
|
||||
EmojiPickerView(selectedEmoji: $selected_emoji, emojiProvider: damus_state.emoji_provider)
|
||||
}.presentationDetents([.medium, .large])
|
||||
}
|
||||
.onChange(of: selected_emoji) { newSelectedEmoji in
|
||||
if let newSelectedEmoji {
|
||||
send_like(emoji: newSelectedEmoji.value)
|
||||
popover_state = .closed
|
||||
}
|
||||
}
|
||||
}
|
||||
.scaleEffect(self.popover_state == .open_emoji_selector ? 1.08 : is_pressing ? 1.02 : 1)
|
||||
.shadow(color: (is_pressing || self.popover_state == .open_emoji_selector) ? .black.opacity(0.1) : .black.opacity(0.3), radius: (is_pressing || self.popover_state == .open_emoji_selector) ? 8 : 0, y: (is_pressing || self.popover_state == .open_emoji_selector) ? 15 : 0)
|
||||
.onLongPressGesture(minimumDuration: 0.5, maximumDistance: 10, perform: {
|
||||
long_press_bounce_work_item?.cancel()
|
||||
}, onPressingChanged: { is_pressing in
|
||||
withAnimation(is_pressing ? .easeIn(duration: 0.5) : .easeOut(duration: 0.1)) {
|
||||
self.is_pressing = is_pressing
|
||||
if popover_state != .closed {
|
||||
return
|
||||
}
|
||||
if self.is_pressing {
|
||||
let item = DispatchWorkItem {
|
||||
// Ensure the action is performed only if the condition is still valid
|
||||
if self.is_pressing {
|
||||
withAnimation(.bouncy(duration: 0.2, extraBounce: 0.35)) {
|
||||
popover_state = .open_emoji_selector
|
||||
}
|
||||
}
|
||||
}
|
||||
long_press_bounce_work_item = item
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: item)
|
||||
}
|
||||
}
|
||||
})
|
||||
.background(
|
||||
GeometryReader { geometry in
|
||||
EmptyView()
|
||||
.onAppear {
|
||||
let eventActionBarY = geometry.frame(in: .global).midY
|
||||
let screenMidY = UIScreen.main.bounds.midY
|
||||
self.isOnTopHalfOfScreen = eventActionBarY > screenMidY
|
||||
}
|
||||
.onChange(of: geometry.frame(in: .global).midY) { newY in
|
||||
let screenMidY = UIScreen.main.bounds.midY
|
||||
self.isOnTopHalfOfScreen = newY > screenMidY
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
func send_like(emoji: String) {
|
||||
guard let keypair = damus_state.keypair.to_full(),
|
||||
let like_ev = make_like_event(keypair: keypair, liked: event, content: emoji) else {
|
||||
return
|
||||
}
|
||||
|
||||
self.bar.our_like = like_ev
|
||||
|
||||
let generator = UIImpactFeedbackGenerator(style: .medium)
|
||||
generator.impactOccurred()
|
||||
|
||||
damus_state.postbox.send(like_ev)
|
||||
}
|
||||
|
||||
var action_bar: some View {
|
||||
return Group {
|
||||
if !bar.is_empty {
|
||||
HStack {
|
||||
if by_other_user {
|
||||
Spacer()
|
||||
}
|
||||
EventActionBar(damus_state: damus_state, event: event, bar: bar, options: [.no_spread, .hide_items_without_activity])
|
||||
.padding(10)
|
||||
.background(DamusColors.adaptableLighterGrey)
|
||||
.disabled(true)
|
||||
.cornerRadius(100)
|
||||
.overlay(RoundedRectangle(cornerSize: CGSize(width: 100, height: 100)).stroke(DamusColors.adaptableWhite, lineWidth: 1))
|
||||
.shadow(color: Color.black.opacity(0.05),radius: 3, y: 3)
|
||||
.scaleEffect(0.7, anchor: is_ours ? .leading : .trailing)
|
||||
if !by_other_user {
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.padding(.vertical, -20)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var event_bubble_with_long_press_and_swipe_interactions: some View {
|
||||
Group {
|
||||
SwipeView {
|
||||
self.event_bubble_with_long_press_interaction
|
||||
} leadingActions: { context in
|
||||
EventActionBar(
|
||||
damus_state: damus_state,
|
||||
event: event,
|
||||
bar: bar,
|
||||
options: is_ours ? [.swipe_action_menu_reverse] : [.swipe_action_menu],
|
||||
swipe_context: context
|
||||
)
|
||||
}
|
||||
.swipeSpacing(-20)
|
||||
.swipeActionsStyle(.mask)
|
||||
.swipeMinimumDistance(20)
|
||||
}
|
||||
}
|
||||
|
||||
var content: some View {
|
||||
return VStack {
|
||||
HStack(alignment: .bottom, spacing: 4) {
|
||||
if by_other_user {
|
||||
self.profile_picture_view
|
||||
}
|
||||
else {
|
||||
Spacer()
|
||||
}
|
||||
|
||||
self.event_bubble_with_long_press_and_swipe_interactions
|
||||
|
||||
if !by_other_user {
|
||||
self.profile_picture_view
|
||||
}
|
||||
else {
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.id(event.id)
|
||||
.padding([.bottom], bar.is_empty ? 6 : 16)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if [.boost, .zap, .longform].contains(where: { event.known_kind == $0 }) {
|
||||
EmptyView()
|
||||
} else {
|
||||
self.content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Notification.Name {
|
||||
static var toggle_thread_view: Notification.Name {
|
||||
return Notification.Name("convert_to_thread")
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let bar = make_actionbar_model(ev: test_note.id, damus: test_damus_state)
|
||||
return ChatEventView(event: test_note, selected_event: test_note, prev_ev: nil, next_ev: nil, damus_state: test_damus_state, thread: ThreadModel(event: test_note, damus_state: test_damus_state), scroll_to_event: nil, focus_event: nil, highlight_bubble: false, bar: bar)
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let bar = make_actionbar_model(ev: test_note.id, damus: test_damus_state)
|
||||
return ChatEventView(event: test_short_note, selected_event: test_note, prev_ev: nil, next_ev: nil, damus_state: test_damus_state, thread: ThreadModel(event: test_note, damus_state: test_damus_state), scroll_to_event: nil, focus_event: nil, highlight_bubble: false, bar: bar)
|
||||
}
|
||||
|
||||
#Preview {
|
||||
let bar = make_actionbar_model(ev: test_note.id, damus: test_damus_state)
|
||||
return ChatEventView(event: test_short_note, selected_event: test_note, prev_ev: nil, next_ev: nil, damus_state: test_damus_state, thread: ThreadModel(event: test_note, damus_state: test_damus_state), scroll_to_event: nil, focus_event: nil, highlight_bubble: true, bar: bar)
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
//
|
||||
// ChatroomView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2022-04-19.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SwipeActions
|
||||
|
||||
struct ChatroomThreadView: View {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State var once: Bool = false
|
||||
let damus: DamusState
|
||||
@ObservedObject var thread: ThreadModel
|
||||
@State var selected_note_id: NoteId? = nil
|
||||
@State var user_just_posted_flag: Bool = false
|
||||
@Namespace private var animation
|
||||
|
||||
@State var parent_events: [NostrEvent] = []
|
||||
@State var sorted_child_events: [NostrEvent] = []
|
||||
|
||||
func compute_events(selected_event: NostrEvent? = nil) {
|
||||
let selected_event = selected_event ?? thread.event
|
||||
self.parent_events = damus.events.parent_events(event: selected_event, keypair: damus.keypair)
|
||||
let all_recursive_child_events = self.recursive_child_events(event: selected_event)
|
||||
self.sorted_child_events = all_recursive_child_events.filter({
|
||||
should_show_event(event: $0, damus_state: damus) // Hide muted events from chatroom conversation
|
||||
}).sorted(by: { a, b in
|
||||
return a.created_at < b.created_at
|
||||
})
|
||||
}
|
||||
|
||||
func recursive_child_events(event: NdbNote) -> [NdbNote] {
|
||||
let immediate_children = damus.events.child_events(event: event)
|
||||
var indirect_children: [NdbNote] = []
|
||||
for immediate_child in immediate_children {
|
||||
indirect_children.append(contentsOf: self.recursive_child_events(event: immediate_child))
|
||||
}
|
||||
return immediate_children + indirect_children
|
||||
}
|
||||
|
||||
func go_to_event(scroller: ScrollViewProxy, note_id: NoteId) {
|
||||
scroll_to_event(scroller: scroller, id: note_id, delay: 0, animate: true, anchor: .top)
|
||||
selected_note_id = note_id
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
|
||||
withAnimation {
|
||||
selected_note_id = nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func set_active_event(scroller: ScrollViewProxy, ev: NdbNote) {
|
||||
withAnimation {
|
||||
self.compute_events(selected_event: ev)
|
||||
thread.set_active_event(ev, keypair: self.damus.keypair)
|
||||
self.go_to_event(scroller: scroller, note_id: ev.id)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollViewReader { scroller in
|
||||
ScrollView(.vertical) {
|
||||
LazyVStack(alignment: .leading, spacing: 8) {
|
||||
// MARK: - Parents events view
|
||||
ForEach(parent_events, id: \.id) { parent_event in
|
||||
EventMutingContainerView(damus_state: damus, event: parent_event) {
|
||||
EventView(damus: damus, event: parent_event)
|
||||
.matchedGeometryEffect(id: parent_event.id.hex(), in: animation, anchor: .center)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.onTapGesture {
|
||||
self.set_active_event(scroller: scroller, ev: parent_event)
|
||||
}
|
||||
.id(parent_event.id)
|
||||
|
||||
Divider()
|
||||
.padding(.top, 4)
|
||||
.padding(.leading, 25 * 2)
|
||||
|
||||
}.background(GeometryReader { geometry in
|
||||
// get the height and width of the EventView view
|
||||
let eventHeight = geometry.frame(in: .global).height
|
||||
// let eventWidth = geometry.frame(in: .global).width
|
||||
|
||||
// vertical gray line in the background
|
||||
Rectangle()
|
||||
.fill(Color.gray.opacity(0.25))
|
||||
.frame(width: 2, height: eventHeight)
|
||||
.offset(x: 40, y: 40)
|
||||
})
|
||||
|
||||
// MARK: - Actual event view
|
||||
EventMutingContainerView(
|
||||
damus_state: damus,
|
||||
event: self.thread.event,
|
||||
muteBox: { event_shown, muted_reason in
|
||||
AnyView(
|
||||
EventMutedBoxView(shown: event_shown, reason: muted_reason)
|
||||
.padding(5)
|
||||
)
|
||||
}
|
||||
) {
|
||||
SelectedEventView(damus: damus, event: self.thread.event, size: .selected)
|
||||
.matchedGeometryEffect(id: self.thread.event.id.hex(), in: animation, anchor: .center)
|
||||
}
|
||||
.id(self.thread.event.id)
|
||||
|
||||
|
||||
// MARK: - Children view
|
||||
let events = sorted_child_events
|
||||
let count = events.count
|
||||
SwipeViewGroup {
|
||||
ForEach(Array(zip(events, events.indices)), id: \.0.id) { (ev, ind) in
|
||||
ChatEventView(event: events[ind],
|
||||
selected_event: self.thread.event,
|
||||
prev_ev: ind > 0 ? events[ind-1] : nil,
|
||||
next_ev: ind == count-1 ? nil : events[ind+1],
|
||||
damus_state: damus,
|
||||
thread: thread,
|
||||
scroll_to_event: { note_id in
|
||||
self.go_to_event(scroller: scroller, note_id: note_id)
|
||||
},
|
||||
focus_event: {
|
||||
self.set_active_event(scroller: scroller, ev: ev)
|
||||
},
|
||||
highlight_bubble: selected_note_id == ev.id,
|
||||
bar: make_actionbar_model(ev: ev.id, damus: damus)
|
||||
)
|
||||
.padding(.horizontal)
|
||||
.id(ev.id)
|
||||
.matchedGeometryEffect(id: ev.id.hex(), in: animation, anchor: .center)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.top)
|
||||
EndBlock()
|
||||
}
|
||||
.onReceive(handle_notify(.post), perform: { notify in
|
||||
switch notify {
|
||||
case .post(_):
|
||||
user_just_posted_flag = true
|
||||
case .cancel:
|
||||
return
|
||||
}
|
||||
})
|
||||
.onReceive(thread.objectWillChange) {
|
||||
self.compute_events()
|
||||
if let last_event = thread.events().last, last_event.pubkey == damus.pubkey, user_just_posted_flag {
|
||||
self.go_to_event(scroller: scroller, note_id: last_event.id)
|
||||
user_just_posted_flag = false
|
||||
}
|
||||
}
|
||||
.onAppear() {
|
||||
thread.subscribe()
|
||||
self.compute_events()
|
||||
scroll_to_event(scroller: scroller, id: thread.event.id, delay: 0.1, animate: false)
|
||||
}
|
||||
.onDisappear() {
|
||||
thread.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toggle_thread_view() {
|
||||
NotificationCenter.default.post(name: .toggle_thread_view, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
struct ChatroomView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Group {
|
||||
ChatroomThreadView(damus: test_damus_state, thread: ThreadModel(event: test_note, damus_state: test_damus_state))
|
||||
.previewDisplayName("Test note")
|
||||
|
||||
let test_thread = ThreadModel(event: test_thread_note_1, damus_state: test_damus_state)
|
||||
ChatroomThreadView(damus: test_damus_state, thread: test_thread)
|
||||
.onAppear {
|
||||
test_thread.add_event(test_thread_note_2, keypair: test_keypair)
|
||||
test_thread.add_event(test_thread_note_3, keypair: test_keypair)
|
||||
test_thread.add_event(test_thread_note_4, keypair: test_keypair)
|
||||
test_thread.add_event(test_thread_note_5, keypair: test_keypair)
|
||||
test_thread.add_event(test_thread_note_6, keypair: test_keypair)
|
||||
test_thread.add_event(test_thread_note_7, keypair: test_keypair)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func scroll_after_load(thread: ThreadModel, proxy: ScrollViewProxy) {
|
||||
scroll_to_event(scroller: proxy, id: thread.event.id, delay: 0.1, animate: false)
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
//
|
||||
// ReplyQuoteView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2022-04-19.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ReplyQuoteView: View {
|
||||
let keypair: Keypair
|
||||
let quoter: NostrEvent
|
||||
let event_id: NoteId
|
||||
let state: DamusState
|
||||
@ObservedObject var thread: ThreadModel
|
||||
let options: EventViewOptions
|
||||
|
||||
func content(event: NdbNote) -> some View {
|
||||
ZStack(alignment: .leading) {
|
||||
VStack(alignment: .leading) {
|
||||
HStack(alignment: .center) {
|
||||
if should_show_event(event: event, damus_state: state) {
|
||||
ProfilePicView(pubkey: event.pubkey, size: 14, highlight: .reply, profiles: state.profiles, disable_animation: false)
|
||||
let blur_images = should_blur_images(settings: state.settings, contacts: state.contacts, ev: event, our_pubkey: state.pubkey)
|
||||
NoteContentView(damus_state: state, event: event, blur_images: blur_images, size: .small, options: options)
|
||||
.font(.callout)
|
||||
.lineLimit(1)
|
||||
.padding(.bottom, -7)
|
||||
.padding(.top, -5)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.frame(height: 20)
|
||||
.clipped()
|
||||
}
|
||||
else {
|
||||
Text("Note you've muted", comment: "Label indicating note has been muted")
|
||||
.italic()
|
||||
.font(.caption)
|
||||
.opacity(0.5)
|
||||
.padding(.bottom, -7)
|
||||
.padding(.top, -5)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.frame(height: 20)
|
||||
.clipped()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(5)
|
||||
.padding(.leading, 5+3)
|
||||
Rectangle()
|
||||
.foregroundStyle(.accent)
|
||||
.frame(width: 3)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let event = state.events.lookup(event_id) {
|
||||
self.content(event: event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ReplyQuoteView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let s = test_damus_state
|
||||
let quoter = test_note
|
||||
ReplyQuoteView(keypair: s.keypair, quoter: quoter, event_id: test_note.id, state: s, thread: ThreadModel(event: quoter, damus_state: s), options: [.no_previews, .no_action_bar, .truncate_content_very_short, .no_show_more, .no_translate])
|
||||
}
|
||||
}
|
||||
@@ -67,10 +67,6 @@ struct ConfigView: View {
|
||||
NavigationLink(value: Route.DeveloperSettings(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("Developer", comment: "Section header for developer settings"), img_name: "magic-stick2.fill", color: DamusColors.adaptableBlack)
|
||||
}
|
||||
|
||||
NavigationLink(value: Route.FirstAidSettings(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("First Aid", comment: "Section header for first aid tools and settings"), img_name: "help2", color: .red)
|
||||
}
|
||||
}
|
||||
|
||||
Section(NSLocalizedString("Sign Out", comment: "Section title for signing out")) {
|
||||
@@ -88,7 +84,7 @@ struct ConfigView: View {
|
||||
}
|
||||
|
||||
if state.is_privkey_user {
|
||||
Section(header: Text("Permanently Delete Account", comment: "Section title for deleting the user")) {
|
||||
Section(header: Text(NSLocalizedString("Permanently Delete Account", comment: "Section title for deleting the user"))) {
|
||||
Button(action: {
|
||||
delete_account_warning = true
|
||||
}, label: {
|
||||
|
||||
@@ -25,44 +25,68 @@ struct CreateAccountView: View {
|
||||
var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
VStack {
|
||||
Spacer()
|
||||
VStack(alignment: .center) {
|
||||
|
||||
EditPictureControl(uploader: .nostrBuild, pubkey: account.pubkey, size: 75, setup: true, image_url: $account.profile_image , uploadObserver: profileUploadObserver, callback: uploadedProfilePicture)
|
||||
.shadow(radius: 2)
|
||||
.padding(.top, 100)
|
||||
|
||||
Text("Add Photo", comment: "Label to indicate user can add a photo.")
|
||||
EditPictureControl(uploader: .nostrBuild, pubkey: account.pubkey, image_url: $account.profile_image , uploadObserver: profileUploadObserver, callback: uploadedProfilePicture)
|
||||
|
||||
Text(NSLocalizedString("Public Key", comment: "Label to indicate the public key of the account."))
|
||||
.bold()
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
.padding()
|
||||
.onTapGesture {
|
||||
regen_key()
|
||||
}
|
||||
|
||||
KeyText($account.pubkey)
|
||||
.padding(.horizontal, 20)
|
||||
.onTapGesture {
|
||||
regen_key()
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 250, alignment: .center)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(DamusColors.adaptableGrey, strokeBorder: .gray.opacity(0.5), lineWidth: 1)
|
||||
}
|
||||
|
||||
SignupForm {
|
||||
FormLabel(NSLocalizedString("Name", comment: "Label to prompt name entry."), optional: false)
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
FormTextInput(NSLocalizedString("Satoshi Nakamoto", comment: "Name of Bitcoin creator(s)."), text: $account.name)
|
||||
FormLabel(NSLocalizedString("Display name", comment: "Label to prompt display name entry."), optional: true)
|
||||
FormTextInput(NSLocalizedString("Satoshi Nakamoto", comment: "Name of Bitcoin creator(s)."), text: $account.real_name)
|
||||
.textInputAutocapitalization(.words)
|
||||
|
||||
FormLabel(NSLocalizedString("Bio", comment: "Label to prompt bio entry for user to describe themself."), optional: true)
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
FormTextInput(NSLocalizedString("Absolute legend.", comment: "Example Bio"), text: $account.about)
|
||||
FormLabel(NSLocalizedString("About", comment: "Label to prompt for about text entry for user to describe about themself."), optional: true)
|
||||
FormTextInput(NSLocalizedString("Creator(s) of Bitcoin. Absolute legend.", comment: "Example description about Bitcoin creator(s), Satoshi Nakamoto."), text: $account.about)
|
||||
}
|
||||
.padding(.top, 25)
|
||||
.padding(.top, 10)
|
||||
|
||||
Button(action: {
|
||||
nav.push(route: Route.SaveKeys(account: account))
|
||||
}) {
|
||||
HStack {
|
||||
Text("Next", comment: "Button to continue with account creation.")
|
||||
Text("Create account now", comment: "Button to create account.")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
.disabled(profileUploadObserver.isLoading || account.name.isEmpty)
|
||||
.opacity(profileUploadObserver.isLoading || account.name.isEmpty ? 0.5 : 1)
|
||||
.disabled(profileUploadObserver.isLoading)
|
||||
.opacity(profileUploadObserver.isLoading ? 0.5 : 1)
|
||||
.padding(.top, 20)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
Text("By signing up, you agree to our ", comment: "Ask the user if they already have an account on Nostr")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color("DamusMediumGrey"))
|
||||
|
||||
Button(action: {
|
||||
nav.push(route: Route.EULA)
|
||||
}, label: {
|
||||
Text("EULA")
|
||||
.font(.subheadline)
|
||||
})
|
||||
.padding(.vertical, 5)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
LoginPrompt()
|
||||
.padding(.top)
|
||||
|
||||
@@ -70,8 +94,8 @@ struct CreateAccountView: View {
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.background(DamusBackground(maxHeight: UIScreen.main.bounds.size.height/2), alignment: .top)
|
||||
.dismissKeyboardOnTap()
|
||||
.navigationTitle("Create account")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.navigationBarItems(leading: BackNav())
|
||||
@@ -87,7 +111,7 @@ struct LoginPrompt: View {
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text("Already on Nostr?", comment: "Ask the user if they already have an account on Nostr")
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
.foregroundColor(Color("DamusMediumGrey"))
|
||||
|
||||
Button(NSLocalizedString("Login", comment: "Button to navigate to login view.")) {
|
||||
self.dismiss()
|
||||
@@ -103,8 +127,8 @@ struct BackNav: View {
|
||||
var body: some View {
|
||||
Image("chevron-left")
|
||||
.foregroundColor(DamusColors.adaptableBlack)
|
||||
.onTapGesture {
|
||||
self.dismiss()
|
||||
.onTapGesture {
|
||||
self.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,11 +148,20 @@ extension View {
|
||||
|
||||
struct CreateAccountView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let model = CreateAccountModel(display_name: "", name: "jb55", about: "")
|
||||
let model = CreateAccountModel(real: "", nick: "jb55", about: "")
|
||||
return CreateAccountView(account: model, nav: .init())
|
||||
}
|
||||
}
|
||||
|
||||
func KeyText(_ pubkey: Binding<Pubkey>) -> some View {
|
||||
let bechkey = bech32_encode(hrp: PUBKEY_HRP, pubkey.wrappedValue.bytes)
|
||||
return Text(bechkey)
|
||||
.textSelection(.enabled)
|
||||
.multilineTextAlignment(.center)
|
||||
.font(.callout.monospaced())
|
||||
.foregroundStyle(DamusLogoGradient.gradient)
|
||||
}
|
||||
|
||||
func FormTextInput(_ title: String, text: Binding<String>) -> some View {
|
||||
return TextField("", text: text)
|
||||
.placeholder(when: text.wrappedValue.isEmpty) {
|
||||
@@ -138,10 +171,6 @@ func FormTextInput(_ title: String, text: Binding<String>) -> some View {
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(.gray.opacity(0.5), lineWidth: 1)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.foregroundColor(.damusAdaptableWhite)
|
||||
}
|
||||
}
|
||||
.font(.body.bold())
|
||||
}
|
||||
@@ -154,10 +183,6 @@ func FormLabel(_ title: String, optional: Bool = false) -> some View {
|
||||
Text("optional", comment: "Label indicating that a form input is optional.")
|
||||
.font(.callout)
|
||||
.foregroundColor(DamusColors.mediumGrey)
|
||||
} else {
|
||||
Text("required", comment: "Label indicating that a form input is required.")
|
||||
.font(.callout)
|
||||
.foregroundColor(DamusColors.mediumGrey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ struct DMChatView: View, KeyboardReadable {
|
||||
ScrollViewReader { scroller in
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading) {
|
||||
ForEach(Array(zip(dms.events, dms.events.indices)).filter { should_show_event(state: damus_state, ev: $0.0)}, id: \.0.id) { (ev, ind) in
|
||||
ForEach(Array(zip(dms.events, dms.events.indices)).filter { should_show_event(state: damus_state, ev: $0.0, keypair: damus_state.keypair)}, id: \.0.id) { (ev, ind) in
|
||||
DMView(event: dms.events[ind], damus_state: damus_state)
|
||||
.contextMenu{MenuItems(damus_state: damus_state, event: ev, target_pubkey: ev.pubkey, profileModel: ProfileModel(pubkey: ev.pubkey, damus: damus_state))}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ struct DirectMessagesView: View {
|
||||
|
||||
func MaybeEvent(_ model: DirectMessageModel) -> some View {
|
||||
Group {
|
||||
if let ev = model.events.last(where: { should_show_event(state: damus_state, ev: $0) }) {
|
||||
if let ev = model.events.last(where: { should_show_event(state: damus_state, ev: $0, keypair: damus_state.keypair) }) {
|
||||
EventView(damus: damus_state, event: ev, pubkey: model.pubkey, options: options)
|
||||
.onTapGesture {
|
||||
self.model.set_active_dm_model(model)
|
||||
|
||||
@@ -45,8 +45,6 @@ struct EventView: View {
|
||||
}
|
||||
} else if event.known_kind == .longform {
|
||||
LongformPreview(state: damus, ev: event, options: options)
|
||||
} else if event.known_kind == .highlight {
|
||||
HighlightView(state: damus, event: event, options: options)
|
||||
} else {
|
||||
TextEvent(damus: damus, event: event, pubkey: pubkey, options: options)
|
||||
//.padding([.top], 6)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// ContextButton.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-06-01.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContextButton: View {
|
||||
var body: some View {
|
||||
Text(verbatim: /*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
}
|
||||
}
|
||||
|
||||
struct ContextButton_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContextButton()
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ struct ProxyView: View {
|
||||
HStack {
|
||||
let protocolLogo = get_protocol_image(protocolName: proxy.protocolName)
|
||||
if protocolLogo.isEmpty {
|
||||
Text(proxy.protocolName)
|
||||
Text("\(proxy.protocolName)")
|
||||
.font(.caption)
|
||||
} else {
|
||||
Image(protocolLogo)
|
||||
|
||||
@@ -13,18 +13,18 @@ struct ReplyPart: View {
|
||||
let keypair: Keypair
|
||||
let ndb: Ndb
|
||||
|
||||
var replying_to: NostrEvent? {
|
||||
guard let note_ref = event.event_refs(keypair).first(where: { evref in evref.is_direct_reply != nil })?.is_direct_reply else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return events.lookup(note_ref.note_id)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let reply_ref = event.thread_reply()?.reply {
|
||||
let replying_to = events.lookup(reply_ref.note_id)
|
||||
if event.known_kind != .highlight {
|
||||
ReplyDescription(event: event, replying_to: replying_to, ndb: ndb)
|
||||
} else if event.known_kind == .highlight {
|
||||
HighlightDescription(event: event, highlighted_event: replying_to, ndb: ndb)
|
||||
}
|
||||
else {
|
||||
EmptyView()
|
||||
}
|
||||
if event_is_reply(event.event_refs(keypair)) {
|
||||
ReplyDescription(event: event, replying_to: replying_to, ndb: ndb)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
@@ -29,14 +29,12 @@ struct EventBody: View {
|
||||
|
||||
var body: some View {
|
||||
if event.known_kind == .longform {
|
||||
LongformPreviewBody(state: damus_state, ev: event, options: options, header: true)
|
||||
LongformPreviewBody(state: damus_state, ev: event, options: options)
|
||||
|
||||
// truncated longform bodies are just the preview
|
||||
if !options.contains(.truncate_content) {
|
||||
note_content
|
||||
}
|
||||
} else if event.known_kind == .highlight {
|
||||
HighlightBodyView(state: damus_state, ev: event, options: options)
|
||||
} else {
|
||||
note_content
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ struct MenuItems: View {
|
||||
if event.known_kind != .dm {
|
||||
MuteDurationMenu { duration in
|
||||
if let full_keypair = self.damus_state.keypair.to_full(),
|
||||
let new_mutelist_ev = toggle_from_mutelist(keypair: full_keypair, prev: damus_state.mutelist_manager.event, to_toggle: .thread(event.thread_id(), duration?.date_from_now)) {
|
||||
let new_mutelist_ev = toggle_from_mutelist(keypair: full_keypair, prev: damus_state.mutelist_manager.event, to_toggle: .thread(event.thread_id(keypair: damus_state.keypair), duration?.date_from_now)) {
|
||||
damus_state.mutelist_manager.set_mutelist(new_mutelist_ev)
|
||||
damus_state.postbox.send(new_mutelist_ev)
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
//
|
||||
// HighlightDescription.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 4/28/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// Modified from Reply Description
|
||||
struct HighlightDescription: View {
|
||||
let event: NostrEvent
|
||||
let highlighted_event: NostrEvent?
|
||||
let ndb: Ndb
|
||||
|
||||
var body: some View {
|
||||
(Text(Image(systemName: "highlighter")) + Text(verbatim: " \(highlight_desc(ndb: ndb, event: event, highlighted_event: highlighted_event))"))
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct HighlightDescription_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HighlightDescription(event: test_note, highlighted_event: test_note, ndb: test_damus_state.ndb)
|
||||
}
|
||||
}
|
||||
|
||||
func highlight_desc(ndb: Ndb, event: NostrEvent, highlighted_event: NostrEvent?, locale: Locale = Locale.current) -> String {
|
||||
let desc = make_reply_description(event, replying_to: highlighted_event)
|
||||
let pubkeys = desc.pubkeys
|
||||
|
||||
let bundle = bundleForLocale(locale: locale)
|
||||
|
||||
if pubkeys.count == 0 {
|
||||
return NSLocalizedString("Highlighted", bundle: bundle, comment: "Label to indicate that the user is highlighting their own post.")
|
||||
}
|
||||
|
||||
guard let profile_txn = NdbTxn(ndb: ndb) else {
|
||||
return ""
|
||||
}
|
||||
|
||||
let names: [String] = pubkeys.map { pk in
|
||||
let prof = ndb.lookup_profile_with_txn(pk, txn: profile_txn)
|
||||
|
||||
return Profile.displayName(profile: prof?.profile, pubkey: pk).username.truncate(maxLength: 50)
|
||||
}
|
||||
|
||||
let uniqueNames: [String] = Array(Set(names))
|
||||
return String(format: NSLocalizedString("Highlighted %@", bundle: bundle, comment: "Label to indicate that the user is highlighting 1 user."), locale: locale, uniqueNames.first ?? "")
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
//
|
||||
// HighlightEventRef.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 4/29/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct HighlightEventRef: View {
|
||||
let damus_state: DamusState
|
||||
let event_ref: NoteId
|
||||
|
||||
init(damus_state: DamusState, event_ref: NoteId) {
|
||||
self.damus_state = damus_state
|
||||
self.event_ref = event_ref
|
||||
}
|
||||
|
||||
struct FailedImage: View {
|
||||
var body: some View {
|
||||
Image("markdown")
|
||||
.resizable()
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
.background(DamusColors.neutral3)
|
||||
.frame(width: 35, height: 35)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.overlay(RoundedRectangle(cornerRadius: 10).stroke(.gray.opacity(0.5), lineWidth: 0.5))
|
||||
.scaledToFit()
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
EventLoaderView(damus_state: damus_state, event_id: event_ref) { event in
|
||||
EventMutingContainerView(damus_state: damus_state, event: event) {
|
||||
if event.known_kind == .longform {
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
let longform_event = LongformEvent.parse(from: event)
|
||||
if let url = longform_event.image {
|
||||
KFAnimatedImage(url)
|
||||
.callbackQueue(.dispatch(.global(qos:.background)))
|
||||
.backgroundDecode(true)
|
||||
.imageContext(.note, disable_animation: true)
|
||||
.image_fade(duration: 0.25)
|
||||
.cancelOnDisappear(true)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.background {
|
||||
FailedImage()
|
||||
}
|
||||
.frame(width: 35, height: 35)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.overlay(RoundedRectangle(cornerRadius: 10).stroke(.gray.opacity(0.5), lineWidth: 0.5))
|
||||
.scaledToFit()
|
||||
} else {
|
||||
FailedImage()
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(longform_event.title ?? "Untitled")
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.lineLimit(1)
|
||||
|
||||
let profile_txn = damus_state.profiles.lookup(id: longform_event.event.pubkey, txn_name: "highlight-profile")
|
||||
let profile = profile_txn?.unsafeUnownedValue
|
||||
|
||||
if let display_name = profile?.display_name {
|
||||
Text(display_name)
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.gray)
|
||||
} else if let name = profile?.name {
|
||||
Text(name)
|
||||
.font(.system(size: 12))
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding([.leading, .vertical], 7)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.cornerRadius(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(DamusColors.neutral3, lineWidth: 2)
|
||||
)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
//
|
||||
// HighlightLink.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 4/28/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct HighlightLink: View {
|
||||
let state: DamusState
|
||||
let url: URL
|
||||
let content: String
|
||||
@Environment(\.openURL) var openURL
|
||||
|
||||
func text_fragment_url() -> URL? {
|
||||
let fragmentDirective = "#:~:"
|
||||
let textDirective = "text="
|
||||
let separator = ","
|
||||
var text = ""
|
||||
|
||||
let components = content.components(separatedBy: " ")
|
||||
if components.count <= 10 {
|
||||
text = content
|
||||
} else {
|
||||
let textStart = Array(components.prefix(5)).joined(separator: " ")
|
||||
let textEnd = Array(components.suffix(2)).joined(separator: " ")
|
||||
text = textStart + separator + textEnd
|
||||
}
|
||||
|
||||
let url_with_fragments = url.absoluteString + fragmentDirective + textDirective + text
|
||||
return URL(string: url_with_fragments)
|
||||
}
|
||||
|
||||
func get_url_icon() -> URL? {
|
||||
var icon = URL(string: url.absoluteString + "/favicon.ico")
|
||||
if let url_host = url.host() {
|
||||
icon = URL(string: "https://" + url_host + "/favicon.ico")
|
||||
}
|
||||
return icon
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Button(action: {
|
||||
openURL(text_fragment_url() ?? url)
|
||||
}, label: {
|
||||
HStack(spacing: 10) {
|
||||
if let url = get_url_icon() {
|
||||
KFAnimatedImage(url)
|
||||
.imageContext(.pfp, disable_animation: true)
|
||||
.cancelOnDisappear(true)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.placeholder { _ in
|
||||
Image("link")
|
||||
.resizable()
|
||||
.padding(5)
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
.background(DamusColors.adaptableWhite)
|
||||
}
|
||||
.frame(width: 35, height: 35)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
.scaledToFit()
|
||||
} else {
|
||||
Image("link")
|
||||
.resizable()
|
||||
.padding(5)
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
.background(DamusColors.adaptableWhite)
|
||||
.frame(width: 35, height: 35)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
|
||||
Text(url.absoluteString)
|
||||
.font(eventviewsize_to_font(.normal, font_size: state.settings.font_size))
|
||||
.foregroundColor(DamusColors.adaptableBlack)
|
||||
.truncationMode(.tail)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.padding([.leading, .vertical], 7)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(DamusColors.neutral3)
|
||||
.cornerRadius(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(DamusColors.neutral3, lineWidth: 2)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct HighlightLink_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let url = URL(string: "https://damus.io")!
|
||||
VStack {
|
||||
HighlightLink(state: test_damus_state, url: url, content: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
//
|
||||
// HighlightPostView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 5/26/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct HighlightPostView: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
@Binding var selectedText: String
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
VStack {
|
||||
HStack(spacing: 5.0) {
|
||||
Button(action: {
|
||||
dismiss()
|
||||
}, label: {
|
||||
Text("Cancel", comment: "Button to cancel out of highlighting a note.")
|
||||
.padding(10)
|
||||
})
|
||||
.buttonStyle(NeutralButtonStyle())
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(NSLocalizedString("Post", comment: "Button to post a highlight.")) {
|
||||
var tags: [[String]] = [ ["e", "\(self.event.id)"] ]
|
||||
tags.append(["context", self.event.content])
|
||||
|
||||
let kind = NostrKind.highlight.rawValue
|
||||
guard let ev = NostrEvent(content: selectedText, keypair: damus_state.keypair, kind: kind, tags: tags) else {
|
||||
return
|
||||
}
|
||||
damus_state.postbox.send(ev)
|
||||
dismiss()
|
||||
}
|
||||
.bold()
|
||||
.buttonStyle(GradientButtonStyle(padding: 10))
|
||||
}
|
||||
|
||||
Divider()
|
||||
.foregroundColor(DamusColors.neutral3)
|
||||
.padding(.top, 5)
|
||||
}
|
||||
.frame(height: 30)
|
||||
.padding()
|
||||
.padding(.top, 15)
|
||||
|
||||
HStack {
|
||||
var attributedString: AttributedString {
|
||||
var attributedString = AttributedString(self.event.content)
|
||||
|
||||
if let range = attributedString.range(of: selectedText) {
|
||||
attributedString[range].backgroundColor = DamusColors.highlight
|
||||
}
|
||||
|
||||
return attributedString
|
||||
}
|
||||
|
||||
Text(attributedString)
|
||||
.lineSpacing(5)
|
||||
.padding(10)
|
||||
}
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 25).fill(DamusColors.highlight).frame(width: 4),
|
||||
alignment: .leading
|
||||
)
|
||||
.padding()
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
//
|
||||
// HighlightView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 4/22/24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct HighlightTruncatedText: View {
|
||||
let attributedString: AttributedString
|
||||
let maxChars: Int
|
||||
|
||||
init(attributedString: AttributedString, maxChars: Int = 360) {
|
||||
self.attributedString = attributedString
|
||||
self.maxChars = maxChars
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
let truncatedAttributedString: AttributedString? = attributedString.truncateOrNil(maxLength: maxChars)
|
||||
|
||||
if let truncatedAttributedString {
|
||||
Text(truncatedAttributedString)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
} else {
|
||||
Text(attributedString)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
if truncatedAttributedString != nil {
|
||||
Spacer()
|
||||
Button(NSLocalizedString("Show more", comment: "Button to show entire note.")) { }
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HighlightBodyView: View {
|
||||
let state: DamusState
|
||||
let event: HighlightEvent
|
||||
let options: EventViewOptions
|
||||
|
||||
init(state: DamusState, ev: HighlightEvent, options: EventViewOptions) {
|
||||
self.state = state
|
||||
self.event = ev
|
||||
self.options = options
|
||||
}
|
||||
|
||||
init(state: DamusState, ev: NostrEvent, options: EventViewOptions) {
|
||||
self.state = state
|
||||
self.event = HighlightEvent.parse(from: ev)
|
||||
self.options = options
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if options.contains(.wide) {
|
||||
Main.padding(.horizontal)
|
||||
} else {
|
||||
Main
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var truncate: Bool {
|
||||
return options.contains(.truncate_content)
|
||||
}
|
||||
|
||||
var truncate_very_short: Bool {
|
||||
return options.contains(.truncate_content_very_short)
|
||||
}
|
||||
|
||||
func truncatedText(attributedString: AttributedString) -> some View {
|
||||
Group {
|
||||
if truncate_very_short {
|
||||
HighlightTruncatedText(attributedString: attributedString, maxChars: 140)
|
||||
.font(eventviewsize_to_font(.normal, font_size: state.settings.font_size))
|
||||
}
|
||||
else if truncate {
|
||||
HighlightTruncatedText(attributedString: attributedString)
|
||||
.font(eventviewsize_to_font(.normal, font_size: state.settings.font_size))
|
||||
} else {
|
||||
Text(attributedString)
|
||||
.font(eventviewsize_to_font(.normal, font_size: state.settings.font_size))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var Main: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack {
|
||||
var attributedString: AttributedString {
|
||||
var attributedString: AttributedString = ""
|
||||
if let context = event.context {
|
||||
if context.count < event.event.content.count {
|
||||
attributedString = AttributedString(event.event.content)
|
||||
} else {
|
||||
attributedString = AttributedString(context)
|
||||
}
|
||||
} else {
|
||||
attributedString = AttributedString(event.event.content)
|
||||
}
|
||||
|
||||
if let range = attributedString.range(of: event.event.content) {
|
||||
attributedString[range].backgroundColor = DamusColors.highlight
|
||||
}
|
||||
return attributedString
|
||||
}
|
||||
|
||||
truncatedText(attributedString: attributedString)
|
||||
.lineSpacing(5)
|
||||
.padding(10)
|
||||
}
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 25).fill(DamusColors.highlight).frame(width: 4),
|
||||
alignment: .leading
|
||||
)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
if let url = event.url_ref {
|
||||
HighlightLink(state: state, url: url, content: event.event.content)
|
||||
} else {
|
||||
if let evRef = event.event_ref {
|
||||
if let eventHex = hex_decode_id(evRef) {
|
||||
HighlightEventRef(damus_state: state, event_ref: NoteId(eventHex))
|
||||
.padding(.top, 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HighlightView: View {
|
||||
let state: DamusState
|
||||
let event: HighlightEvent
|
||||
let options: EventViewOptions
|
||||
|
||||
init(state: DamusState, event: NostrEvent, options: EventViewOptions) {
|
||||
self.state = state
|
||||
self.event = HighlightEvent.parse(from: event)
|
||||
self.options = options.union(.no_mentions)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
EventShell(state: state, event: event.event, options: options) {
|
||||
HighlightBodyView(state: state, ev: event, options: options)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HighlightView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
|
||||
let content = "Nostr, a decentralized and open social network protocol. Without ads, toxic algorithms, or censorship"
|
||||
let context = "Damus is built on Nostr, a decentralized and open social network protocol. Without ads, toxic algorithms, or censorship, Damus gives you access to the social network that a truly free and healthy society needs — and deserves."
|
||||
|
||||
let test_highlight_event = HighlightEvent.parse(from: NostrEvent(
|
||||
content: content,
|
||||
keypair: test_keypair,
|
||||
kind: NostrKind.highlight.rawValue,
|
||||
tags: [
|
||||
["context", context],
|
||||
["r", "https://damus.io"],
|
||||
["p", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"],
|
||||
])!
|
||||
)
|
||||
|
||||
let test_highlight_event2 = HighlightEvent.parse(from: NostrEvent(
|
||||
content: content,
|
||||
keypair: test_keypair,
|
||||
kind: NostrKind.highlight.rawValue,
|
||||
tags: [
|
||||
["context", context],
|
||||
["e", "36017b098859d62e1dbd802290d59c9de9f18bb0ca00ba4b875c2930dd5891ae"],
|
||||
["p", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"],
|
||||
])!
|
||||
)
|
||||
VStack {
|
||||
HighlightView(state: test_damus_state, event: test_highlight_event.event, options: [])
|
||||
|
||||
HighlightView(state: test_damus_state, event: test_highlight_event2.event, options: [.wide])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,31 +6,25 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct LongformPreviewBody: View {
|
||||
let state: DamusState
|
||||
let event: LongformEvent
|
||||
let options: EventViewOptions
|
||||
let header: Bool
|
||||
@State var blur_images: Bool = true
|
||||
|
||||
@ObservedObject var artifacts: NoteArtifactsModel
|
||||
|
||||
init(state: DamusState, ev: LongformEvent, options: EventViewOptions, header: Bool) {
|
||||
init(state: DamusState, ev: LongformEvent, options: EventViewOptions) {
|
||||
self.state = state
|
||||
self.event = ev
|
||||
self.options = options
|
||||
self.header = header
|
||||
|
||||
self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.event.id).artifacts_model)
|
||||
}
|
||||
|
||||
init(state: DamusState, ev: NostrEvent, options: EventViewOptions, header: Bool) {
|
||||
init(state: DamusState, ev: NostrEvent, options: EventViewOptions) {
|
||||
self.state = state
|
||||
self.event = LongformEvent.parse(from: ev)
|
||||
self.options = options
|
||||
self.header = header
|
||||
|
||||
self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.id).artifacts_model)
|
||||
}
|
||||
@@ -39,67 +33,6 @@ struct LongformPreviewBody: View {
|
||||
let wordCount = pluralizedString(key: "word_count", count: words)
|
||||
return Text(wordCount)
|
||||
}
|
||||
|
||||
var truncate: Bool {
|
||||
return options.contains(.truncate_content)
|
||||
}
|
||||
|
||||
var truncate_very_short: Bool {
|
||||
return options.contains(.truncate_content_very_short)
|
||||
}
|
||||
|
||||
func truncatedText(content: CompatibleText) -> some View {
|
||||
Group {
|
||||
if truncate_very_short {
|
||||
TruncatedText(text: content, maxChars: 140, show_show_more_button: !options.contains(.no_show_more))
|
||||
.font(header ? .body : .caption)
|
||||
.foregroundColor(.gray)
|
||||
.padding(.horizontal, 10)
|
||||
}
|
||||
else if truncate {
|
||||
TruncatedText(text: content, show_show_more_button: !options.contains(.no_show_more))
|
||||
.font(header ? .body : .caption)
|
||||
.foregroundColor(.gray)
|
||||
.padding(.horizontal, 10)
|
||||
} else {
|
||||
content.text
|
||||
.font(header ? .body : .caption)
|
||||
.foregroundColor(.gray)
|
||||
.padding(.horizontal, 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Placeholder(url: URL) -> some View {
|
||||
Group {
|
||||
if let meta = state.events.lookup_img_metadata(url: url),
|
||||
case .processed(let blurhash) = meta.state {
|
||||
Image(uiImage: blurhash)
|
||||
.resizable()
|
||||
.frame(maxWidth: .infinity, maxHeight: header ? .infinity : 150)
|
||||
} else {
|
||||
DamusColors.adaptableWhite
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func titleImage(url: URL) -> some View {
|
||||
KFAnimatedImage(url)
|
||||
.callbackQueue(.dispatch(.global(qos:.background)))
|
||||
.backgroundDecode(true)
|
||||
.imageContext(.note, disable_animation: state.settings.disable_animation)
|
||||
.image_fade(duration: 0.25)
|
||||
.cancelOnDisappear(true)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.background {
|
||||
Placeholder(url: url)
|
||||
}
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(maxWidth: .infinity, maxHeight: header ? .infinity : 150)
|
||||
.cornerRadius(1)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
@@ -113,71 +46,23 @@ struct LongformPreviewBody: View {
|
||||
|
||||
var Main: some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
if let url = event.image {
|
||||
if (self.options.contains(.no_media)) {
|
||||
EmptyView()
|
||||
} else if !blur_images || (!blur_images && !state.settings.media_previews) {
|
||||
titleImage(url: url)
|
||||
} else if blur_images || (blur_images && !state.settings.media_previews) {
|
||||
ZStack {
|
||||
titleImage(url: url)
|
||||
Blur()
|
||||
.onTapGesture {
|
||||
blur_images = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text(event.title ?? "Untitled")
|
||||
.font(header ? .title : .headline)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.top, 5)
|
||||
|
||||
if let summary = event.summary {
|
||||
truncatedText(content: CompatibleText(stringLiteral: summary))
|
||||
}
|
||||
|
||||
if let labels = event.labels {
|
||||
ScrollView(.horizontal) {
|
||||
HStack {
|
||||
ForEach(labels, id: \.self) { label in
|
||||
Text(label)
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
.padding(EdgeInsets(top: 5, leading: 15, bottom: 5, trailing: 15))
|
||||
.background(DamusColors.neutral1)
|
||||
.cornerRadius(20)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.stroke(DamusColors.neutral3, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.scrollIndicators(.hidden)
|
||||
.padding(10)
|
||||
if let title = event.title {
|
||||
Text(title)
|
||||
.font(.title)
|
||||
} else {
|
||||
Text("Untitled", comment: "Text indicating that the long-form note title is untitled.")
|
||||
.font(.title)
|
||||
}
|
||||
|
||||
|
||||
Text(event.summary ?? "")
|
||||
.foregroundColor(.gray)
|
||||
|
||||
if case .loaded(let arts) = artifacts.state,
|
||||
case .longform(let longform) = arts
|
||||
{
|
||||
Words(longform.words).font(.footnote)
|
||||
.padding([.horizontal, .bottom], 10)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.background(DamusColors.neutral3)
|
||||
.cornerRadius(10)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(DamusColors.neutral1, lineWidth: 1)
|
||||
)
|
||||
.padding(.top, 10)
|
||||
.onAppear {
|
||||
blur_images = should_blur_images(settings: state.settings, contacts: state.contacts, ev: event.event, our_pubkey: state.pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +79,7 @@ struct LongformPreview: View {
|
||||
|
||||
var body: some View {
|
||||
EventShell(state: state, event: event.event, options: options) {
|
||||
LongformPreviewBody(state: state, ev: event, options: options, header: false)
|
||||
LongformPreviewBody(state: state, ev: event, options: options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,10 +21,10 @@ struct LongformView: View {
|
||||
var options: EventViewOptions {
|
||||
return [.wide, .no_mentions, .no_replying_to]
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
EventShell(state: state, event: event.event, options: options) {
|
||||
SelectableText(damus_state: state, event: event.event, attributedString: AttributedString(stringLiteral: event.title ?? "Untitled"), size: .title)
|
||||
SelectableText(attributedString: AttributedString(stringLiteral: event.title ?? "Untitled"), size: .title)
|
||||
|
||||
NoteContentView(damus_state: state, event: event.event, blur_images: false, size: .selected, options: options)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,14 @@ struct SelectedEventView: View {
|
||||
|
||||
@StateObject var bar: ActionBarModel
|
||||
|
||||
var replying_to: NostrEvent? {
|
||||
guard let note_ref = event.event_refs(damus.keypair).first(where: { evref in evref.is_direct_reply != nil })?.is_direct_reply else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return damus.events.lookup(note_ref.note_id)
|
||||
}
|
||||
|
||||
init(damus: DamusState, event: NostrEvent, size: EventViewKind) {
|
||||
self.damus = damus
|
||||
self.event = event
|
||||
@@ -40,17 +48,8 @@ struct SelectedEventView: View {
|
||||
.minimumScaleFactor(0.75)
|
||||
.lineLimit(1)
|
||||
|
||||
if let reply_ref = event.thread_reply()?.reply {
|
||||
let replying_to = damus.events.lookup(reply_ref.note_id)
|
||||
if event.known_kind == .highlight {
|
||||
HighlightDescription(event: event, highlighted_event: replying_to, ndb: damus.ndb)
|
||||
.padding(.horizontal)
|
||||
} else {
|
||||
ReplyDescription(event: event, replying_to: replying_to, ndb: damus.ndb)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
} else if event.known_kind == .highlight {
|
||||
HighlightDescription(event: event, highlighted_event: nil, ndb: damus.ndb)
|
||||
if event_is_reply(event.event_refs(damus.keypair)) {
|
||||
ReplyDescription(event: event, replying_to: replying_to, ndb: damus.ndb)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,11 +21,9 @@ struct EventViewOptions: OptionSet {
|
||||
static let no_mentions = EventViewOptions(rawValue: 1 << 9)
|
||||
static let no_media = EventViewOptions(rawValue: 1 << 10)
|
||||
static let truncate_content_very_short = EventViewOptions(rawValue: 1 << 11)
|
||||
static let no_previews = EventViewOptions(rawValue: 1 << 12)
|
||||
static let no_show_more = EventViewOptions(rawValue: 1 << 13)
|
||||
|
||||
static let embedded: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested]
|
||||
static let embedded_text_only: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested, .no_media, .truncate_content_very_short, .no_previews]
|
||||
static let embedded_text_only: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested, .no_media, .truncate_content_very_short]
|
||||
}
|
||||
|
||||
struct TextEvent: View {
|
||||
|
||||
@@ -145,7 +145,7 @@ struct FullScreenCarouselView_Previews: PreviewProvider {
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
Text(verbatim: "Some content")
|
||||
Text("Some content")
|
||||
.padding()
|
||||
.foregroundColor(.white)
|
||||
|
||||
|
||||
@@ -62,9 +62,8 @@ struct LoginView: View {
|
||||
var body: some View {
|
||||
ZStack(alignment: .top) {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
SignInHeader()
|
||||
.padding(.top, 100)
|
||||
|
||||
SignInEntry(key: $key, shouldSaveKey: $shouldSaveKey)
|
||||
|
||||
@@ -113,9 +112,8 @@ struct LoginView: View {
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.padding(.bottom, 50)
|
||||
}
|
||||
.background(DamusBackground(maxHeight: UIScreen.main.bounds.size.height/2), alignment: .top)
|
||||
.background(DamusBackground(maxHeight: 350), alignment: .top)
|
||||
.onAppear {
|
||||
credential_handler.check_credentials()
|
||||
}
|
||||
@@ -322,13 +320,9 @@ struct KeyInput: View {
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
.padding(.horizontal, 10)
|
||||
.background {
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.stroke(.gray, lineWidth: 1)
|
||||
.background {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.foregroundColor(.damusAdaptableWhite)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -343,12 +337,11 @@ struct SignInHeader: View {
|
||||
.padding(.bottom)
|
||||
|
||||
Text("Sign in", comment: "Title of view to log into an account.")
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
.font(.system(size: 32, weight: .bold))
|
||||
.padding(.bottom, 5)
|
||||
|
||||
Text("Welcome to the social network you control", comment: "Welcome text")
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
.foregroundColor(Color("DamusMediumGrey"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -360,7 +353,6 @@ struct SignInEntry: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Enter your account key", comment: "Prompt for user to enter an account key to login.")
|
||||
.foregroundColor(DamusColors.neutral6)
|
||||
.fontWeight(.medium)
|
||||
.padding(.top, 30)
|
||||
|
||||
@@ -452,9 +444,7 @@ struct LoginView_Previews: PreviewProvider {
|
||||
let bech32_pubkey = "KeyInput"
|
||||
Group {
|
||||
LoginView(key: pubkey, nav: .init())
|
||||
.previewDevice(PreviewDevice(rawValue: "iPhone SE (3rd generation)"))
|
||||
LoginView(key: bech32_pubkey, nav: .init())
|
||||
.previewDevice(PreviewDevice(rawValue: "iPhone 15 Pro Max"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,29 +36,7 @@ struct MediaPicker: UIViewControllerRepresentable {
|
||||
result.itemProvider.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { (item, error) in
|
||||
guard let url = item as? URL else { return }
|
||||
|
||||
if(url.pathExtension == "gif") {
|
||||
// GIFs do not natively support location metadata (See https://superuser.com/a/556320 and https://www.w3.org/Graphics/GIF/spec-gif89a.txt)
|
||||
// It is better to avoid any GPS data processing at all, as it can cause the image to be converted to JPEG.
|
||||
// Therefore, we should load the file directtly and deliver it as "already processed".
|
||||
|
||||
// Load the data for the GIF image
|
||||
// - Don't load it as an UIImage since that can only get exported into JPEG/PNG
|
||||
// - Don't load it as a file representation because it gets deleted before the upload can occur
|
||||
_ = result.itemProvider.loadDataRepresentation(for: .gif, completionHandler: { imageData, error in
|
||||
guard let imageData else { return }
|
||||
let destinationURL = generateUniqueTemporaryMediaURL(fileExtension: "gif")
|
||||
do {
|
||||
try imageData.write(to: destinationURL)
|
||||
Task {
|
||||
await self.chooseMedia(.processed_image(destinationURL))
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Log.error("Failed to write GIF image data from Photo picker into a local copy", for: .image_uploading)
|
||||
}
|
||||
})
|
||||
}
|
||||
else if canGetSourceTypeFromUrl(url: url) {
|
||||
if canGetSourceTypeFromUrl(url: url) {
|
||||
// Media was not taken from camera
|
||||
self.attemptAcquireResourceAndChooseMedia(
|
||||
url: url,
|
||||
|
||||
@@ -17,7 +17,7 @@ struct MuteDurationMenu<T: View>: View {
|
||||
Button {
|
||||
action(duration)
|
||||
} label: {
|
||||
Text(duration.title)
|
||||
Text("\(duration.title)")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
@@ -30,6 +30,6 @@ struct MuteDurationMenu<T: View>: View {
|
||||
MuteDurationMenu { _ in
|
||||
|
||||
} label: {
|
||||
Text(verbatim: "Mute hashtag")
|
||||
Text("Mute hashtag")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ struct MutelistView: View {
|
||||
Section(NSLocalizedString("Hashtags", comment: "Section header title for a list of hashtags that are muted.")) {
|
||||
ForEach(hashtags, id: \.self) { item in
|
||||
if case let MuteItem.hashtag(hashtag, _) = item {
|
||||
Text(verbatim: "#\(hashtag.hashtag)")
|
||||
Text("#\(hashtag.hashtag)")
|
||||
.id(hashtag.hashtag)
|
||||
.swipeActions {
|
||||
RemoveAction(item: .hashtag(hashtag, nil))
|
||||
@@ -76,7 +76,7 @@ struct MutelistView: View {
|
||||
Section(NSLocalizedString("Words", comment: "Section header title for a list of words that are muted.")) {
|
||||
ForEach(words, id: \.self) { item in
|
||||
if case let MuteItem.word(word, _) = item {
|
||||
Text(word)
|
||||
Text("\(word)")
|
||||
.id(word)
|
||||
.swipeActions {
|
||||
RemoveAction(item: .word(word, nil))
|
||||
@@ -94,7 +94,7 @@ struct MutelistView: View {
|
||||
RemoveAction(item: .thread(note_id, nil))
|
||||
}
|
||||
} else {
|
||||
Text("Error retrieving muted event", comment: "Text for an item that application failed to retrieve the muted event for.")
|
||||
Text(NSLocalizedString("Error retrieving muted event", comment: "Text for an item that application failed to retrieve the muted event for."))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,11 +78,11 @@ struct NoteContentView: View {
|
||||
func truncatedText(content: CompatibleText) -> some View {
|
||||
Group {
|
||||
if truncate_very_short {
|
||||
TruncatedText(text: content, maxChars: 140, show_show_more_button: !options.contains(.no_show_more))
|
||||
TruncatedText(text: content, maxChars: 140)
|
||||
.font(eventviewsize_to_font(size, font_size: damus_state.settings.font_size))
|
||||
}
|
||||
else if truncate {
|
||||
TruncatedText(text: content, show_show_more_button: !options.contains(.no_show_more))
|
||||
TruncatedText(text: content)
|
||||
.font(eventviewsize_to_font(size, font_size: damus_state.settings.font_size))
|
||||
} else {
|
||||
content.text
|
||||
@@ -132,10 +132,10 @@ struct NoteContentView: View {
|
||||
VStack(alignment: .leading) {
|
||||
if size == .selected {
|
||||
if with_padding {
|
||||
SelectableText(damus_state: damus_state, event: self.event, attributedString: artifacts.content.attributed, size: self.size)
|
||||
SelectableText(attributedString: artifacts.content.attributed, size: self.size)
|
||||
.padding(.horizontal)
|
||||
} else {
|
||||
SelectableText(damus_state: damus_state, event: self.event, attributedString: artifacts.content.attributed, size: self.size)
|
||||
SelectableText(attributedString: artifacts.content.attributed, size: self.size)
|
||||
}
|
||||
} else {
|
||||
if with_padding {
|
||||
@@ -185,22 +185,18 @@ struct NoteContentView: View {
|
||||
invoicesView(invoices: artifacts.invoices)
|
||||
}
|
||||
}
|
||||
|
||||
if damus_state.settings.media_previews, has_previews {
|
||||
|
||||
if damus_state.settings.media_previews {
|
||||
if with_padding {
|
||||
previewView(links: artifacts.links).padding(.horizontal)
|
||||
} else {
|
||||
previewView(links: artifacts.links)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var has_previews: Bool {
|
||||
!options.contains(.no_previews)
|
||||
}
|
||||
|
||||
|
||||
func loadMediaButton(artifacts: NoteArtifactsSeparated) -> some View {
|
||||
Button(action: {
|
||||
load_media = true
|
||||
@@ -401,14 +397,6 @@ struct NoteContentView_Previews: PreviewProvider {
|
||||
.border(Color.red)
|
||||
}
|
||||
.previewDisplayName("Long-form note")
|
||||
|
||||
VStack {
|
||||
NoteContentView(damus_state: state, event: test_note, blur_images: false, size: .small, options: [.no_previews, .no_action_bar, .truncate_content_very_short, .no_show_more])
|
||||
.font(.callout)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.previewDisplayName("Small single-line note")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,9 +37,9 @@ struct DamusAppNotificationView: View {
|
||||
.shadow(radius: 5, y: 5)
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
HStack(alignment: .center, spacing: 3) {
|
||||
Text("Damus", comment: "Name of the app for the title of an internal notification")
|
||||
Text(NSLocalizedString("Damus", comment: "Name of the app for the title of an internal notification"))
|
||||
.font(.body.weight(.bold))
|
||||
Text(verbatim: "·")
|
||||
Text("·")
|
||||
.foregroundStyle(.secondary)
|
||||
Text(relative_date)
|
||||
.font(.system(size: 16))
|
||||
@@ -49,7 +49,7 @@ struct DamusAppNotificationView: View {
|
||||
Image("check-circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 15, height: 15)
|
||||
Text("Internal app notification", comment: "Badge indicating that a notification is an official internal app notification")
|
||||
Text(NSLocalizedString("Internal app notification", comment: "Badge indicating that a notification is an official internal app notification"))
|
||||
.font(.caption2)
|
||||
.bold()
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ struct EventGroupView: View {
|
||||
return VStack(alignment: .center) {
|
||||
Image("zap.fill")
|
||||
.foregroundColor(.orange)
|
||||
Text(fmt)
|
||||
Text(verbatim: fmt)
|
||||
.foregroundColor(Color.orange)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ struct OnboardingSuggestionsView: View {
|
||||
.navigationBarItems(leading: Button(action: {
|
||||
self.next_page()
|
||||
}, label: {
|
||||
Text("Skip", comment: "Button to dismiss the suggested users screen")
|
||||
Text(NSLocalizedString("Skip", comment: "Button to dismiss the suggested users screen"))
|
||||
.font(.subheadline.weight(.semibold))
|
||||
}))
|
||||
.tag(0)
|
||||
@@ -48,7 +48,7 @@ struct OnboardingSuggestionsView: View {
|
||||
AnyView(
|
||||
HStack {
|
||||
Image(systemName: "sparkles")
|
||||
Text("Add your first post", comment: "Prompt given to the user during onboarding, suggesting them to write their first post")
|
||||
Text(NSLocalizedString("Add your first post", comment: "Prompt given to the user during onboarding, suggesting them to write their first post"))
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
.font(.callout)
|
||||
@@ -97,7 +97,7 @@ fileprivate struct SuggestedUsersPageView: View {
|
||||
Button(action: {
|
||||
self.next_page()
|
||||
}) {
|
||||
Text("Continue", comment: "Button to dismiss suggested users view and continue to the main app")
|
||||
Text(NSLocalizedString("Continue", comment: "Button to dismiss suggested users view and continue to the main app"))
|
||||
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
|
||||
+11
-54
@@ -92,24 +92,13 @@ struct PostView: View {
|
||||
}
|
||||
|
||||
func send_post() {
|
||||
// don't add duplicate pubkeys but retain order
|
||||
var pkset = Set<Pubkey>()
|
||||
|
||||
// we only want pubkeys really
|
||||
let pks = references.reduce(into: Array<Pubkey>()) { acc, ref in
|
||||
guard case .pubkey(let pk) = ref else {
|
||||
return
|
||||
let refs = references.filter { ref in
|
||||
if case .pubkey(let pk) = ref, filtered_pubkeys.contains(pk) {
|
||||
return false
|
||||
}
|
||||
|
||||
if pkset.contains(pk) || filtered_pubkeys.contains(pk) {
|
||||
return
|
||||
}
|
||||
|
||||
pkset.insert(pk)
|
||||
acc.append(pk)
|
||||
return true
|
||||
}
|
||||
|
||||
let new_post = build_post(state: damus_state, post: self.post, action: action, uploadedMedias: uploadedMedias, pubkeys: pks)
|
||||
let new_post = build_post(state: damus_state, post: self.post, action: action, uploadedMedias: uploadedMedias, references: refs)
|
||||
|
||||
notify(.post(.post(new_post)))
|
||||
|
||||
@@ -279,7 +268,7 @@ struct PostView: View {
|
||||
Button(action: {
|
||||
self.cancel()
|
||||
}, label: {
|
||||
Text("Cancel", comment: "Button to cancel out of posting a note.")
|
||||
Text(NSLocalizedString("Cancel", comment: "Button to cancel out of posting a note."))
|
||||
.padding(10)
|
||||
})
|
||||
.buttonStyle(NeutralButtonStyle())
|
||||
@@ -615,24 +604,7 @@ private func isAlphanumeric(_ char: Character) -> Bool {
|
||||
return char.isLetter || char.isNumber
|
||||
}
|
||||
|
||||
func nip10_reply_tags(replying_to: NostrEvent, keypair: Keypair) -> [[String]] {
|
||||
guard let nip10 = replying_to.thread_reply() else {
|
||||
// we're replying to a post that isn't in a thread,
|
||||
// just add a single reply-to-root tag
|
||||
return [["e", replying_to.id.hex(), "", "root"]]
|
||||
}
|
||||
|
||||
// otherwise use the root tag from the parent's nip10 reply and include the note
|
||||
// that we are replying to's note id.
|
||||
let tags = [
|
||||
["e", nip10.root.note_id.hex(), nip10.root.relay ?? "", "root"],
|
||||
["e", replying_to.id.hex(), "", "reply"]
|
||||
]
|
||||
|
||||
return tags
|
||||
}
|
||||
|
||||
func build_post(state: DamusState, post: NSMutableAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], pubkeys: [Pubkey]) -> NostrPost {
|
||||
func build_post(state: DamusState, post: NSMutableAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], references: [RefId]) -> NostrPost {
|
||||
post.enumerateAttributes(in: NSRange(location: 0, length: post.length), options: []) { attributes, range, stop in
|
||||
if let link = attributes[.link] as? String {
|
||||
let nextCharIndex = range.upperBound
|
||||
@@ -662,35 +634,20 @@ func build_post(state: DamusState, post: NSMutableAttributedString, action: Post
|
||||
|
||||
let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: " ")
|
||||
|
||||
var tags = uploadedMedias.compactMap { $0.metadata?.to_tag() }
|
||||
|
||||
if !imagesString.isEmpty {
|
||||
content.append(" " + imagesString + " ")
|
||||
}
|
||||
|
||||
var tags: [[String]] = []
|
||||
|
||||
switch action {
|
||||
case .replying_to(let replying_to):
|
||||
// start off with the reply tags
|
||||
tags = nip10_reply_tags(replying_to: replying_to, keypair: state.keypair)
|
||||
|
||||
case .quoting(let ev):
|
||||
if case .quoting(let ev) = action {
|
||||
content.append(" nostr:" + bech32_note_id(ev.id))
|
||||
|
||||
if let quoted_ev = state.events.lookup(ev.id) {
|
||||
tags.append(["p", quoted_ev.pubkey.hex()])
|
||||
}
|
||||
case .posting(let postTarget):
|
||||
break
|
||||
}
|
||||
|
||||
// include pubkeys
|
||||
tags += pubkeys.map { pk in
|
||||
["p", pk.hex()]
|
||||
}
|
||||
|
||||
// append additional tags
|
||||
tags += uploadedMedias.compactMap { $0.metadata?.to_tag() }
|
||||
|
||||
return NostrPost(content: content, kind: .text, tags: tags)
|
||||
return NostrPost(content: content, references: references, kind: .text, tags: tags)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,17 @@ struct UserSearch: View {
|
||||
|
||||
var users: [Pubkey] {
|
||||
guard let txn = NdbTxn(ndb: damus_state.ndb) else { return [] }
|
||||
return search_profiles(profiles: damus_state.profiles, contacts: damus_state.contacts, search: search, txn: txn)
|
||||
return search_profiles(profiles: damus_state.profiles, search: search, txn: txn).sorted { a, b in
|
||||
let aFriendTypePriority = get_friend_type(contacts: damus_state.contacts, pubkey: a)?.priority ?? 0
|
||||
let bFriendTypePriority = get_friend_type(contacts: damus_state.contacts, pubkey: b)?.priority ?? 0
|
||||
|
||||
if aFriendTypePriority > bFriendTypePriority {
|
||||
// `a` should be sorted before `b`
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func on_user_tapped(pk: Pubkey) {
|
||||
|
||||
@@ -26,7 +26,7 @@ struct AboutView: View {
|
||||
Group {
|
||||
if let about_string {
|
||||
let truncated_about = show_full_about ? about_string : about_string.truncateOrNil(maxLength: max_about_length)
|
||||
SelectableText(damus_state: state, event: nil, attributedString: truncated_about ?? about_string, textAlignment: self.text_alignment, size: .subheadline)
|
||||
SelectableText(attributedString: truncated_about ?? about_string, textAlignment: self.text_alignment, size: .subheadline)
|
||||
|
||||
if truncated_about != nil {
|
||||
if show_full_about {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user