Compare commits

..

1 Commits

Author SHA1 Message Date
tyiu 5ff352361c Fix reaction events to not tag all e and p tags in the thread
Changelog-Fixed: Fix reaction events to not tag all e and p tags in the thread
2024-06-05 18:41:51 -04:00
103 changed files with 1330 additions and 3654 deletions
-76
View File
@@ -1,79 +1,3 @@
## [1.9 (14)] - 2024-07-14
### Added
- Completely new threads experience that is easier and more pleasant to use (Daniel DAquino)
- Add emoji search to emoji picker (Terry Yiu)
### Changed
- Added first aid contact damus support email (alltheseas)
- Disable mutiny wallet button (William Casarin)
- Make friends show up first when searching for profiles (Terry Yiu)
### Fixed
- Fix crash on profile page when there are profile updates (William Casarin)
- Fix crash when adding duplicate mute items (William Casarin)
- Fix pretty bad crash when building flatbuffer profiles (William Casarin)
- Fix reactions view to not show reactions from replies on parent note (Terry Yiu)
- Fix missing Mute button in profile view menu (Terry Yiu)
- Fixed wallet not disconnecting when a user logs out (ericholguin)
- Fix stale feed issue when follow list is too big (Daniel DAquino)
[1.9 (14)]: https://github.com/damus-io/damus/releases/tag/v1.9-14
## [1.8] - 2024-05-11
### Added
- Added nip10 marker replies (William Casarin)
- Add marker nip10 support when reading notes (William Casarin)
- Added title image and tags to longform events (ericholguin)
- Add First Aid solution for users who do not have a contact list created for their account (Daniel DAquino)
- Relay fees metadata (ericholguin)
- Added callbackuri for a better ux when connecting mutiny wallet nwc (ericholguin)
- Add event content preview to the full screen carousel (Daniel DAquino)
- Show list of quoted reposts in threads (William Casarin)
- Proxy Tags are now viewable on Selected Events (ericholguin)
- Connect to Mutiny Wallet Button (ericholguin)
- Add ability to mute words, add new mutelist interface (Charlie) (William Casarin)
- Add ability to mute hashtag from SearchView (Charlie Fish)
### Changed
- Change reactions to use a native looking emoji picker (Terry Yiu)
- Relay detail design (ericholguin)
- Updated Zeus logo (ericholguin)
- Improve UX around video playback (Daniel DAquino)
- Moved paste nwc button to main wallet view (ericholguin)
- Errors with an NWC will show as an alert (ericholguin)
- Relay config view user interface (ericholguin)
- Always strip GPS data from images (kernelkind)
### Fixed
- Fix thread bug where a quote isn't picked up as a reply (William Casarin)
- Fixed threads not loading sometimes (William Casarin)
- Fixed issue where some replies were including the q tag (William Casarin)
- Fixed issue where timeline was scrolling when it isn't supposed to (William Casarin)
- Fix issue where bootstrap relays would inadvertently be added to the user's list on connectivity issues (Daniel DAquino)
- Fix broken GIF uploads (Daniel DAquino)
- Fix ghost notifications caused by Purple impending expiration notifications (Daniel DAquino)
- Improve reliability of contact list creation during onboarding (Daniel DAquino)
- Fix emoji reactions being cut off (ericholguin)
- Fix image indicators to limit number of dots to not spill screen beyond visible margins (ericholguin)
- Fix bug that would cause connection issues with relays defined with a trailing slash URL, and an inability to delete them. (Daniel DAquino)
- Issue where NWC Scanner view would not dismiss after a failed scan/paste (ericholguin)
[1.8]: https://github.com/damus-io/damus/releases/tag/v1.8
## [1.7-rc2] - 2024-02-28
### Added
+26 -95
View File
@@ -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 */; };
@@ -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,12 +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 */; };
5C8711DE2C460C06007879C2 /* PostingTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C8711DD2C460C06007879C2 /* PostingTimelineView.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 */; };
@@ -504,9 +498,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 */; };
@@ -762,6 +753,8 @@
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>"; };
@@ -775,6 +768,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>"; };
@@ -835,9 +830,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>"; };
@@ -1341,7 +1333,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>"; };
@@ -1349,12 +1340,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>"; };
5C8711DD2C460C06007879C2 /* PostingTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostingTimelineView.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>"; };
@@ -1433,8 +1418,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>"; };
@@ -1499,10 +1482,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;
};
@@ -1666,6 +1648,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 */,
@@ -1682,7 +1666,6 @@
B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */,
B533694D2B66D791008A805E /* MutelistManager.swift */,
D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */,
5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -2023,7 +2006,6 @@
4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup;
children = (
D78DB85D2C20FE9E00F0AB12 /* Chat */,
D71AC4CA2BA8E3320076268E /* Extensions */,
BA3759952ABCCF360018D73B /* Camera */,
F71694E82A66221E001F4053 /* Onboarding */,
@@ -2417,7 +2399,6 @@
4CC7AAEE297F11B300430951 /* Events */ = {
isa = PBXGroup;
children = (
5CC852A02BDED9970039FFC5 /* Highlight */,
4CA927682A290F8F0098A105 /* Components */,
4CC7AAEF297F11C700430951 /* SelectedEventView.swift */,
4CC7AAF5297F1A6A00430951 /* EventBody.swift */,
@@ -2449,7 +2430,6 @@
isa = PBXGroup;
children = (
4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */,
5C8711DD2C460C06007879C2 /* PostingTimelineView.swift */,
);
path = Timeline;
sourceTree = "<group>";
@@ -2579,6 +2559,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 */,
@@ -2716,25 +2698,12 @@
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>";
@@ -2803,17 +2772,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 = (
@@ -2895,8 +2853,7 @@
4C649880286E0EE300EAE2B3 /* secp256k1 */,
4C06670328FC7EC500038D2A /* Kingfisher */,
4C27C9312A64766F007DBC75 /* MarkdownUI */,
3A0A30BA2C21397A00F8C9BC /* EmojiPicker */,
D78DB8582C1CE9CA00F0AB12 /* SwipeActions */,
3AFE89C22BD4156F00AD31EF /* MCEmojiPicker */,
);
productName = damus;
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
@@ -3036,8 +2993,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 = "";
@@ -3178,12 +3134,10 @@
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */,
4CC14FF12A73FCDB007AEB17 /* Pubkey.swift in Sources */,
5C8711DE2C460C06007879C2 /* PostingTimelineView.swift in Sources */,
4CA9275D2A28FF630098A105 /* LongformView.swift in Sources */,
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 */,
@@ -3214,7 +3168,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 */,
@@ -3231,6 +3184,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 */,
@@ -3294,7 +3248,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 */,
@@ -3303,7 +3256,6 @@
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 */,
@@ -3354,7 +3306,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 */,
@@ -3393,14 +3344,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 */,
@@ -3455,8 +3403,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 */,
@@ -3512,7 +3460,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 */,
@@ -3566,7 +3513,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 */,
@@ -3603,6 +3549,7 @@
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 */,
@@ -3632,6 +3579,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 */,
@@ -4073,7 +4021,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;
@@ -4100,7 +4047,6 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.11;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -4124,7 +4070,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;
@@ -4151,7 +4096,6 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.11;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -4356,12 +4300,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" */ = {
@@ -4396,14 +4340,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";
@@ -4415,10 +4351,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;
@@ -4440,11 +4376,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,5 @@
{
"originHash" : "babaf4d5748afecf49bbb702530d8e9576460692f478b0a50ee43195dd4440e2",
"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 +18,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 +35,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,25 +60,7 @@
"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
"version" : 2
}
@@ -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
}
}
-12
View File
@@ -1,12 +0,0 @@
{
"images" : [
{
"filename" : "damoose.jpeg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

-12
View File
@@ -1,12 +0,0 @@
{
"images" : [
{
"filename" : "tor.svg.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

+13 -7
View File
@@ -12,25 +12,31 @@ let RECTANGLE_GRADIENT = LinearGradient(gradient: Gradient(colors: [
DamusColors.blue
]), startPoint: .leading, endPoint: .trailing)
struct CustomPicker<SelectionValue: Hashable>: View {
struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
let tabs: [(String, SelectionValue)]
@Environment(\.colorScheme) var colorScheme
@Namespace var picker
@Binding var selection: SelectionValue
@ViewBuilder let content: Content
public var body: some View {
let contentMirror = Mirror(reflecting: content)
let blocksCount = Mirror(reflecting: contentMirror.descendant("value")!).children.count
HStack {
ForEach(tabs, id: \.1) { (text, tag) in
ForEach(0..<blocksCount, id: \.self) { index in
let tupleBlock = contentMirror.descendant("value", ".\(index)")
let text = Mirror(reflecting: tupleBlock!).descendant("content") as! Text
let tag = Mirror(reflecting: tupleBlock!).descendant("modifier", "value", "tagged") as! SelectionValue
Button {
withAnimation(.spring()) {
selection = tag
}
} label: {
Text(text).padding(EdgeInsets(top: 15, leading: 0, bottom: 10, trailing: 0))
text
.padding(EdgeInsets(top: 15, leading: 0, bottom: 10, trailing: 0))
.font(.system(size: 14, weight: .heavy))
.tag(tag)
}
.background(
Group {
-6
View File
@@ -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")
+9 -85
View File
@@ -9,21 +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 showMutePost = 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
@@ -37,10 +32,6 @@ struct SelectableText: View {
font: eventviewsize_to_uifont(size),
fixedWidth: selectedTextWidth,
textAlignment: self.textAlignment,
enableHighlighting: self.enableHighlighting(),
showHighlightPost: $showHighlightPost,
showMutePost: $showMutePost,
selectedText: $selectedText,
height: $selectedTextHeight
)
.padding([.leading, .trailing], -1.0)
@@ -55,66 +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])
}
}
.sheet(isPresented: $showMutePost) {
AddMuteItemView(state: damus_state, new_text: $selectedText)
.presentationDragIndicator(.visible)
.presentationDetents([.height(300), .medium, .large])
}
.frame(height: selectedTextHeight)
}
func enableHighlighting() -> Bool {
self.event != nil
}
}
fileprivate class TextView: UITextView {
@Binding var showHighlightPost: Bool
@Binding var showMutePost: Bool
@Binding var selectedText: String
init(frame: CGRect, textContainer: NSTextContainer?, showHighlightPost: Binding<Bool>, showMutePost: Binding<Bool>, selectedText: Binding<String>) {
self._showHighlightPost = showHighlightPost
self._showMutePost = showMutePost
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
}
if action == #selector(muteText(_:)) {
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()
}
@objc public func muteText(_ sender: Any?) {
guard let selectedRange = self.selectedTextRange else { return }
selectedText = self.text(in: selectedRange) ?? ""
showMutePost.toggle()
}
}
fileprivate struct TextViewRepresentable: UIViewRepresentable {
@@ -124,14 +57,11 @@ fileprivate class TextView: UITextView {
let font: UIFont
let fixedWidth: CGFloat
let textAlignment: NSTextAlignment
let enableHighlighting: Bool
@Binding var showHighlightPost: Bool
@Binding var showMutePost: Bool
@Binding var selectedText: String
@Binding var height: CGFloat
func makeUIView(context: UIViewRepresentableContext<Self>) -> TextView {
let view = TextView(frame: .zero, textContainer: nil, showHighlightPost: $showHighlightPost, showMutePost: $showMutePost, selectedText: $selectedText)
func makeUIView(context: UIViewRepresentableContext<Self>) -> UITextView {
let view = UITextView()
view.isEditable = false
view.dataDetectorTypes = .all
view.isSelectable = true
@@ -141,16 +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(_:)))
let muteItem = UIMenuItem(title: "Mute", action: #selector(view.muteText(_:)))
menuController.menuItems = self.enableHighlighting ? [highlightItem, muteItem] : []
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
+2 -2
View File
@@ -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))
+5 -9
View File
@@ -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)
}
}
+58 -4
View File
@@ -8,7 +8,6 @@
import SwiftUI
import AVKit
import MediaPlayer
import EmojiPicker
struct ZapSheet {
let target: ZapTarget
@@ -79,15 +78,71 @@ struct ContentView: View {
@State var hide_bar: Bool = false
@State var user_muted_confirm: Bool = false
@State var confirm_overwrite_mutelist: Bool = false
@SceneStorage("ContentView.filter_state") var filter_state : FilterState = .posts_and_replies
@State private var isSideBarOpened = false
var home: HomeModel = HomeModel()
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
@AppStorage("has_seen_suggested_users") private var hasSeenOnboardingSuggestions = false
let sub_id = UUID().description
@Environment(\.colorScheme) var colorScheme
// connect retry timer
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var mystery: some View {
Text("Are you lost?", comment: "Text asking the user if they are lost in the app.")
.id("what")
}
func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
var filters = ContentFilters.defaults(damus_state: damus_state!)
filters.append(fstate.filter)
return ContentFilters(filters: filters).filter
}
var PostingTimelineView: some View {
VStack {
ZStack {
TabView(selection: $filter_state) {
// This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why.
mystery
contentTimelineView(filter: content_filter(.posts))
.tag(FilterState.posts)
.id(FilterState.posts)
contentTimelineView(filter: content_filter(.posts_and_replies))
.tag(FilterState.posts_and_replies)
.id(FilterState.posts_and_replies)
}
.tabViewStyle(.page(indexDisplayMode: .never))
if privkey != nil {
PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) {
self.active_sheet = .post(.posting(.none))
}
}
}
}
.safeAreaInset(edge: .top, spacing: 0) {
VStack(spacing: 0) {
CustomPicker(selection: $filter_state, content: {
Text("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies).").tag(FilterState.posts)
Text("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes).").tag(FilterState.posts_and_replies)
})
Divider()
.frame(height: 1)
}
.background(colorScheme == .dark ? Color.black : Color.white)
}
}
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
TimelineView(events: home.events, loading: .constant(false), damus: damus_state, show_friend_icon: false, filter: filter) {
PullDownSearchView(state: damus_state, on_cancel: {})
}
}
func navIsAtRoot() -> Bool {
return navigationCoordinator.isAtRoot()
}
@@ -115,7 +170,7 @@ struct ContentView: View {
}
case .home:
PostingTimelineView(damus_state: damus_state!, home: home, active_sheet: $active_sheet)
PostingTimelineView
case .notifications:
NotificationsView(state: damus, notifications: home.notifications)
@@ -664,8 +719,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!
+1 -1
View File
@@ -16,7 +16,7 @@ enum FilterState : Int {
func filter(ev: NostrEvent) -> Bool {
switch self {
case .posts:
return ev.known_kind == .boost || ev.known_kind == .highlight || !ev.is_reply()
return ev.known_kind == .boost || !ev.is_reply()
case .posts_and_replies:
return true
}
+8 -8
View File
@@ -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
}
}
+2 -7
View File
@@ -7,7 +7,6 @@
import Foundation
import LinkPresentation
import EmojiPicker
class DamusState: HeadlessDamusState {
let pool: RelayPool
@@ -38,9 +37,8 @@ class DamusState: HeadlessDamusState {
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
@@ -72,7 +70,6 @@ class DamusState: HeadlessDamusState {
)
self.quote_reposts = quote_reposts
self.push_notification_client = PushNotificationClient(keypair: keypair, settings: settings)
self.emoji_provider = emoji_provider
}
@discardableResult
@@ -102,7 +99,6 @@ class DamusState: HeadlessDamusState {
func close() {
print("txn: damus close")
wallet.disconnect()
pool.close()
ndb.close()
}
@@ -138,8 +134,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)
)
}
}
-34
View File
@@ -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
}
}
+2 -2
View File
@@ -184,7 +184,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)
@@ -586,7 +586,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)
-4
View File
@@ -111,16 +111,12 @@ class MutelistManager {
private func add_mute_item(_ item: MuteItem) {
switch item {
case .user(_, _):
guard !users.contains(item) else { return }
users.insert(item)
case .hashtag(_, _):
guard !hashtags.contains(item) else { return }
hashtags.insert(item)
case .word(_, _):
guard !words.contains(item) else { return }
words.insert(item)
case .thread(_, _):
guard !threads.contains(item) else { return }
threads.insert(item)
}
}
+3 -3
View File
@@ -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]
+22 -196
View File
@@ -11,10 +11,6 @@ struct PushNotificationClient {
let keypair: Keypair
let settings: UserSettingsStore
private(set) var device_token: Data? = nil
var device_token_hex: String? {
guard let device_token else { return nil }
return device_token.map { String(format: "%02.2hhx", $0) }.joined()
}
mutating func set_device_token(new_device_token: Data) async throws {
self.device_token = new_device_token
@@ -24,21 +20,26 @@ struct PushNotificationClient {
}
func send_token() async throws {
guard let device_token else { return }
// Send the device token and pubkey to the server
guard let token = device_token_hex else { return }
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.current_push_notification_environment().api_base_url()
.appendingPathComponent("user-info")
.appendingPathComponent(self.keypair.pubkey.hex())
.appendingPathComponent(token)
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: .put,
method: .post,
url: url,
payload: nil,
payload: json_data,
payload_type: .json,
auth_keypair: self.keypair
)
@@ -57,23 +58,26 @@ struct PushNotificationClient {
}
func revoke_token() async throws {
guard let token = device_token_hex else { return }
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.current_push_notification_environment().api_base_url()
.appendingPathComponent("user-info")
.appendingPathComponent(pubkey.hex())
.appendingPathComponent(token)
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: .delete,
method: .post,
url: url,
payload: nil,
payload: json_data,
payload_type: .json,
auth_keypair: self.keypair
)
@@ -90,78 +94,6 @@ struct PushNotificationClient {
return
}
func set_settings(_ new_settings: NotificationSettings? = nil) async throws {
// Send the device token and pubkey to the server
guard let token = device_token_hex else { return }
Log.info("Sending notification preferences to the server", for: .push_notifications)
let url = self.current_push_notification_environment().api_base_url()
.appendingPathComponent("user-info")
.appendingPathComponent(self.keypair.pubkey.hex())
.appendingPathComponent(token)
.appendingPathComponent("preferences")
let json_payload = try JSONEncoder().encode(new_settings ?? NotificationSettings.from(settings: settings))
let (data, response) = try await make_nip98_authenticated_request(
method: .put,
url: url,
payload: json_payload,
payload_type: .json,
auth_keypair: self.keypair
)
if let httpResponse = response as? HTTPURLResponse {
switch httpResponse.statusCode {
case 200:
Log.info("Sent notification settings to Damus push notification server successfully", for: .push_notifications)
default:
Log.error("Error in sending notification settings 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 get_settings() async throws -> NotificationSettings {
// Send the device token and pubkey to the server
guard let token = device_token_hex else {
throw ClientError.no_device_token
}
let url = self.current_push_notification_environment().api_base_url()
.appendingPathComponent("user-info")
.appendingPathComponent(self.keypair.pubkey.hex())
.appendingPathComponent(token)
.appendingPathComponent("preferences")
let (data, response) = try await make_nip98_authenticated_request(
method: .get,
url: url,
payload: nil,
payload_type: .json,
auth_keypair: self.keypair
)
if let httpResponse = response as? HTTPURLResponse {
switch httpResponse.statusCode {
case 200:
guard let notification_settings = NotificationSettings.from(json_data: data) else { throw ClientError.json_decoding_error }
return notification_settings
default:
Log.error("Error in getting notification settings 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)
}
}
throw ClientError.could_not_process_response
}
func current_push_notification_environment() -> Environment {
return self.settings.send_device_token_to_localhost ? .local_test(host: nil) : .production
}
}
// MARK: Helper structures
@@ -169,111 +101,5 @@ struct PushNotificationClient {
extension PushNotificationClient {
enum ClientError: Error {
case http_response_error(status_code: Int, response: Data)
case could_not_process_response
case no_device_token
case json_decoding_error
}
struct NotificationSettings: Codable, Equatable {
let zap_notifications_enabled: Bool
let mention_notifications_enabled: Bool
let repost_notifications_enabled: Bool
let reaction_notifications_enabled: Bool
let dm_notifications_enabled: Bool
let only_notifications_from_following_enabled: Bool
static func from(json_data: Data) -> Self? {
guard let decoded = try? JSONDecoder().decode(Self.self, from: json_data) else { return nil }
return decoded
}
static func from(settings: UserSettingsStore) -> Self {
return NotificationSettings(
zap_notifications_enabled: settings.zap_notification,
mention_notifications_enabled: settings.mention_notification,
repost_notifications_enabled: settings.repost_notification,
reaction_notifications_enabled: settings.like_notification,
dm_notifications_enabled: settings.dm_notification,
only_notifications_from_following_enabled: settings.notification_only_from_following
)
}
}
enum Environment: CaseIterable, Codable, Identifiable, StringCodable, Equatable, Hashable {
static var allCases: [Environment] = [.local_test(host: nil), .production]
case local_test(host: String?)
case production
func text_description() -> String {
switch self {
case .local_test:
return NSLocalizedString("Test (local)", comment: "Label indicating a local test environment for Push notification functionality (Developer feature)")
case .production:
return NSLocalizedString("Production", comment: "Label indicating the production environment for Push notification functionality")
}
}
func api_base_url() -> URL {
switch self {
case .local_test(let host):
URL(string: "http://\(host ?? "localhost:8000")") ?? Constants.PUSH_NOTIFICATION_SERVER_TEST_BASE_URL
case .production:
Constants.PURPLE_API_PRODUCTION_BASE_URL
}
}
func custom_host() -> String? {
switch self {
case .local_test(let host):
return host
default:
return nil
}
}
init?(from string: String) {
switch string {
case "local_test":
self = .local_test(host: nil)
case "production":
self = .production
default:
let components = string.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false)
if components.count == 2 && components[0] == "local_test" {
self = .local_test(host: String(components[1]))
} else {
return nil
}
}
}
func to_string() -> String {
switch self {
case .local_test(let host):
if let host {
return "local_test:\(host)"
}
return "local_test"
case .production:
return "production"
}
}
var id: String {
switch self {
case .local_test(let host):
if let host {
return "local_test:\(host)"
}
else {
return "local_test"
}
case .production:
return "production"
}
}
}
}
+1 -1
View File
@@ -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!
+2 -10
View File
@@ -11,24 +11,16 @@ import Foundation
class ThreadModel: ObservableObject {
@Published var event: NostrEvent
let original_event: NostrEvent
let highlight: String?
var event_map: Set<NostrEvent>
init(event: NostrEvent, damus_state: DamusState, highlight: String? = nil) {
init(event: NostrEvent, damus_state: DamusState) {
self.damus_state = damus_state
self.event_map = Set()
self.event = event
self.original_event = event
self.highlight = highlight
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
}
+129
View File
@@ -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)
}
}
}
}
}
}
+116
View File
@@ -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)
}
}
}
+7 -6
View File
@@ -443,16 +443,17 @@ func make_boost_event(keypair: FullKeypair, boosted: NostrEvent) -> NostrEvent?
}
func make_like_event(keypair: FullKeypair, liked: NostrEvent, content: String = "🤙") -> NostrEvent? {
var tags = liked.tags.reduce(into: [[String]]()) { ts, tag in
guard tag.count >= 2,
(tag[0].matches_char("e") || tag[0].matches_char("p")) else {
return
}
ts.append(tag.strings())
var tags: [[String]] = []
if liked.is_non_parameterized_replaceable {
tags.append(["a", "\(liked.kind.description):\(liked.pubkey.hex()):"])
} else if liked.is_parameterized_replaceable, let dTag = liked.tags.first(where: { $0.count >= 2 && $0[0].matches_char("d") }) {
tags.append(["a", "\(liked.kind.description):\(liked.pubkey.hex()):\(dTag[1])"])
}
tags.append(["e", liked.id.hex()])
tags.append(["p", liked.pubkey.hex()])
tags.append(["k", liked.kind.description])
return NostrEvent(content: content, keypair: keypair.to_keypair(), kind: 7, tags: tags)
}
-1
View File
@@ -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
+1 -26
View File
File diff suppressed because one or more lines are too long
+4 -4
View File
@@ -10,13 +10,13 @@ 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: "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: "https://notify.damus.io: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"
// MARK: Push notification server
static let PUSH_NOTIFICATION_SERVER_PRODUCTION_BASE_URL: URL = URL(string: "http://45.33.32.5:8000")!
static let PUSH_NOTIFICATION_SERVER_TEST_BASE_URL: URL = URL(string: "http://localhost:8000")!
// MARK: Purple
// API
static let PURPLE_API_LOCAL_TEST_BASE_URL: URL = URL(string: "http://localhost:8989")!
+3 -3
View File
@@ -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(
@@ -187,7 +187,7 @@ class EventCache {
replies.add(id: reply, reply_id: ev.id)
}
}
func child_events(event: NostrEvent) -> [NostrEvent] {
guard let xs = replies.lookup(event.id) else {
return []
-27
View File
@@ -1,27 +0,0 @@
//
// VectorMath.swift
// damus
//
// Created by Daniel DAquino 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)
}
}
+3 -5
View File
@@ -60,8 +60,7 @@ struct ImageMetadata: Equatable {
func process_blurhash(blurhash: String, size: CGSize?) async -> UIImage? {
let res = Task.detached(priority: .low) {
let default_size = CGSize(width: 100.0, height: 100.0)
let size = get_blurhash_size(img_size: size ?? default_size) ?? default_size
let size = get_blurhash_size(img_size: size ?? CGSize(width: 100.0, height: 100.0))
guard let img = UIImage.init(blurHash: blurhash, size: size) else {
let noimg: UIImage? = nil
return noimg
@@ -136,8 +135,7 @@ extension UIImage {
}
}
func get_blurhash_size(img_size: CGSize) -> CGSize? {
guard img_size.width > 0 && img_size.height > 0 else { return nil }
func get_blurhash_size(img_size: CGSize) -> CGSize {
return CGSize(width: 100.0, height: (100.0/img_size.width) * img_size.height)
}
@@ -147,7 +145,7 @@ func calculate_blurhash(img: UIImage) async -> String? {
}
let res = Task.detached(priority: .low) {
let bhs = get_blurhash_size(img_size: img.size) ?? CGSize(width: 100.0, height: 100.0)
let bhs = get_blurhash_size(img_size: img.size)
let smaller = img.resized(to: bhs)
guard let blurhash = smaller.blurHash(numberOfComponents: (5,5)) else {
+2 -3
View File
@@ -85,7 +85,7 @@ enum Route: Hashable {
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):
@@ -93,8 +93,7 @@ enum Route: Hashable {
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):
+75 -230
View File
@@ -6,34 +6,28 @@
//
import SwiftUI
import EmojiPicker
import EmojiKit
import SwipeActions
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 +44,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 +136,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 +164,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 +183,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 +192,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 +231,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 +245,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 +299,7 @@ struct LikeButton: View {
}
}
struct EventActionBar_Previews: PreviewProvider {
static var previews: some View {
let ds = test_damus_state
@@ -442,44 +324,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)
}
}
}
+2 -2
View File
@@ -13,7 +13,7 @@ struct EditBannerImageView: View {
var damus_state: DamusState
@ObservedObject var viewModel: ImageUploadingObserver
let callback: (URL?) -> Void
let defaultImage = UIImage(named: "damoose") ?? UIImage()
let defaultImage = UIImage(named: "profile-banner") ?? UIImage()
@State var banner_image: URL? = nil
@@ -38,7 +38,7 @@ struct EditBannerImageView: View {
struct InnerBannerImageView: View {
let disable_animation: Bool
let url: URL?
let defaultImage = UIImage(named: "damoose") ?? UIImage()
let defaultImage = UIImage(named: "profile-banner") ?? UIImage()
var body: some View {
ZStack {
-184
View File
@@ -1,184 +0,0 @@
//
// ChatBubbleView.swift
// damus
//
// Created by Daniel DAquino 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(verbatim: "Hello there")
.padding()
}
.foregroundColor(.white)
ChatBubble(
direction: .right,
stroke_content: Color.accentColor.opacity(0),
stroke_style: .init(lineWidth: 4),
background_style: Color.accentColor
) {
Text(verbatim: "Hello there")
.padding()
}
.foregroundColor(.white)
}
}
-374
View File
@@ -1,374 +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.some_sheet_open() ? .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
case open_zap_sheet
func some_sheet_open() -> Bool {
return self == .open_zap_sheet || self == .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 }
// MARK: Zapping properties
var lnurl: String? {
damus_state.profiles.lookup_with_timestamp(event.pubkey)?.map({ pr in
pr?.lnurl
}).value
}
var zap_target: ZapTarget {
ZapTarget.note(id: event.id, author: event.pubkey)
}
// MARK: Views
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)
}
.lineLimit(1)
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: [.truncate_content])
.padding(2)
if let mention = first_eref_mention(ev: event, keypair: damus_state.keypair) {
MentionView(damus_state: damus_state, mention: mention)
.background(DamusColors.adaptableWhite)
.clipShape(RoundedRectangle(cornerSize: CGSize(width: 10, height: 10)))
}
}
.frame(minWidth: 5, alignment: is_ours ? .trailing : .leading)
.padding(10)
}
.tint(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])
}
.sheet(isPresented: Binding(get: { popover_state == .open_zap_sheet }, set: { new_state in
withAnimation(new_state == true ? .easeIn(duration: 0.5) : .easeOut(duration: 0.1)) {
popover_state = new_state == true ? .open_zap_sheet : .closed
}
})) {
ZapSheetViewIfPossible(damus_state: damus_state, target: zap_target, lnurl: lnurl)
.presentationDetents([.medium, .large])
}
.onChange(of: selected_emoji) { newSelectedEmoji in
if let newSelectedEmoji {
send_like(emoji: newSelectedEmoji.value)
popover_state = .closed
}
}
}
.scaleEffect(self.popover_state.some_sheet_open() ? 1.08 : is_pressing ? 1.02 : 1)
.shadow(color: (is_pressing || self.popover_state.some_sheet_open()) ? .black.opacity(0.1) : .black.opacity(0.3), radius: (is_pressing || self.popover_state.some_sheet_open()) ? 8 : 0, y: (is_pressing || self.popover_state.some_sheet_open()) ? 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)) {
let should_show_zap_sheet = !damus_state.settings.nozaps && damus_state.settings.onlyzaps_mode
popover_state = should_show_zap_sheet ? .open_zap_sheet : .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
if !is_ours {
EventActionBar(
damus_state: damus_state,
event: event,
bar: bar,
options: is_ours ? [.swipe_action_menu_reverse] : [.swipe_action_menu],
swipe_context: context
)
}
} trailingActions: { context in
if is_ours {
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)
}
#Preview {
let bar = make_actionbar_model(ev: test_note.id, damus: test_damus_state)
return ChatEventView(event: test_super_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)
}
-195
View File
@@ -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)
}
-70
View File
@@ -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])
}
}
@@ -182,6 +182,25 @@ extension CodeScannerView {
delegate?.didFail(reason: .badOutput)
return
}
}
override public func viewWillLayoutSubviews() {
previewLayer?.frame = view.layer.bounds
}
@objc func updateOrientation() {
guard let orientation = view.window?.windowScene?.interfaceOrientation else { return }
guard let connection = captureSession.connections.last, connection.isVideoOrientationSupported else { return }
connection.videoOrientation = AVCaptureVideoOrientation(rawValue: orientation.rawValue) ?? .portrait
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updateOrientation()
}
override public func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if previewLayer == nil {
previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
@@ -201,21 +220,6 @@ extension CodeScannerView {
}
}
override public func viewWillLayoutSubviews() {
previewLayer?.frame = view.layer.bounds
}
@objc func updateOrientation() {
guard let orientation = view.window?.windowScene?.interfaceOrientation else { return }
guard let connection = captureSession.connections.last, connection.isVideoOrientationSupported else { return }
connection.videoOrientation = AVCaptureVideoOrientation(rawValue: orientation.rawValue) ?? .portrait
}
override public func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
updateOrientation()
}
private func addviewfinder() {
guard showViewfinder, let imageView = viewFinder else { return }
+56 -31
View File
@@ -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("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)
}
}
}
+7 -5
View File
@@ -72,11 +72,13 @@ struct DirectMessagesView: View {
var body: some View {
VStack(spacing: 0) {
CustomPicker(tabs: [
(NSLocalizedString("DMs", comment: "Picker option for DM selector for seeing only DMs that have been responded to. DM is the English abbreviation for Direct Message."), DMType.friend),
(NSLocalizedString("Requests", comment: "Picker option for DM selector for seeing only message requests (DMs that someone else sent the user which has not been responded to yet"), DMType.rando),
], selection: $dm_type)
CustomPicker(selection: $dm_type, content: {
Text("DMs", comment: "Picker option for DM selector for seeing only DMs that have been responded to. DM is the English abbreviation for Direct Message.")
.tag(DMType.friend)
Text("Requests", comment: "Picker option for DM selector for seeing only message requests (DMs that someone else sent the user which has not been responded to yet). DM is the English abbreviation for Direct Message.")
.tag(DMType.rando)
})
Divider()
.frame(height: 1)
-2
View File
@@ -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)
@@ -15,12 +15,10 @@ struct ReplyPart: View {
var body: some View {
Group {
if event.known_kind == .highlight {
let highlighted_note = event.highlighted_note_id().flatMap { events.lookup($0) }
HighlightDescription(event: event, highlighted_event: highlighted_note, ndb: ndb)
} else if let reply_ref = event.thread_reply()?.reply {
let replying_to = events.lookup(reply_ref.note_id)
ReplyDescription(event: event, replying_to: replying_to, ndb: ndb)
if let reply_ref = event.thread_reply()?.reply {
ReplyDescription(event: event, replying_to: events.lookup(reply_ref.note_id), ndb: ndb)
} else {
EmptyView()
}
}
}
-8
View File
@@ -35,14 +35,6 @@ struct EventBody: View {
if !options.contains(.truncate_content) {
note_content
}
} else if event.known_kind == .highlight {
HighlightBodyView(state: damus_state, ev: event, options: options)
.onTapGesture {
if let highlighted_note = event.highlighted_note_id().flatMap({ damus_state.events.lookup($0) }) {
let thread = ThreadModel(event: highlighted_note, damus_state: damus_state, highlight: event.content)
damus_state.nav.push(route: Route.Thread(thread: thread))
}
}
} else {
note_content
}
@@ -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,77 +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.")) {
let tags: [[String]] = [ ["e", "\(self.event.id)"] ]
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(selectedText)
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])
}
}
}
@@ -51,13 +51,13 @@ struct LongformPreviewBody: 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(header ? .body : .caption)
.foregroundColor(.gray)
.padding(.horizontal, 10)
}
else if truncate {
TruncatedText(text: content, show_show_more_button: !options.contains(.no_show_more))
TruncatedText(text: content)
.font(header ? .body : .caption)
.foregroundColor(.gray)
.padding(.horizontal, 10)
@@ -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)
}
@@ -36,6 +36,7 @@ let test_longform_event = LongformEvent.parse(from: NostrEvent(
keypair: test_keypair,
kind: NostrKind.longform.rawValue,
tags: [
["d", UUID().uuidString],
["title", "What is WASTOIDS?"],
["summary", "WASTOIDS is an audio/visual feed, created by Sam Means..."],
["published_at", "1685638715"],
+6 -4
View File
@@ -39,10 +39,12 @@ struct SelectedEventView: View {
.padding(.horizontal)
.minimumScaleFactor(0.75)
.lineLimit(1)
ReplyPart(events: damus.events, event: event, keypair: damus.keypair, ndb: damus.ndb)
.padding(.horizontal)
if let reply_ref = event.thread_reply()?.reply {
ReplyDescription(event: event, replying_to: damus.events.lookup(reply_ref.note_id), ndb: damus.ndb)
.padding(.horizontal)
}
ProxyView(event: event)
.padding(.top, 5)
.padding(.horizontal)
+1 -3
View File
@@ -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 {
+4 -4
View File
@@ -160,10 +160,10 @@ struct FollowingView: View {
.navigationBarTitle(NSLocalizedString("Following", comment: "Navigation bar title for view that shows who a user is following."))
.safeAreaInset(edge: .top, spacing: 0) {
VStack(spacing: 0) {
CustomPicker(tabs: [
(NSLocalizedString("People", comment: "Label for filter for seeing only people follows."), FollowingViewTabSelection.people),
(NSLocalizedString("Hashtags", comment: "Label for filter for seeing only hashtag follows."), FollowingViewTabSelection.hashtags)
], selection: $tab_selection)
CustomPicker(selection: $tab_selection, content: {
Text("People", comment: "Label for filter for seeing only people follows.").tag(FollowingViewTabSelection.people)
Text("Hashtags", comment: "Label for filter for seeing only hashtag follows.").tag(FollowingViewTabSelection.hashtags)
})
Divider()
.frame(height: 1)
}
+4 -14
View File
@@ -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"))
}
}
}
+2 -2
View File
@@ -8,7 +8,7 @@ import SwiftUI
struct AddMuteItemView: View {
let state: DamusState
@Binding var new_text: String
@State var new_text: String = ""
@State var expiration: DamusDuration = .indefinite
@Environment(\.dismiss) var dismiss
@@ -108,6 +108,6 @@ struct AddMuteItemView: View {
struct AddMuteItemView_Previews: PreviewProvider {
static var previews: some View {
AddMuteItemView(state: test_damus_state, new_text: .constant(""))
AddMuteItemView(state: test_damus_state)
}
}
+7 -5
View File
@@ -15,8 +15,6 @@ struct MutelistView: View {
@State var hashtags: [MuteItem] = []
@State var threads: [MuteItem] = []
@State var words: [MuteItem] = []
@State var new_text: String = ""
func RemoveAction(item: MuteItem) -> some View {
Button {
@@ -122,9 +120,13 @@ struct MutelistView: View {
}
}
.sheet(isPresented: $show_add_muteitem, onDismiss: { self.show_add_muteitem = false }) {
AddMuteItemView(state: damus_state, new_text: $new_text)
.presentationDetents([.height(300)])
.presentationDragIndicator(.visible)
if #available(iOS 16.0, *) {
AddMuteItemView(state: damus_state)
.presentationDetents([.height(300)])
.presentationDragIndicator(.visible)
} else {
AddMuteItemView(state: damus_state)
}
}
}
}
+9 -26
View File
@@ -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
@@ -390,12 +386,7 @@ struct NoteContentView_Previews: PreviewProvider {
NoteContentView(damus_state: state, event: test_note, blur_images: false, size: .normal, options: [])
}
.previewDisplayName("Short note")
VStack {
NoteContentView(damus_state: state, event: test_super_short_note, blur_images: true, size: .normal, options: [])
}
.previewDisplayName("Super short note")
VStack {
NoteContentView(damus_state: state, event: test_encoded_note_with_image!, blur_images: false, size: .normal, options: [])
}
@@ -406,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")
}
}
}
@@ -117,11 +117,17 @@ struct NotificationsView: View {
}
.safeAreaInset(edge: .top, spacing: 0) {
VStack(spacing: 0) {
CustomPicker(tabs: [
(NSLocalizedString("All", comment: "Label for filter for all notifications."), NotificationFilterState.all),
(NSLocalizedString("Zaps", comment: "Label for filter for zap notifications."), NotificationFilterState.zaps),
(NSLocalizedString("Mentions", comment: "Label for filter for seeing mention notifications (replies, etc)."), NotificationFilterState.replies),
], selection: $filter_state)
CustomPicker(selection: $filter_state, content: {
Text("All", comment: "Label for filter for all notifications.")
.tag(NotificationFilterState.all)
Text("Zaps", comment: "Label for filter for zap notifications.")
.tag(NotificationFilterState.zaps)
Text("Mentions", comment: "Label for filter for seeing mention notifications (replies, etc).")
.tag(NotificationFilterState.replies)
})
Divider()
.frame(height: 1)
}
+1 -1
View File
@@ -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 {
+31 -105
View File
@@ -21,15 +21,13 @@ struct EditMetadataView: View {
@State var ln: String
@State var website: String
@Environment(\.dismiss) var dismiss
@State var confirm_ln_address: Bool = false
@State var confirm_save_alert: Bool = false
@StateObject var profileUploadObserver = ImageUploadingObserver()
@StateObject var bannerUploadObserver = ImageUploadingObserver()
@Environment(\.dismiss) var dismiss
@Environment(\.presentationMode) var presentationMode
init(damus_state: DamusState) {
self.damus_state = damus_state
let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey)
@@ -79,7 +77,7 @@ struct EditMetadataView: View {
var TopSection: some View {
ZStack(alignment: .top) {
GeometryReader { geo in
EditBannerImageView(damus_state: damus_state, viewModel: bannerUploadObserver, callback: uploadedBanner(image_url:), banner_image: URL(string: banner))
EditBannerImageView(damus_state: damus_state, viewModel: bannerUploadObserver, callback: uploadedBanner(image_url:))
.aspectRatio(contentMode: .fill)
.frame(width: geo.size.width, height: BANNER_HEIGHT)
.clipped()
@@ -88,7 +86,7 @@ struct EditMetadataView: View {
let pfp_size: CGFloat = 90.0
HStack(alignment: .center) {
EditProfilePictureView(profile_url: URL(string: picture), pubkey: damus_state.pubkey, damus_state: damus_state, size: pfp_size, uploadObserver: profileUploadObserver, callback: uploadedProfilePicture(image_url:))
EditProfilePictureView(pubkey: damus_state.pubkey, damus_state: damus_state, size: pfp_size, uploadObserver: profileUploadObserver, callback: uploadedProfilePicture(image_url:))
.offset(y: -(pfp_size/2.0)) // Increase if set a frame
Spacer()
@@ -99,28 +97,6 @@ struct EditMetadataView: View {
}
}
func navImage(img: String) -> some View {
Image(img)
.frame(width: 33, height: 33)
.background(Color.black.opacity(0.6))
.clipShape(Circle())
}
var navBackButton: some View {
HStack {
Button {
if didChange() {
confirm_save_alert.toggle()
} else {
presentationMode.wrappedValue.dismiss()
}
} label: {
navImage(img: "chevron-left")
}
Spacer()
}
}
var body: some View {
VStack(alignment: .leading) {
TopSection
@@ -140,6 +116,18 @@ struct EditMetadataView: View {
}
Section (NSLocalizedString("Profile Picture", comment: "Label for Profile Picture section of user profile form.")) {
TextField(NSLocalizedString("https://example.com/pic.jpg", comment: "Placeholder example text for profile picture URL."), text: $picture)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
}
Section (NSLocalizedString("Banner Image", comment: "Label for Banner Image section of user profile form.")) {
TextField(NSLocalizedString("https://example.com/pic.jpg", comment: "Placeholder example text for profile picture URL."), text: $banner)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
}
Section(NSLocalizedString("Website", comment: "Label for Website section of user profile form.")) {
TextField(NSLocalizedString("https://jb55.com", comment: "Placeholder example text for website URL for user profile."), text: $website)
.autocorrectionDisabled(true)
@@ -151,10 +139,10 @@ struct EditMetadataView: View {
ZStack(alignment: .topLeading) {
TextEditor(text: $about)
.textInputAutocapitalization(.sentences)
.frame(minHeight: 45, alignment: .leading)
.frame(minHeight: 20, alignment: .leading)
.multilineTextAlignment(.leading)
Text(about.isEmpty ? placeholder : about)
.padding(4)
.padding(.leading, 4)
.opacity(about.isEmpty ? 1 : 0)
.foregroundColor(Color(uiColor: .placeholderText))
}
@@ -187,48 +175,25 @@ struct EditMetadataView: View {
}
})
}
Button(action: {
if !ln.isEmpty && !is_ln_valid(ln: ln) {
confirm_ln_address = true
} else {
save()
dismiss()
Button(NSLocalizedString("Save", comment: "Button for saving profile.")) {
if !ln.isEmpty && !is_ln_valid(ln: ln) {
confirm_ln_address = true
} else {
save()
dismiss()
}
}
}, label: {
Text(NSLocalizedString("Save", comment: "Button for saving profile."))
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
})
.buttonStyle(GradientButtonStyle(padding: 15))
.padding(.horizontal, 10)
.padding(.bottom, 10)
.disabled(!didChange())
.opacity(!didChange() ? 0.5 : 1)
.disabled(profileUploadObserver.isLoading || bannerUploadObserver.isLoading)
.alert(NSLocalizedString("Invalid Tip Address", comment: "Title of alerting as invalid tip address."), isPresented: $confirm_ln_address) {
Button(NSLocalizedString("Ok", comment: "Button to dismiss the alert.")) {
.disabled(profileUploadObserver.isLoading || bannerUploadObserver.isLoading)
.alert(NSLocalizedString("Invalid Tip Address", comment: "Title of alerting as invalid tip address."), isPresented: $confirm_ln_address) {
Button(NSLocalizedString("Ok", comment: "Button to dismiss the alert.")) {
}
} message: {
Text("The address should either begin with LNURL or should look like an email address.", comment: "Giving the description of the alert message.")
}
} message: {
Text("The address should either begin with LNURL or should look like an email address.", comment: "Giving the description of the alert message.")
}
}
.ignoresSafeArea(edges: .top)
.background(Color(.systemGroupedBackground))
.navigationBarBackButtonHidden()
.toolbar {
ToolbarItem(placement: .principal) {
navBackButton
}
}
.alert(NSLocalizedString("Discard changes?", comment: "Alert user that changes have been made."), isPresented: $confirm_save_alert) {
Button(NSLocalizedString("No", comment: "Do not discard changes."), role: .cancel) {
}
Button(NSLocalizedString("Yes", comment: "Agree to discard changes made to profile.")) {
dismiss()
}
}
}
func uploadedProfilePicture(image_url: URL?) {
@@ -238,45 +203,6 @@ struct EditMetadataView: View {
func uploadedBanner(image_url: URL?) {
banner = image_url?.absoluteString ?? ""
}
func didChange() -> Bool {
let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey)
let data = profile_txn?.unsafeUnownedValue
if data?.name ?? "" != name {
return true
}
if data?.display_name ?? "" != display_name {
return true
}
if data?.about ?? "" != about {
return true
}
if data?.website ?? "" != website {
return true
}
if data?.picture ?? "" != picture {
return true
}
if data?.banner ?? "" != banner {
return true
}
if data?.nip05 ?? "" != nip05 {
return true
}
if data?.lud16 ?? data?.lud06 ?? "" != ln {
return true
}
return false
}
}
struct EditMetadataView_Previews: PreviewProvider {
+10 -120
View File
@@ -6,7 +6,6 @@
//
import SwiftUI
import Kingfisher
class ImageUploadingObserver: ObservableObject {
@Published var isLoading: Bool = false
@@ -15,10 +14,7 @@ class ImageUploadingObserver: ObservableObject {
struct EditPictureControl: View {
let uploader: MediaUploader
let pubkey: Pubkey
var size: CGFloat? = 25
var setup: Bool? = false
@Binding var image_url: URL?
@State var image_url_temp: URL?
@ObservedObject var uploadObserver: ImageUploadingObserver
let callback: (URL?) -> Void
@@ -26,21 +22,12 @@ struct EditPictureControl: View {
@State private var show_camera = false
@State private var show_library = false
@State private var show_url_sheet = false
@State var image_upload_confirm: Bool = false
@State var preUploadedMedia: PreUploadedMedia? = nil
@Environment(\.dismiss) var dismiss
var body: some View {
Menu {
Button(action: {
self.show_url_sheet = true
}) {
Text("Image URL", comment: "Option to enter a url")
}
Button(action: {
self.show_library = true
}) {
@@ -56,53 +43,20 @@ struct EditPictureControl: View {
if uploadObserver.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: DamusColors.purple))
.frame(width: size, height: size)
.padding(10)
.background(DamusColors.white.opacity(0.7))
.clipShape(Circle())
.shadow(color: DamusColors.purple, radius: 15, x: 0, y: 0)
} else if let url = image_url, setup ?? false {
KFAnimatedImage(url)
.imageContext(.pfp, disable_animation: false)
.onFailure(fallbackUrl: URL(string: robohash(pubkey)), cacheKey: url.absoluteString)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
.scaledToFill()
.frame(width: (size ?? 25) + 10, height: (size ?? 25) + 10)
.foregroundColor(DamusColors.white)
.clipShape(Circle())
.overlay(Circle().stroke(.white, lineWidth: 4))
} else {
if setup ?? false {
Image(systemName: "person")
.resizable()
.scaledToFit()
.frame(width: size, height: size)
.foregroundColor(DamusColors.white)
.padding(20)
.clipShape(Circle())
.background {
Circle()
.fill(PinkGradient, strokeBorder: .white, lineWidth: 4)
}
} else {
Image("camera")
.resizable()
.scaledToFit()
.frame(width: size, height: size)
.foregroundColor(DamusColors.purple)
.padding(10)
.background(DamusColors.white.opacity(0.7))
.clipShape(Circle())
.background {
Circle()
.fill(DamusColors.purple, strokeBorder: .white, lineWidth: 2)
}
}
Image("camera")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
.foregroundColor(DamusColors.purple)
.padding(10)
.background(DamusColors.white.opacity(0.7))
.clipShape(Circle())
.shadow(color: DamusColors.purple, radius: 15, x: 0, y: 0)
}
}
.sheet(isPresented: $show_camera) {
@@ -125,70 +79,6 @@ struct EditPictureControl: View {
Button(NSLocalizedString("Cancel", comment: "Button to cancel the upload."), role: .cancel) {}
}
}
.sheet(isPresented: $show_url_sheet) {
ZStack {
DamusColors.adaptableWhite.edgesIgnoringSafeArea(.all)
VStack {
Text("Image URL")
.bold()
Divider()
.padding(.horizontal)
HStack {
Image(systemName: "doc.on.clipboard")
.foregroundColor(.gray)
.onTapGesture {
if let pastedURL = UIPasteboard.general.string {
image_url_temp = URL(string: pastedURL)
}
}
TextField(image_url_temp?.absoluteString ?? "", text: Binding(
get: { image_url_temp?.absoluteString ?? "" },
set: { image_url_temp = URL(string: $0) }
))
}
.padding(12)
.background {
RoundedRectangle(cornerRadius: 12)
.stroke(.gray.opacity(0.5), lineWidth: 1)
.background {
RoundedRectangle(cornerRadius: 12)
.foregroundColor(.damusAdaptableWhite)
}
}
.padding(10)
Button(action: {
show_url_sheet.toggle()
}, label: {
Text("Cancel", comment: "Cancel button text for dismissing updating image url.")
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
.padding(10)
})
.buttonStyle(NeutralButtonStyle())
.padding(10)
Button(action: {
image_url = image_url_temp
callback(image_url)
show_url_sheet.toggle()
}, label: {
Text("Update", comment: "Update button text for updating image url.")
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
})
.buttonStyle(GradientButtonStyle(padding: 10))
.padding(.horizontal, 10)
.disabled(image_url_temp == image_url)
.opacity(image_url_temp == image_url ? 0.5 : 1)
}
}
.onAppear {
image_url_temp = image_url
}
.presentationDetents([.height(300)])
.presentationDragIndicator(.visible)
}
}
private func handle_upload(media: MediaUpload) {
@@ -220,7 +110,7 @@ struct EditPictureControl_Previews: PreviewProvider {
let observer = ImageUploadingObserver()
ZStack {
Color.gray
EditPictureControl(uploader: .nostrBuild, pubkey: test_pubkey, size: 100, setup: false, image_url: url, uploadObserver: observer) { _ in
EditPictureControl(uploader: .nostrBuild, pubkey: test_pubkey, image_url: url, uploadObserver: observer) { _ in
//
}
}
+18 -21
View File
@@ -125,34 +125,31 @@ struct ProfileName: View {
return
}
var profile: Profile!
var profile_txn: NdbTxn<Profile?>!
switch update {
case .remote(let pubkey):
guard let profile_txn = damus_state.profiles.lookup(id: pubkey),
let prof = profile_txn.unsafeUnownedValue else {
return
}
handle_profile_update(profile: prof)
profile_txn = damus_state.profiles.lookup(id: pubkey)
guard let prof = profile_txn.unsafeUnownedValue else { return }
profile = prof
case .manual(_, let prof):
handle_profile_update(profile: prof)
profile = prof
}
}
}
let display_name = Profile.displayName(profile: profile, pubkey: pubkey)
if self.display_name != display_name {
self.display_name = display_name
}
@MainActor
func handle_profile_update(profile: Profile) {
let display_name = Profile.displayName(profile: profile, pubkey: pubkey)
if self.display_name != display_name {
self.display_name = display_name
}
let nip05 = damus_state.profiles.is_validated(pubkey)
if nip05 != self.nip05 {
self.nip05 = nip05
}
let nip05 = damus_state.profiles.is_validated(pubkey)
if nip05 != self.nip05 {
self.nip05 = nip05
}
if donation != profile.damus_donation {
donation = profile.damus_donation
if donation != profile.damus_donation {
donation = profile.damus_donation
}
}
}
}
+21 -28
View File
@@ -70,7 +70,6 @@ struct ProfileView: View {
@State var show_share_sheet: Bool = false
@State var show_qr_code: Bool = false
@State var action_sheet_presented: Bool = false
@State var mute_dialog_presented: Bool = false
@State var filter_state : FilterState = .posts
@State var yOffset: CGFloat = 0
@@ -163,10 +162,7 @@ struct ProfileView: View {
Button(action: {
action_sheet_presented = true
}) {
Image(systemName: "ellipsis")
.frame(width: 33, height: 33)
.background(Color.black.opacity(0.6))
.clipShape(Circle())
navImage(img: "share3")
}
.confirmationDialog(NSLocalizedString("Actions", comment: "Title for confirmation dialog to either share, report, or mute a profile."), isPresented: $action_sheet_presented) {
Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) {
@@ -200,21 +196,25 @@ struct ProfileView: View {
damus_state.postbox.send(new_ev)
}
} else {
Button(NSLocalizedString("Mute", comment: "Button to mute a profile"), role: .destructive) {
mute_dialog_presented = true
MuteDurationMenu { duration in
notify(.mute(.user(profile.pubkey, duration?.date_from_now)))
} label: {
Text("Mute", comment: "Button to mute a profile.")
.foregroundStyle(.red)
}
}
}
}
.confirmationDialog(NSLocalizedString("Mute", comment: "Title for confirmation dialog to mute a profile."), isPresented: $mute_dialog_presented) {
ForEach(DamusDuration.allCases, id: \.self) { duration in
Button {
notify(.mute(.user(profile.pubkey, duration.date_from_now)))
} label: {
Text(duration.title)
}
}
}
var customNavbar: some View {
HStack {
navBackButton
Spacer()
navActionSheetButton
}
.padding(.top, 5)
.accentColor(DamusColors.white)
}
func lnButton(unownedProfile: Profile?, record: ProfileRecord?) -> some View {
@@ -424,10 +424,10 @@ struct ProfileView: View {
aboutSection
VStack(spacing: 0) {
CustomPicker(tabs: [
(NSLocalizedString("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies)."), FilterState.posts),
(NSLocalizedString("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes)."), FilterState.posts_and_replies)
], selection: $filter_state)
CustomPicker(selection: $filter_state, content: {
Text("Notes", comment: "Label for filter for seeing only your notes (instead of notes and replies).").tag(FilterState.posts)
Text("Notes & Replies", comment: "Label for filter for seeing your notes and replies (instead of only your notes).").tag(FilterState.posts_and_replies)
})
Divider()
.frame(height: 1)
}
@@ -448,15 +448,8 @@ struct ProfileView: View {
.navigationTitle("")
.navigationBarBackButtonHidden()
.toolbar {
ToolbarItem(placement: .topBarLeading) {
navBackButton
.padding(.top, 5)
.accentColor(DamusColors.white)
}
ToolbarItem(placement: .topBarTrailing) {
navActionSheetButton
.padding(.top, 5)
.accentColor(DamusColors.white)
ToolbarItem(placement: .principal) {
customNavbar
}
}
.toolbarBackground(.hidden)
-18
View File
@@ -45,21 +45,6 @@ struct ProfileActionSheetView: View {
)
}
var muteButton: some View {
let target_pubkey = self.profile.pubkey
return VStack(alignment: .center, spacing: 10) {
MuteDurationMenu { duration in
notify(.mute(.user(target_pubkey, duration?.date_from_now)))
} label: {
Image("mute")
}
.buttonStyle(NeutralButtonShape.circle.style)
Text("Mute", comment: "Button label that allows the user to mute the user shown on-screen")
.foregroundStyle(.secondary)
.font(.caption)
}
}
var dmButton: some View {
let dm_model = damus_state.dms.lookup_or_create(profile.pubkey)
return VStack(alignment: .center, spacing: 10) {
@@ -118,9 +103,6 @@ struct ProfileActionSheetView: View {
self.followButton
self.zapButton
self.dmButton
if damus_state.keypair.pubkey != profile.pubkey && damus_state.keypair.privkey != nil {
self.muteButton
}
}
.padding()
+9 -10
View File
@@ -126,11 +126,11 @@ struct QRCodeView: View {
if our_profile?.picture != nil {
ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 3.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
.padding(.top, 20)
.padding(.top, 50)
} else {
Image(systemName: "person.fill")
.font(.system(size: 60))
.padding(.top, 20)
.padding(.top, 50)
}
if let display_name = profile?.display_name {
@@ -150,18 +150,17 @@ struct QRCodeView: View {
.interpolation(.none)
.resizable()
.scaledToFit()
.frame(minWidth: 100, maxWidth: 300, minHeight: 100, maxHeight: 300)
.frame(width: 300, height: 300)
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.white, lineWidth: 5.0)
.scaledToFit())
.stroke(DamusColors.white, lineWidth: 5.0))
.shadow(radius: 10)
Spacer()
Text("Follow me on Nostr", comment: "Text on QR code view to prompt viewer looking at screen to follow the user.")
.font(.system(size: 24, weight: .heavy))
.padding(.top, 10)
.padding(.top)
.foregroundColor(.white)
Text("Scan the code", comment: "Text on QR code view to prompt viewer to scan the QR code on screen with their device camera.")
@@ -180,7 +179,7 @@ struct QRCodeView: View {
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
.padding(20)
.padding(50)
}
}
@@ -202,11 +201,11 @@ struct QRCodeView: View {
}
}
.scaledToFit()
.frame(maxWidth: 300, maxHeight: 300)
.frame(width: 300, height: 300)
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(DamusColors.white, lineWidth: 5.0).scaledToFit())
.overlay(RoundedRectangle(cornerRadius: 10).stroke(DamusColors.white, lineWidth: 5.0))
.overlay(RoundedRectangle(cornerRadius: 10).trim(from: 0.0, to: outerTrimEnd).stroke(DamusColors.black, lineWidth: 5.5)
.rotationEffect(.degrees(-90)).scaledToFit())
.rotationEffect(.degrees(-90)))
.shadow(radius: 10)
Spacer()
+1 -1
View File
@@ -16,7 +16,7 @@ struct ReactionsView: View {
var body: some View {
ScrollView {
LazyVStack {
ForEach(model.events.events.filter { $0.last_refid() == model.target }, id: \.id) { ev in
ForEach(model.events.events, id: \.id) { ev in
ReactionView(damus_state: damus_state, reaction: ev)
}
}
-7
View File
@@ -50,13 +50,6 @@ struct RelayView: View {
.padding(.bottom, 2)
.lineLimit(1)
RelayType(is_paid: state.relay_model_cache.model(with_relay_id: relay)?.metadata.is_paid ?? false)
if relay.absoluteString.hasSuffix(".onion") {
Image("tor")
.resizable()
.interpolation(.none)
.frame(width: 20, height: 20)
}
}
Text(relay.absoluteString)
.font(.subheadline)
+124 -76
View File
@@ -11,6 +11,8 @@ import Security
struct SaveKeysView: View {
let account: CreateAccountModel
let pool: RelayPool = RelayPool(ndb: Ndb()!)
@State var pub_copied: Bool = false
@State var priv_copied: Bool = false
@State var loading: Bool = false
@State var error: String? = nil
@@ -29,98 +31,81 @@ struct SaveKeysView: View {
var body: some View {
ZStack(alignment: .top) {
VStack(alignment: .center) {
Spacer()
Image("logo-nobg")
.resizable()
.shadow(color: DamusColors.purple, radius: 2)
.frame(width: 56, height: 56, alignment: .center)
.padding(.top, 20.0)
if account.rendered_name.isEmpty {
Text("Welcome!", comment: "Text to welcome user.")
.font(.title)
.fontWeight(.heavy)
.foregroundStyle(DamusLogoGradient.gradient)
.font(.title.bold())
.padding(.bottom, 10)
} else {
Text("Welcome, \(account.rendered_name)!", comment: "Text to welcome user.")
.font(.title)
.fontWeight(.heavy)
.foregroundStyle(DamusLogoGradient.gradient)
.font(.title.bold())
.padding(.bottom, 10)
}
Text("Save your login info?", comment: "Ask user if they want to save their account information.")
.font(.title)
.fontWeight(.heavy)
.foregroundColor(DamusColors.neutral6)
.padding(.top, 5)
Text("Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus.", comment: "Reminder to user that they should save their account information.")
.padding(.bottom, 10)
Text("We'll save your account key, so you won't need to enter it manually next time you log in.", comment: "Reminder to user that they should save their account information.")
.font(.system(size: 14))
.foregroundColor(DamusColors.neutral6)
.padding(.top, 2)
.padding(.bottom, 100)
.multilineTextAlignment(.center)
Text("Private Key", comment: "Label to indicate that the text below is the user's private key used by only the user themself as a secret to login to access their account.")
.font(.title2.bold())
.padding(.bottom, 10)
Spacer()
Text("This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!", comment: "Label to describe that a private key is the user's secret account key and what they should do with it.")
.padding(.bottom, 10)
if loading {
ProgressView()
.progressViewStyle(.circular)
} else if let err = error {
Text("Error: \(err)", comment: "Error message indicating why saving keys failed.")
.foregroundColor(.red)
Button(action: {
complete_account_creation(account)
}) {
HStack {
Text("Retry", comment: "Button to retry completing account creation after an error occurred.")
.fontWeight(.semibold)
SaveKeyView(text: account.privkey.nsec, textContentType: .newPassword, is_copied: $priv_copied, focus: $privkey_focused)
.padding(.bottom, 10)
if priv_copied {
if loading {
ProgressView()
.progressViewStyle(.circular)
} else if let err = error {
Text("Error: \(err)", comment: "Error message indicating why saving keys failed.")
.foregroundColor(.red)
Button(action: {
complete_account_creation(account)
}) {
HStack {
Text("Retry", comment: "Button to retry completing account creation after an error occurred.")
.fontWeight(.semibold)
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
.padding(.top, 20)
} else {
Button(action: {
save_key(account)
complete_account_creation(account)
}) {
HStack {
Text("Save", comment: "Button to save key, complete account creation, and start using the app.")
.fontWeight(.semibold)
.buttonStyle(GradientButtonStyle())
.padding(.top, 20)
} else {
Button(action: {
complete_account_creation(account)
}) {
HStack {
Text("Let's go!", comment: "Button to complete account creation and start using the app.")
.fontWeight(.semibold)
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
.buttonStyle(GradientButtonStyle())
.padding(.top, 20)
}
.buttonStyle(GradientButtonStyle())
.padding(.top, 20)
Button(action: {
complete_account_creation(account)
}) {
HStack {
Text("Not now", comment: "Button to not save key, complete account creation, and start using the app.")
.fontWeight(.semibold)
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
}
.buttonStyle(NeutralButtonStyle(padding: EdgeInsets(top: 15, leading: 15, bottom: 15, trailing: 15), cornerRadius: 12))
.padding(.top, 20)
}
}
.padding(20)
}
.background(DamusBackground(maxHeight: UIScreen.main.bounds.size.height/2), alignment: .top)
.background(
Image("eula-bg")
.resizable()
.blur(radius: 70)
.ignoresSafeArea(),
alignment: .top
)
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: BackNav())
}
func save_key(_ account: CreateAccountModel) {
credential_handler.save_credential(pubkey: account.pubkey, privkey: account.privkey)
.onAppear {
// Hack to force keyboard to show up for a short moment and then hiding it to register password autofill flow.
pubkey_focused = true
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
pubkey_focused = false
}
}
}
func complete_account_creation(_ account: CreateAccountModel) {
@@ -137,6 +122,8 @@ struct SaveKeysView: View {
}
self.pool.register_handler(sub_id: "signup", handler: handle_event)
credential_handler.save_credential(pubkey: account.pubkey, privkey: account.privkey)
self.loading = true
@@ -201,13 +188,74 @@ struct SaveKeysView: View {
}
}
struct SaveKeyView: View {
let text: String
let textContentType: UITextContentType
@Binding var is_copied: Bool
var focus: FocusState<Bool>.Binding
func copy_text() {
UIPasteboard.general.string = text
is_copied = true
}
var body: some View {
HStack {
Spacer()
VStack {
spacerBlock(width: 0, height: 0)
Button(action: copy_text) {
Label("", image: is_copied ? "check-circle.fill" : "copy2")
.foregroundColor(is_copied ? .green : .gray)
.background {
if is_copied {
Circle()
.foregroundColor(.white)
.frame(width: 25, height: 25, alignment: .center)
.padding(.leading, -8)
.padding(.top, 1)
} else {
EmptyView()
}
}
}
}
TextField("", text: .constant(text))
.padding(5)
.background {
RoundedRectangle(cornerRadius: 4.0).opacity(0.1)
}
.textSelection(.enabled)
.font(.callout.monospaced())
.onTapGesture {
copy_text()
// Hack to force keyboard to hide. Showing keyboard on text field is necessary to register password autofill flow but the text itself should not be modified.
DispatchQueue.main.async {
end_editing()
}
}
.textContentType(textContentType)
.deleteDisabled(true)
.focused(focus)
spacerBlock(width: 0, height: 0) /// set a 'width' > 0 here to vary key Text's aspect ratio
}
}
@ViewBuilder private func spacerBlock(width: CGFloat, height: CGFloat) -> some View {
Color.orange.opacity(1)
.frame(width: width, height: height)
}
}
struct SaveKeysView_Previews: PreviewProvider {
static var previews: some View {
let model = CreateAccountModel(display_name: "William", name: "jb55", about: "I'm me")
let model = CreateAccountModel(real: "William", nick: "jb55", about: "I'm me")
SaveKeysView(account: model)
}
}
func create_account_to_metadata(_ model: CreateAccountModel) -> Profile {
return Profile(name: model.name, display_name: model.display_name, about: model.about, picture: model.profile_image?.absoluteString, banner: nil, website: nil, lud06: nil, lud16: nil, nip05: nil, damus_donation: nil)
return Profile(name: model.nick_name, display_name: model.real_name, about: model.about, picture: model.profile_image?.absoluteString, banner: nil, website: nil, lud06: nil, lud16: nil, nip05: nil, damus_donation: nil)
}
@@ -7,13 +7,10 @@
import SwiftUI
let MINIMUM_PUSH_NOTIFICATION_SYNC_DELAY_IN_SECONDS = 0.25
struct NotificationSettingsView: View {
let damus_state: DamusState
@ObservedObject var settings: UserSettingsStore
@State var notification_mode_setting_error: String? = nil
@State var notification_preferences_sync_state: PreferencesSyncState = .undefined
@Environment(\.dismiss) var dismiss
@@ -35,7 +32,6 @@ struct NotificationSettingsView: View {
Task {
do {
try await damus_state.push_notification_client.send_token()
await self.sync_up_remote_notification_settings()
settings.notifications_mode = new_value
}
catch {
@@ -48,7 +44,6 @@ struct NotificationSettingsView: View {
do {
try await damus_state.push_notification_client.revoke_token()
settings.notifications_mode = new_value
notification_preferences_sync_state = .not_applicable
}
catch {
notification_mode_setting_error = String(format: NSLocalizedString("Error disabling push notifications with the server: %@", comment: "Error label shown when user tries to disable push notifications but something fails"), error.localizedDescription)
@@ -57,61 +52,6 @@ struct NotificationSettingsView: View {
}
}
// MARK: - Push notification preference sync management
func notification_preference_binding<T>(_ raw_binding: Binding<T>) -> Binding<T> {
return Binding(
get: {
return raw_binding.wrappedValue
},
set: { new_value in
let old_value = raw_binding.wrappedValue
raw_binding.wrappedValue = new_value
if self.settings.notifications_mode == .push {
Task {
await self.send_push_notification_preferences(on_failure: {
raw_binding.wrappedValue = old_value
})
}
}
}
)
}
func sync_up_remote_notification_settings() async {
do {
notification_preferences_sync_state = .syncing
let remote_settings = try await damus_state.push_notification_client.get_settings()
let local_settings = PushNotificationClient.NotificationSettings.from(settings: settings)
if remote_settings != local_settings {
await self.send_push_notification_preferences(local_settings)
}
else {
notification_preferences_sync_state = .success
}
}
catch {
notification_preferences_sync_state = .failure(error: String(format: NSLocalizedString("Failed to get push notification preferences from the server", comment: "Error label indicating about a failure in fetching notification preferences"), error.localizedDescription))
}
}
func send_push_notification_preferences(_ new_settings: PushNotificationClient.NotificationSettings? = nil, on_failure: (() -> Void)? = nil) async {
do {
notification_preferences_sync_state = .syncing
try await damus_state.push_notification_client.set_settings(new_settings)
// Make sync appear to take at least a few milliseconds or so to avoid issues with labor perception bias (https://growth.design/case-studies/labor-perception-bias)
DispatchQueue.main.asyncAfter(deadline: .now() + MINIMUM_PUSH_NOTIFICATION_SYNC_DELAY_IN_SECONDS) {
notification_preferences_sync_state = .success
}
}
catch {
notification_preferences_sync_state = .failure(error: String(format: NSLocalizedString("Error syncing up push notifications preferences with the server: %@", comment: "Error label shown when system tries to sync up notification preferences to the push notification server but something fails"), error.localizedDescription))
on_failure?()
}
}
// MARK: - View layout
var body: some View {
Form {
if settings.enable_experimental_push_notifications {
@@ -140,40 +80,21 @@ struct NotificationSettingsView: View {
}
}
Section(
header: Text("Notification Preferences", comment: "Section header for Notification Preferences"),
footer: VStack {
switch notification_preferences_sync_state {
case .undefined, .not_applicable:
EmptyView()
case .success:
HStack {
Image("check-circle.fill")
.foregroundStyle(.damusGreen)
Text("Successfully synced", comment: "Label indicating success in syncing notification preferences")
}
case .syncing:
HStack(spacing: 10) {
ProgressView()
Text("Syncing", comment: "Label indicating success in syncing notification preferences")
}
case .failure(let error):
Text(error)
.foregroundStyle(.damusDangerPrimary)
}
}
) {
Toggle(NSLocalizedString("Zaps", comment: "Setting to enable Zap Local Notification"), isOn: self.notification_preference_binding($settings.zap_notification))
Section(header: Text("Local Notifications", comment: "Section header for damus local notifications user configuration")) {
Toggle(NSLocalizedString("Zaps", comment: "Setting to enable Zap Local Notification"), isOn: $settings.zap_notification)
.toggleStyle(.switch)
Toggle(NSLocalizedString("Mentions", comment: "Setting to enable Mention Local Notification"), isOn: self.notification_preference_binding($settings.mention_notification))
Toggle(NSLocalizedString("Mentions", comment: "Setting to enable Mention Local Notification"), isOn: $settings.mention_notification)
.toggleStyle(.switch)
Toggle(NSLocalizedString("Reposts", comment: "Setting to enable Repost Local Notification"), isOn: self.notification_preference_binding($settings.repost_notification))
Toggle(NSLocalizedString("Reposts", comment: "Setting to enable Repost Local Notification"), isOn: $settings.repost_notification)
.toggleStyle(.switch)
Toggle(NSLocalizedString("Likes", comment: "Setting to enable Like Local Notification"), isOn: self.notification_preference_binding($settings.like_notification))
Toggle(NSLocalizedString("Likes", comment: "Setting to enable Like Local Notification"), isOn: $settings.like_notification)
.toggleStyle(.switch)
Toggle(NSLocalizedString("DMs", comment: "Setting to enable DM Local Notification"), isOn: self.notification_preference_binding($settings.dm_notification))
Toggle(NSLocalizedString("DMs", comment: "Setting to enable DM Local Notification"), isOn: $settings.dm_notification)
.toggleStyle(.switch)
Toggle(NSLocalizedString("Show only from users you follow", comment: "Setting to Show notifications only associated to users your follow"), isOn: self.notification_preference_binding($settings.notification_only_from_following))
}
Section(header: Text("Notification Preference", comment: "Section header for Notification Preferences")) {
Toggle(NSLocalizedString("Show only from users you follow", comment: "Setting to Show notifications only associated to users your follow"), isOn: $settings.notification_only_from_following)
.toggleStyle(.switch)
}
@@ -192,28 +113,6 @@ struct NotificationSettingsView: View {
.onReceive(handle_notify(.switched_timeline)) { _ in
dismiss()
}
.onAppear(perform: {
Task {
if self.settings.notifications_mode == .push {
await self.sync_up_remote_notification_settings()
}
}
})
}
}
extension NotificationSettingsView {
enum PreferencesSyncState {
/// State is unknown
case undefined
/// State is not applicable (e.g. Notifications are set to local)
case not_applicable
/// Preferences are successfully synced
case success
/// Preferences are being synced
case syncing
/// There was a failure during syncing
case failure(error: String)
}
}
@@ -6,20 +6,22 @@
//
import SwiftUI
import EmojiPicker
import EmojiKit
import MCEmojiPicker
struct ReactionsSettingsView: View {
@ObservedObject var settings: UserSettingsStore
let damus_state: DamusState
@State private var isReactionsVisible: Bool = false
@State private var selectedEmoji: Emoji? = nil
var body: some View {
Form {
Section {
Text(settings.default_emoji_reaction)
.emojiPicker(
isPresented: $isReactionsVisible,
selectedEmoji: $settings.default_emoji_reaction,
arrowDirection: .up,
isDismissAfterChoosing: true
)
.onTapGesture {
isReactionsVisible = true
}
@@ -29,23 +31,43 @@ struct ReactionsSettingsView: View {
}
.navigationTitle(NSLocalizedString("Reactions", comment: "Title of emoji reactions view"))
.navigationBarTitleDisplayMode(.large)
.sheet(isPresented: $isReactionsVisible) {
NavigationView {
EmojiPickerView(selectedEmoji: $selectedEmoji, emojiProvider: damus_state.emoji_provider)
}
.presentationDetents([.medium, .large])
}
.onChange(of: selectedEmoji) { newEmoji in
guard let newEmoji else {
return
}
settings.default_emoji_reaction = newEmoji.value
}
}
}
/// From: https://stackoverflow.com/a/39425959
extension Character {
/// A simple emoji is one scalar and presented to the user as an Emoji
var isSimpleEmoji: Bool {
guard let firstScalar = unicodeScalars.first else { return false }
return firstScalar.properties.isEmoji && firstScalar.value > 0x238C
}
/// Checks if the scalars will be merged into an emoji
var isCombinedIntoEmoji: Bool { unicodeScalars.count > 1 && unicodeScalars.first?.properties.isEmoji ?? false }
var isEmoji: Bool { isSimpleEmoji || isCombinedIntoEmoji }
}
extension String {
var isSingleEmoji: Bool { count == 1 && containsEmoji }
var containsEmoji: Bool { contains { $0.isEmoji } }
var containsOnlyEmoji: Bool { !isEmpty && !contains { !$0.isEmoji } }
var emojiString: String { emojis.map { String($0) }.reduce("", +) }
var emojis: [Character] { filter { $0.isEmoji } }
var emojiScalars: [UnicodeScalar] { filter { $0.isEmoji }.flatMap { $0.unicodeScalars } }
}
func isValidEmoji(_ string: String) -> Bool {
return string.isSingleEmoji
}
struct ReactionsSettingsView_Previews: PreviewProvider {
static var previews: some View {
ReactionsSettingsView(settings: UserSettingsStore(), damus_state: test_damus_state)
ReactionsSettingsView(settings: UserSettingsStore())
}
}
+65 -31
View File
@@ -28,53 +28,32 @@ struct SetupView: View {
.fontWeight(.heavy)
.foregroundStyle(DamusLogoGradient.gradient)
Text("The social network you control", comment: "Quick description of what Damus is")
.foregroundColor(DamusColors.neutral6)
Text("The go-to iOS Nostr client", comment: "Quick description of what Damus is")
.foregroundColor(DamusColors.mediumGrey)
.padding(.top, 10)
Spacer()
WhatIsNostr()
.padding()
Button(action: {
navigationCoordinator.push(route: Route.CreateAccount)
}) {
HStack {
Text("Create Account", comment: "Button to continue to the create account page.")
.fontWeight(.semibold)
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
.padding(.horizontal)
WhyWeNeedNostr()
.padding()
Spacer()
Button(action: {
navigationCoordinator.push(route: Route.Login)
}) {
HStack {
Text("Sign In", comment: "Button to continue to login page.")
Text("Let's get started!", comment: "Button to continue to login page.")
.fontWeight(.semibold)
}
.frame(minWidth: 300, maxWidth: .infinity, maxHeight: 12, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
.padding()
HStack(spacing: 0) {
Text("By continuing you agree to our ")
.font(.subheadline)
.foregroundColor(DamusColors.neutral6)
Button(action: {
navigationCoordinator.push(route: Route.EULA)
}, label: {
Text("EULA", comment: "End User License Agreement")
.font(.subheadline)
})
.padding(.vertical, 5)
}
.padding(.bottom)
}
}
.background(DamusBackground(maxHeight: UIScreen.main.bounds.size.height/2), alignment: .top)
.background(DamusBackground(maxHeight: 300), alignment: .top)
.navigationDestination(for: Route.self) { route in
route.view(navigationCoordinator: navigationCoordinator, damusState: DamusState.empty)
}
@@ -84,6 +63,61 @@ struct SetupView: View {
}
}
struct LearnAboutNostrLink: View {
@Environment(\.openURL) var openURL
var body: some View {
HStack {
Button(action: {
openURL(URL(string: "https://nostr.com")!)
}, label: {
Text("Learn more about Nostr", comment: "Button that opens up a webpage where the user can learn more about Nostr.")
.foregroundColor(.accentColor)
})
Image(systemName: "arrow.up.right")
.font(.footnote)
.foregroundColor(.accentColor)
}
}
}
struct WhatIsNostr: View {
var body: some View {
HStack(alignment: .top) {
Image("nostr-logo")
VStack(alignment: .leading) {
Text("What is Nostr?", comment: "Heading text for section describing what is Nostr.")
.fontWeight(.bold)
.padding(.vertical, 10)
Text("Nostr is a protocol, designed for simplicity, that aims to create a censorship-resistant global social network", comment: "Description about what is Nostr.")
.foregroundColor(DamusColors.mediumGrey)
LearnAboutNostrLink()
.padding(.top, 10)
}
Spacer()
}
}
}
struct WhyWeNeedNostr: View {
var body: some View {
HStack(alignment: .top) {
Image("lightbulb")
VStack(alignment: .leading) {
Text("Why we need Nostr?", comment: "Heading text for section describing why Nostr is needed.")
.fontWeight(.bold)
.padding(.vertical, 10)
Text("Social media has developed into a key way information flows around the world. Unfortunately, our current social media systems are broken", comment: "Description about why Nostr is needed.")
.foregroundColor(DamusColors.mediumGrey)
}
Spacer()
}
}
}
struct SetupView_Previews: PreviewProvider {
static var previews: some View {
Group {
@@ -1,80 +0,0 @@
//
// PostingTimelineView.swift
// damus
//
// Created by eric on 7/15/24.
//
import SwiftUI
struct PostingTimelineView: View {
let damus_state: DamusState
var home: HomeModel
@State var search: String = ""
@State var results: [NostrEvent] = []
@State var initialOffset: CGFloat?
@State var offset: CGFloat?
@State var showSearch: Bool = true
@Binding var active_sheet: Sheets?
@FocusState private var isSearchFocused: Bool
@State private var contentOffset: CGFloat = 0
@State private var indicatorWidth: CGFloat = 0
@State private var indicatorPosition: CGFloat = 0
@SceneStorage("PostingTimelineView.filter_state") var filter_state : FilterState = .posts_and_replies
var mystery: some View {
Text("Are you lost?", comment: "Text asking the user if they are lost in the app.")
.id("what")
}
func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
var filters = ContentFilters.defaults(damus_state: damus_state)
filters.append(fstate.filter)
return ContentFilters(filters: filters).filter
}
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
TimelineView(events: home.events, loading: .constant(false), damus: damus_state, show_friend_icon: false, filter: filter) {
PullDownSearchView(state: damus_state, on_cancel: {})
}
}
var body: some View {
VStack {
ZStack {
TabView(selection: $filter_state) {
// This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why.
mystery
contentTimelineView(filter: content_filter(.posts))
.tag(FilterState.posts)
.id(FilterState.posts)
contentTimelineView(filter: content_filter(.posts_and_replies))
.tag(FilterState.posts_and_replies)
.id(FilterState.posts_and_replies)
}
.tabViewStyle(.page(indexDisplayMode: .never))
if damus_state.keypair.privkey != nil {
PostButtonContainer(is_left_handed: damus_state.settings.left_handed) {
self.active_sheet = .post(.posting(.none))
}
}
}
}
.safeAreaInset(edge: .top, spacing: 0) {
VStack(spacing: 0) {
CustomPicker(tabs: [
(NSLocalizedString("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies)."), FilterState.posts),
(NSLocalizedString("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes)."), FilterState.posts_and_replies)
],
selection: $filter_state)
Divider()
.frame(height: 1)
}
.background(DamusColors.adaptableWhite)
}
}
}
+5 -79
View File
@@ -295,64 +295,6 @@ struct CustomizeZapView: View {
}
}
struct ZapSheetViewIfPossible: View {
let damus_state: DamusState
let target: ZapTarget
let lnurl: String?
var zap_sheet: ZapSheet? {
guard let lnurl else { return nil }
return ZapSheet(target: target, lnurl: lnurl)
}
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
var body: some View {
if let zap_sheet {
CustomizeZapView(state: damus_state, target: zap_sheet.target, lnurl: zap_sheet.lnurl)
}
else {
zap_sheet_not_possible
}
}
var zap_sheet_not_possible: some View {
VStack(alignment: .center, spacing: 20) {
Image(systemName: "bolt.trianglebadge.exclamationmark.fill")
.resizable()
.scaledToFit()
.frame(width: 70)
Text("User not zappable", comment: "Headline indicating a user cannot be zapped")
.font(.headline)
Text("This user cannot be zapped because they have not configured zaps on their account yet. Time to orange-pill?", comment: "Comment explaining why a user cannot be zapped.")
.multilineTextAlignment(.center)
.opacity(0.6)
self.dm_button
}
.padding()
}
var dm_button: some View {
let dm_model = damus_state.dms.lookup_or_create(target.pubkey)
return VStack(alignment: .center, spacing: 10) {
Button(
action: {
damus_state.nav.push(route: Route.DMChat(dms: dm_model))
dismiss()
},
label: {
Image("messages")
.profile_button_style(scheme: colorScheme)
}
)
.buttonStyle(NeutralButtonShape.circle.style)
Text("Orange-pill", comment: "Button label that allows the user to start a direct message conversation with the user shown on-screen, to orange-pill them (i.e. help them to setup zaps)")
.foregroundStyle(.secondary)
.font(.caption)
}
}
}
extension View {
func hideKeyboard() {
let resign = #selector(UIResponder.resignFirstResponder)
@@ -360,25 +302,9 @@ extension View {
}
}
fileprivate func test_zap_sheet() -> ZapSheet {
let zap_target = ZapTarget.note(id: test_note.id, author: test_note.pubkey)
let lnurl = ""
return ZapSheet(target: zap_target, lnurl: lnurl)
}
#Preview {
CustomizeZapView(state: test_damus_state, target: test_zap_sheet().target, lnurl: test_zap_sheet().lnurl)
.frame(width: 400, height: 600)
}
#Preview {
ZapSheetViewIfPossible(damus_state: test_damus_state, target: test_zap_sheet().target, lnurl: test_zap_sheet().lnurl)
.frame(width: 400, height: 600)
}
#Preview {
ZapSheetViewIfPossible(damus_state: test_damus_state, target: test_zap_sheet().target, lnurl: nil)
.frame(width: 400, height: 600)
struct CustomizeZapView_Previews: PreviewProvider {
static var previews: some View {
CustomizeZapView(state: test_damus_state, target: ZapTarget.note(id: test_note.id, author: test_note.pubkey), lnurl: "")
.frame(width: 400, height: 600)
}
}
Binary file not shown.
+97 -234
View File
@@ -2,7 +2,7 @@
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
<file original="damus/en-US.lproj/InfoPlist.strings" source-language="en-US" target-language="en-US" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.3" build-num="15E204a"/>
</header>
<body>
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
@@ -44,7 +44,7 @@
</file>
<file original="damus/en-US.lproj/Localizable.strings" source-language="en-US" target-language="en-US" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.3" build-num="15E204a"/>
</header>
<body>
<trans-unit id="%@ %@" xml:space="preserve">
@@ -144,6 +144,11 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>API Key (required)</target>
<note>Prompt for required entry of API Key to use translation server.</note>
</trans-unit>
<trans-unit id="About" xml:space="preserve">
<source>About</source>
<target>About</target>
<note>Label to prompt for about text entry for user to describe about themself.</note>
</trans-unit>
<trans-unit id="About Me" xml:space="preserve">
<source>About Me</source>
<target>About Me</target>
@@ -154,11 +159,6 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>Absolute Boss</target>
<note>Placeholder text for About Me description.</note>
</trans-unit>
<trans-unit id="Absolute legend." xml:space="preserve">
<source>Absolute legend.</source>
<target>Absolute legend.</target>
<note>Example Bio</note>
</trans-unit>
<trans-unit id="Accessibility" xml:space="preserve">
<source>Accessibility</source>
<target>Accessibility</target>
@@ -189,11 +189,6 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>Add Bookmark</target>
<note>Button text to add bookmark to a note.</note>
</trans-unit>
<trans-unit id="Add Photo" xml:space="preserve">
<source>Add Photo</source>
<target>Add Photo</target>
<note>Label to indicate user can add a photo.</note>
</trans-unit>
<trans-unit id="Add all" xml:space="preserve">
<source>Add all</source>
<target>Add all</target>
@@ -260,11 +255,6 @@ Button text to add a relay</note>
<target>An additional percentage of each zap will be sent to support Damus development</target>
<note>Text indicating that they can contribute zaps to support Damus development.</note>
</trans-unit>
<trans-unit id="An unexpected error happened while trying to create the new contact list. Please contact support." xml:space="preserve">
<source>An unexpected error happened while trying to create the new contact list. Please contact support.</source>
<target>An unexpected error happened while trying to create the new contact list. Please contact support.</target>
<note>Error message for a failed contact list reset operation</note>
</trans-unit>
<trans-unit id="Animations" xml:space="preserve">
<source>Animations</source>
<target>Animations</target>
@@ -361,10 +351,10 @@ Tip: You can always change this later in Settings → Translations</target>
<target>Be the first to access upcoming premium features: Automatic translations, longer note storage, and more</target>
<note>Description of new features to be expected</note>
</trans-unit>
<trans-unit id="Bio" xml:space="preserve">
<source>Bio</source>
<target>Bio</target>
<note>Label to prompt bio entry for user to describe themself.</note>
<trans-unit id="Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus." xml:space="preserve">
<source>Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus.</source>
<target>Before we get started, you'll need to save your account info, otherwise you won't be able to login in the future if you ever uninstall Damus.</target>
<note>Reminder to user that they should save their account information.</note>
</trans-unit>
<trans-unit id="Bitcoin Lightning Tips" xml:space="preserve">
<source>Bitcoin Lightning Tips</source>
@@ -393,10 +383,10 @@ Tip: You can always change this later in Settings → Translations</target>
<target>Broadcast music playing on Apple Music</target>
<note>Toggle to enable or disable broadcasting what music is being played on Apple Music in their profile status.</note>
</trans-unit>
<trans-unit id="By continuing you agree to our " xml:space="preserve">
<source>By continuing you agree to our </source>
<target>By continuing you agree to our </target>
<note>No comment provided by engineer.</note>
<trans-unit id="By signing up, you agree to our " xml:space="preserve">
<source>By signing up, you agree to our </source>
<target>By signing up, you agree to our </target>
<note>Ask the user if they already have an account on Nostr</note>
</trans-unit>
<trans-unit id="By subscribing to Damus Purple, you are accepting our [privacy policy](https://damus.io/privacy-policy.txt) and Apple's Standard [EULA](https://www.apple.com/legal/internet-services/itunes/dev/stdeula/)" xml:space="preserve">
<source>By subscribing to Damus Purple, you are accepting our [privacy policy](https://damus.io/privacy-policy.txt) and Apple's Standard [EULA](https://www.apple.com/legal/internet-services/itunes/dev/stdeula/)</source>
@@ -423,8 +413,7 @@ Tip: You can always change this later in Settings → Translations</target>
Button to cancel the upload.
Cancel deleting bookmarks.
Cancel deleting the user.
Cancel out of logging out the user.
Cancel resetting the contact list.</note>
Cancel out of logging out the user.</note>
</trans-unit>
<trans-unit id="Changing this setting will cause the cache to be cleared. This will free space, but images may take longer to load again. Are you sure you want to proceed?" xml:space="preserve">
<source>Changing this setting will cause the cache to be cleared. This will free space, but images may take longer to load again. Are you sure you want to proceed?</source>
@@ -497,16 +486,6 @@ Button to connect to the relay.</note>
<target>Connecting</target>
<note>Relay status label that indicates a relay is connecting.</note>
</trans-unit>
<trans-unit id="Contact list (Follows + Relay list)" xml:space="preserve">
<source>Contact list (Follows + Relay list)</source>
<target>Contact list (Follows + Relay list)</target>
<note>Section title for Contact list first aid tools</note>
</trans-unit>
<trans-unit id="Contact list has been reset" xml:space="preserve">
<source>Contact list has been reset</source>
<target>Contact list has been reset</target>
<note>Message indicating that the contact list was successfully reset.</note>
</trans-unit>
<trans-unit id="Content filters" xml:space="preserve">
<source>Content filters</source>
<target>Content filters</target>
@@ -516,8 +495,7 @@ Button to connect to the relay.</note>
<source>Continue</source>
<target>Continue</target>
<note>Continue with bookmarks.
Continue with deleting the user.
Continue with resetting the contact list.</note>
Continue with deleting the user.</note>
</trans-unit>
<trans-unit id="Copied" xml:space="preserve">
<source>Copied</source>
@@ -591,11 +569,6 @@ Button to connect to the relay.</note>
<target>Copy user public key</target>
<note>Context menu option for copying the ID of the user who created the note.</note>
</trans-unit>
<trans-unit id="Could not create your initial contact list event. This is a software bug, please contact Damus support via support@damus.io or through our Nostr account for help." xml:space="preserve">
<source>Could not create your initial contact list event. This is a software bug, please contact Damus support via support@damus.io or through our Nostr account for help.</source>
<target>Could not create your initial contact list event. This is a software bug, please contact Damus support via support@damus.io or through our Nostr account for help.</target>
<note>Error message to the user indicating that the initial contact list failed to be created.</note>
</trans-unit>
<trans-unit id="Could not find the user you're looking for" xml:space="preserve">
<source>Could not find the user you're looking for</source>
<target>Could not find the user you're looking for</target>
@@ -606,21 +579,26 @@ Button to connect to the relay.</note>
<target>Could not find user to mute...</target>
<note>Alert message to indicate that the muted user could not be found.</note>
</trans-unit>
<trans-unit id="Create Account" xml:space="preserve">
<source>Create Account</source>
<target>Create Account</target>
<note>Button to continue to the create account page.</note>
</trans-unit>
<trans-unit id="Create account" xml:space="preserve">
<source>Create account</source>
<target>Create account</target>
<note>Button to navigate to create account view.</note>
</trans-unit>
<trans-unit id="Create account now" xml:space="preserve">
<source>Create account now</source>
<target>Create account now</target>
<note>Button to create account.</note>
</trans-unit>
<trans-unit id="Create new mutelist" xml:space="preserve">
<source>Create new mutelist</source>
<target>Create new mutelist</target>
<note>Title of alert prompting the user to create a new mutelist.</note>
</trans-unit>
<trans-unit id="Creator(s) of Bitcoin. Absolute legend." xml:space="preserve">
<source>Creator(s) of Bitcoin. Absolute legend.</source>
<target>Creator(s) of Bitcoin. Absolute legend.</target>
<note>Example description about Bitcoin creator(s), Satoshi Nakamoto.</note>
</trans-unit>
<trans-unit id="Custom" xml:space="preserve">
<source>Custom</source>
<target>Custom</target>
@@ -631,7 +609,6 @@ Button to connect to the relay.</note>
<target>DMs</target>
<note>Navigation title for DMs view, where DM is the English abbreviation for Direct Message.
Navigation title for view of DMs, where DM is an English abbreviation for Direct Message.
Picker option for DM selector for seeing only DMs that have been responded to. DM is the English abbreviation for Direct Message.
Setting to enable DM Local Notification
Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.</note>
</trans-unit>
@@ -718,6 +695,11 @@ Button to disconnect from a relay server.</note>
<target>Dismiss</target>
<note>Button to dismiss alert</note>
</trans-unit>
<trans-unit id="Display name" xml:space="preserve">
<source>Display name</source>
<target>Display name</target>
<note>Label to prompt display name entry.</note>
</trans-unit>
<trans-unit id="Done" xml:space="preserve">
<source>Done</source>
<target>Done</target>
@@ -784,16 +766,6 @@ Button to disconnect from a relay server.</note>
<note>Label to display that authentication to a server has failed.
Relay status label that indicates a relay had an error when connecting</note>
</trans-unit>
<trans-unit id="Error configuring push notifications with the server: %@" xml:space="preserve">
<source>Error configuring push notifications with the server: %@</source>
<target>Error configuring push notifications with the server: %@</target>
<note>Error label shown when user tries to enable push notifications but something fails</note>
</trans-unit>
<trans-unit id="Error disabling push notifications with the server: %@" xml:space="preserve">
<source>Error disabling push notifications with the server: %@</source>
<target>Error disabling push notifications with the server: %@</target>
<note>Error label shown when user tries to disable push notifications but something fails</note>
</trans-unit>
<trans-unit id="Error fetching lightning invoice" xml:space="preserve">
<source>Error fetching lightning invoice</source>
<target>Error fetching lightning invoice</target>
@@ -844,12 +816,6 @@ Relay status label that indicates a relay had an error when connecting</note>
<target>Failed to parse</target>
<note>NostrScript error message when it fails to parse a script.</note>
</trans-unit>
<trans-unit id="First Aid" xml:space="preserve">
<source>First Aid</source>
<target>First Aid</target>
<note>Navigation title for first aid settings and tools
Section header for first aid tools and settings</note>
</trans-unit>
<trans-unit id="Follow" xml:space="preserve">
<source>Follow</source>
<target>Follow</target>
@@ -941,11 +907,6 @@ My side interests include languages and I am striving to be a #polyglot - I am a
<target>Free</target>
<note>Dropdown option for selecting Free plan for DeepL translation service.</note>
</trans-unit>
<trans-unit id="General" xml:space="preserve">
<source>General</source>
<target>General</target>
<note>Section header for general damus notifications user configuration</note>
</trans-unit>
<trans-unit id="Get API Key" xml:space="preserve">
<source>Get API Key</source>
<target>Get API Key</target>
@@ -965,8 +926,7 @@ My side interests include languages and I am striving to be a #polyglot - I am a
<trans-unit id="Hashtags" xml:space="preserve">
<source>Hashtags</source>
<target>Hashtags</target>
<note>Label for filter for seeing only hashtag follows.
Section header title for a list of hashtags that are muted.</note>
<note>Section header title for a list of hashtags that are muted.</note>
</trans-unit>
<trans-unit id="Hello everybody!&#10;&#10;This is my first post on Damus, I am happy to meet you all 🤙. Whats up?&#10;&#10;#introductions" xml:space="preserve">
<source>Hello everybody!
@@ -1011,16 +971,6 @@ This is my first post on Damus, I am happy to meet you all 🤙. Whats up?
<target>Hide notes with #nsfw tags</target>
<note>Setting to hide notes with the #nsfw (not safe for work) tags</note>
</trans-unit>
<trans-unit id="Highlighted" xml:space="preserve">
<source>Highlighted</source>
<target>Highlighted</target>
<note>Label to indicate that the user is highlighting their own post.</note>
</trans-unit>
<trans-unit id="Highlighted %@" xml:space="preserve">
<source>Highlighted %@</source>
<target>Highlighted %@</target>
<note>Label to indicate that the user is highlighting 1 user.</note>
</trans-unit>
<trans-unit id="Home" xml:space="preserve">
<source>Home</source>
<target>Home</target>
@@ -1055,11 +1005,6 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
<target>Impersonation</target>
<note>Description of report type for impersonation.</note>
</trans-unit>
<trans-unit id="In progress…" xml:space="preserve">
<source>In progress…</source>
<target>In progress…</target>
<note>Loading message indicating that a contact list reset operation is in progress.</note>
</trans-unit>
<trans-unit id="Indefinite" xml:space="preserve">
<source>Indefinite</source>
<target>Indefinite</target>
@@ -1106,6 +1051,11 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
<target>LIVE</target>
<note>Text indicator that the video is a livestream.</note>
</trans-unit>
<trans-unit id="Learn more about Nostr" xml:space="preserve">
<source>Learn more about Nostr</source>
<target>Learn more about Nostr</target>
<note>Button that opens up a webpage where the user can learn more about Nostr.</note>
</trans-unit>
<trans-unit id="Learn more about the features" xml:space="preserve">
<source>Learn more about the features</source>
<target>Learn more about the features</target>
@@ -1116,6 +1066,16 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
<target>Left Handed</target>
<note>Moves the post button to the left side of the screen</note>
</trans-unit>
<trans-unit id="Let's get started!" xml:space="preserve">
<source>Let's get started!</source>
<target>Let's get started!</target>
<note>Button to continue to login page.</note>
</trans-unit>
<trans-unit id="Let's go!" xml:space="preserve">
<source>Let's go!</source>
<target>Let's go!</target>
<note>Button to complete account creation and start using the app.</note>
</trans-unit>
<trans-unit id="LibreTranslate (Open Source)" xml:space="preserve">
<source>LibreTranslate (Open Source)</source>
<target>LibreTranslate (Open Source)</target>
@@ -1146,11 +1106,6 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
<target>Load media</target>
<note>Button to show media in note.</note>
</trans-unit>
<trans-unit id="Local" xml:space="preserve">
<source>Local</source>
<target>Local</target>
<note>Option for notification mode setting: Local notification mode</note>
</trans-unit>
<trans-unit id="Local Notifications" xml:space="preserve">
<source>Local Notifications</source>
<target>Local Notifications</target>
@@ -1220,8 +1175,7 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
<trans-unit id="Mentions" xml:space="preserve">
<source>Mentions</source>
<target>Mentions</target>
<note>Label for filter for seeing mention notifications (replies, etc).
Setting to enable Mention Local Notification</note>
<note>Setting to enable Mention Local Notification</note>
</trans-unit>
<trans-unit id="Merch" xml:space="preserve">
<source>Merch</source>
@@ -1241,9 +1195,7 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
<trans-unit id="Mute" xml:space="preserve">
<source>Mute</source>
<target>Mute</target>
<note>Alert button to mute a user.
Button to mute a profile
Title for confirmation dialog to mute a profile.</note>
<note>Alert button to mute a user.</note>
</trans-unit>
<trans-unit id="Mute %@?" xml:space="preserve">
<source>Mute %@?</source>
@@ -1283,11 +1235,6 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
Text label indicating that there is no NIP-11 relay description information found. In English, N/A stands for not applicable.
Text label indicating that there is no NIP-11 relay software information found. In English, N/A stands for not applicable.</note>
</trans-unit>
<trans-unit id="Name" xml:space="preserve">
<source>Name</source>
<target>Name</target>
<note>Label to prompt name entry.</note>
</trans-unit>
<trans-unit id="Never" xml:space="preserve">
<source>Never</source>
<target>Never</target>
@@ -1313,21 +1260,11 @@ Text label indicating that there is no NIP-11 relay software information found.
<target>New to Nostr?</target>
<note>Ask the user if they are new to Nostr</note>
</trans-unit>
<trans-unit id="Next" xml:space="preserve">
<source>Next</source>
<target>Next</target>
<note>Button to continue with account creation.</note>
</trans-unit>
<trans-unit id="No" xml:space="preserve">
<source>No</source>
<target>No</target>
<note>User confirm No</note>
</trans-unit>
<trans-unit id="No contact list was found. You might experience issues using the app. If you suspect you have permanently lost your contact list (or if you never had one), you can fix this by resetting it" xml:space="preserve">
<source>No contact list was found. You might experience issues using the app. If you suspect you have permanently lost your contact list (or if you never had one), you can fix this by resetting it</source>
<target>No contact list was found. You might experience issues using the app. If you suspect you have permanently lost your contact list (or if you never had one), you can fix this by resetting it</target>
<note>Section footer for Contact list first aid tools</note>
</trans-unit>
<trans-unit id="No logs to display" xml:space="preserve">
<source>No logs to display</source>
<target>No logs to display</target>
@@ -1373,6 +1310,11 @@ Text label indicating that there is no NIP-11 relay software information found.
<target>Nostr Address</target>
<note>Label for the Nostr Address section of user profile form.</note>
</trans-unit>
<trans-unit id="Nostr is a protocol, designed for simplicity, that aims to create a censorship-resistant global social network" xml:space="preserve">
<source>Nostr is a protocol, designed for simplicity, that aims to create a censorship-resistant global social network</source>
<target>Nostr is a protocol, designed for simplicity, that aims to create a censorship-resistant global social network</target>
<note>Description about what is Nostr.</note>
</trans-unit>
<trans-unit id="NostrScript" xml:space="preserve">
<source>NostrScript</source>
<target>NostrScript</target>
@@ -1383,11 +1325,6 @@ Text label indicating that there is no NIP-11 relay software information found.
<target>NostrScript Error</target>
<note>Text indicating that there was an error with loading NostrScript. There is a more descriptive error message shown separately underneath.</note>
</trans-unit>
<trans-unit id="Not now" xml:space="preserve">
<source>Not now</source>
<target>Not now</target>
<note>Button to not save key, complete account creation, and start using the app.</note>
</trans-unit>
<trans-unit id="Note from a %@ you've muted" xml:space="preserve">
<source>Note from a %@ you've muted</source>
<target>Note from a %@ you've muted</target>
@@ -1396,18 +1333,20 @@ Text label indicating that there is no NIP-11 relay software information found.
<trans-unit id="Note you've muted" xml:space="preserve">
<source>Note you've muted</source>
<target>Note you've muted</target>
<note>Text to indicate that what is being shown is a note which has been muted.
Label indicating note has been muted</note>
<note>Text to indicate that what is being shown is a note which has been muted.</note>
</trans-unit>
<trans-unit id="Notes" xml:space="preserve">
<source>Notes</source>
<target>Notes</target>
<note>Label for filter for seeing only notes (instead of notes and replies).</note>
<note>Label for filter for seeing only notes (instead of notes and replies).
Label for filter for seeing only your notes (instead of notes and replies).
A label indicating that the notes being displayed below it are from a timeline, not search results</note>
</trans-unit>
<trans-unit id="Notes &amp; Replies" xml:space="preserve">
<source>Notes &amp; Replies</source>
<target>Notes &amp; Replies</target>
<note>Label for filter for seeing notes and replies (instead of only notes).</note>
<note>Label for filter for seeing notes and replies (instead of only notes).
Label for filter for seeing your notes and replies (instead of only your notes).</note>
</trans-unit>
<trans-unit id="Notes with the #nsfw tag usually contains adult content or other &quot;Not safe for work&quot; content" xml:space="preserve">
<source>Notes with the #nsfw tag usually contains adult content or other "Not safe for work" content</source>
@@ -1435,11 +1374,6 @@ Label indicating note has been muted</note>
<note>Section header for Damus notifications
Toolbar label for Notifications view.</note>
</trans-unit>
<trans-unit id="Notifications mode" xml:space="preserve">
<source>Notifications mode</source>
<target>Notifications mode</target>
<note>Prompt selection of the notification mode (Feature to switch between local notifications (generated from user's own phone) or push notifications (generated by Damus server).</note>
</trans-unit>
<trans-unit id="Nudity" xml:space="preserve">
<source>Nudity</source>
<target>Nudity</target>
@@ -1496,11 +1430,6 @@ Button label to dismiss an error dialog</note>
<target>Optional</target>
<note>Prompt to enter optional additional information when reporting an account or content.</note>
</trans-unit>
<trans-unit id="Orange-pill" xml:space="preserve">
<source>Orange-pill</source>
<target>Orange-pill</target>
<note>Button label that allows the user to start a direct message conversation with the user shown on-screen, to orange-pill them (i.e. help them to setup zaps)</note>
</trans-unit>
<trans-unit id="Paid Relay" xml:space="preserve">
<source>Paid Relay</source>
<target>Paid Relay</target>
@@ -1549,8 +1478,7 @@ Button label to dismiss an error dialog</note>
<trans-unit id="Post" xml:space="preserve">
<source>Post</source>
<target>Post</target>
<note>Button to post a highlight.
Button to post a note.</note>
<note>Button to post a note.</note>
</trans-unit>
<trans-unit id="Private" xml:space="preserve">
<source>Private</source>
@@ -1612,6 +1540,11 @@ Button label to dismiss an error dialog</note>
<target>Public Account ID</target>
<note>Section title for the user's public account ID.</note>
</trans-unit>
<trans-unit id="Public Key" xml:space="preserve">
<source>Public Key</source>
<target>Public Key</target>
<note>Label to indicate the public key of the account.</note>
</trans-unit>
<trans-unit id="Public key" xml:space="preserve">
<source>Public key</source>
<target>Public key</target>
@@ -1637,11 +1570,6 @@ Button label to dismiss an error dialog</note>
<target>Purple</target>
<note>Subscription service name</note>
</trans-unit>
<trans-unit id="Push" xml:space="preserve">
<source>Push</source>
<target>Push</target>
<note>Option for notification mode setting: Push notification mode</note>
</trans-unit>
<trans-unit id="QR Code" xml:space="preserve">
<source>QR Code</source>
<target>QR Code</target>
@@ -1662,11 +1590,6 @@ Button label to dismiss an error dialog</note>
<target>Ran to suspension.</target>
<note>Indication that a NostrScript was run until it reached a suspended state.</note>
</trans-unit>
<trans-unit id="React with default reaction emoji" xml:space="preserve">
<source>React with default reaction emoji</source>
<target>React with default reaction emoji</target>
<note>Accessibility label for react button</note>
</trans-unit>
<trans-unit id="Reactions" xml:space="preserve">
<source>Reactions</source>
<target>Reactions</target>
@@ -1777,11 +1700,6 @@ Button label to dismiss an error dialog</note>
<target>Repost</target>
<note>Button to repost a note</note>
</trans-unit>
<trans-unit id="Repost or quote this note" xml:space="preserve">
<source>Repost or quote this note</source>
<target>Repost or quote this note</target>
<note>Accessibility label for repost/quote button</note>
</trans-unit>
<trans-unit id="Reposted" xml:space="preserve">
<source>Reposted</source>
<target>Reposted</target>
@@ -1802,12 +1720,7 @@ Button label to dismiss an error dialog</note>
<trans-unit id="Requests" xml:space="preserve">
<source>Requests</source>
<target>Requests</target>
<note>Picker option for DM selector for seeing only message requests (DMs that someone else sent the user which has not been responded to yet</note>
</trans-unit>
<trans-unit id="Reset contact list" xml:space="preserve">
<source>Reset contact list</source>
<target>Reset contact list</target>
<note>Button to reset contact list.</note>
<note>Picker option for DM selector for seeing only message requests (DMs that someone else sent the user which has not been responded to yet). DM is the English abbreviation for Direct Message.</note>
</trans-unit>
<trans-unit id="Retry" xml:space="preserve">
<source>Retry</source>
@@ -1864,11 +1777,6 @@ Button label to dismiss an error dialog</note>
<target>Save Key in Secure Keychain</target>
<note>Toggle to save private key to the Apple secure keychain.</note>
</trans-unit>
<trans-unit id="Save your login info?" xml:space="preserve">
<source>Save your login info?</source>
<target>Save your login info?</target>
<note>Ask user if they want to save their account information.</note>
</trans-unit>
<trans-unit id="Scan Code" xml:space="preserve">
<source>Scan Code</source>
<target>Scan Code</target>
@@ -1999,11 +1907,6 @@ Button label to dismiss an error dialog</note>
<target>Share Via...</target>
<note>Button to present iOS share sheet</note>
</trans-unit>
<trans-unit id="Share externally" xml:space="preserve">
<source>Share externally</source>
<target>Share externally</target>
<note>Accessibility label for external share button</note>
</trans-unit>
<trans-unit id="Show" xml:space="preserve">
<source>Show</source>
<target>Show</target>
@@ -2051,11 +1954,6 @@ Button label to dismiss an error dialog</note>
<target>Show wallet selector</target>
<note>Toggle to show or hide selection of wallet.</note>
</trans-unit>
<trans-unit id="Sign In" xml:space="preserve">
<source>Sign In</source>
<target>Sign In</target>
<note>Button to continue to login page.</note>
</trans-unit>
<trans-unit id="Sign Out" xml:space="preserve">
<source>Sign Out</source>
<target>Sign Out</target>
@@ -2076,6 +1974,11 @@ Button label to dismiss an error dialog</note>
<target>Skip</target>
<note>Button to dismiss the suggested users screen</note>
</trans-unit>
<trans-unit id="Social media has developed into a key way information flows around the world. Unfortunately, our current social media systems are broken" xml:space="preserve">
<source>Social media has developed into a key way information flows around the world. Unfortunately, our current social media systems are broken</source>
<target>Social media has developed into a key way information flows around the world. Unfortunately, our current social media systems are broken</target>
<note>Description about why Nostr is needed.</note>
</trans-unit>
<trans-unit id="Someone posted a note" xml:space="preserve">
<source>Someone posted a note</source>
<target>Someone posted a note</target>
@@ -2184,6 +2087,11 @@ Enjoy!</target>
<target>The address should either begin with LNURL or should look like an email address.</target>
<note>Giving the description of the alert message.</note>
</trans-unit>
<trans-unit id="The go-to iOS Nostr client" xml:space="preserve">
<source>The go-to iOS Nostr client</source>
<target>The go-to iOS Nostr client</target>
<note>Quick description of what Damus is</note>
</trans-unit>
<trans-unit id="The relay you are trying to add is already added.&#10;You're all set!" xml:space="preserve">
<source>The relay you are trying to add is already added.
You're all set!</source>
@@ -2191,11 +2099,6 @@ You're all set!</source>
You're all set!</target>
<note>An error message that appears when the user attempts to add a relay that has already been added.</note>
</trans-unit>
<trans-unit id="The social network you control" xml:space="preserve">
<source>The social network you control</source>
<target>The social network you control</target>
<note>Quick description of what Damus is</note>
</trans-unit>
<trans-unit id="There has been an unexpected error with the in-app purchase. Please try again later or contact support@damus.io. Error: %@" xml:space="preserve">
<source>There has been an unexpected error with the in-app purchase. Please try again later or contact support@damus.io. Error: %@</source>
<target>There has been an unexpected error with the in-app purchase. Please try again later or contact support@damus.io. Error: %@</target>
@@ -2225,16 +2128,16 @@ Nice to meet you all! #introductions #plebchain </source>
Nice to meet you all! #introductions #plebchain </target>
<note>First post example given to the user during onboarding, as a suggestion as to what they could post first</note>
</trans-unit>
<trans-unit id="This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!" xml:space="preserve">
<source>This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!</source>
<target>This is your secret account key. You need this to access your account. Don't share this with anyone! Save it in a password manager and keep it safe!</target>
<note>Label to describe that a private key is the user's secret account key and what they should do with it.</note>
</trans-unit>
<trans-unit id="This note contains too many items and cannot be rendered" xml:space="preserve">
<source>This note contains too many items and cannot be rendered</source>
<target>This note contains too many items and cannot be rendered</target>
<note>Error message indicating that a note is too big and cannot be rendered</note>
</trans-unit>
<trans-unit id="This user cannot be zapped because they have not configured zaps on their account yet. Time to orange-pill?" xml:space="preserve">
<source>This user cannot be zapped because they have not configured zaps on their account yet. Time to orange-pill?</source>
<target>This user cannot be zapped because they have not configured zaps on their account yet. Time to orange-pill?</target>
<note>Comment explaining why a user cannot be zapped.</note>
</trans-unit>
<trans-unit id="Thread" xml:space="preserve">
<source>Thread</source>
<target>Thread</target>
@@ -2372,11 +2275,6 @@ Nice to meet you all! #introductions #plebchain </target>
<target>User muted</target>
<note>Alert message to indicate the user has been muted</note>
</trans-unit>
<trans-unit id="User not zappable" xml:space="preserve">
<source>User not zappable</source>
<target>User not zappable</target>
<note>Headline indicating a user cannot be zapped</note>
</trans-unit>
<trans-unit id="Username" xml:space="preserve">
<source>Username</source>
<target>Username</target>
@@ -2444,15 +2342,6 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
ARE YOU SURE YOU WANT TO CONTINUE?</target>
<note>Alert for deleting the users account.</note>
</trans-unit>
<trans-unit id="WARNING:&#10;&#10;This will reset your contact list, including the list of everyone you follow and the list of all relays you usually connect to. ONLY PROCEED IF YOU ARE SURE YOU HAVE LOST YOUR CONTACT LIST BEYOND RECOVERABILITY." xml:space="preserve">
<source>WARNING:
This will reset your contact list, including the list of everyone you follow and the list of all relays you usually connect to. ONLY PROCEED IF YOU ARE SURE YOU HAVE LOST YOUR CONTACT LIST BEYOND RECOVERABILITY.</source>
<target>WARNING:
This will reset your contact list, including the list of everyone you follow and the list of all relays you usually connect to. ONLY PROCEED IF YOU ARE SURE YOU HAVE LOST YOUR CONTACT LIST BEYOND RECOVERABILITY.</target>
<note>Alert for resetting the user's contact list.</note>
</trans-unit>
<trans-unit id="Wake up, %@" xml:space="preserve">
<source>Wake up, %@</source>
<target>Wake up, %@</target>
@@ -2476,16 +2365,6 @@ This will reset your contact list, including the list of everyone you follow and
<target>Wallet Relay</target>
<note>Label text indicating that below it is the information about the wallet relay.</note>
</trans-unit>
<trans-unit id="We did not detect any issues that we can automatically fix for you. If you are having issues, please contact Damus support: [support@damus.io](mailto:support@damus.io)" xml:space="preserve">
<source>We did not detect any issues that we can automatically fix for you. If you are having issues, please contact Damus support: [support@damus.io](mailto:support@damus.io)</source>
<target>We did not detect any issues that we can automatically fix for you. If you are having issues, please contact Damus support: [support@damus.io](mailto:support@damus.io)</target>
<note>Message indicating that no First Aid actions are available.</note>
</trans-unit>
<trans-unit id="We'll save your account key, so you won't need to enter it manually next time you log in." xml:space="preserve">
<source>We'll save your account key, so you won't need to enter it manually next time you log in.</source>
<target>We'll save your account key, so you won't need to enter it manually next time you log in.</target>
<note>Reminder to user that they should save their account information.</note>
</trans-unit>
<trans-unit id="Website" xml:space="preserve">
<source>Website</source>
<target>Website</target>
@@ -2526,11 +2405,21 @@ This will reset your contact list, including the list of everyone you follow and
<target>What do you want to report?</target>
<note>Header text to prompt user what issue they want to report.</note>
</trans-unit>
<trans-unit id="What is Nostr?" xml:space="preserve">
<source>What is Nostr?</source>
<target>What is Nostr?</target>
<note>Heading text for section describing what is Nostr.</note>
</trans-unit>
<trans-unit id="Who to Follow" xml:space="preserve">
<source>Who to Follow</source>
<target>Who to Follow</target>
<note>Title for a screen displaying suggestions of who to follow</note>
</trans-unit>
<trans-unit id="Why we need Nostr?" xml:space="preserve">
<source>Why we need Nostr?</source>
<target>Why we need Nostr?</target>
<note>Heading text for section describing why Nostr is needed.</note>
</trans-unit>
<trans-unit id="Words" xml:space="preserve">
<source>Words</source>
<target>Words</target>
@@ -2641,8 +2530,7 @@ This will reset your contact list, including the list of everyone you follow and
<trans-unit id="Zaps" xml:space="preserve">
<source>Zaps</source>
<target>Zaps</target>
<note>Label for filter for zap notifications.
Navigation bar title for the Zaps view.
<note>Navigation bar title for the Zaps view.
Navigation title for zap settings.
Section header for zap settings
Setting to enable Zap Local Notification
@@ -2759,11 +2647,6 @@ This will reset your contact list, including the list of everyone you follow and
<target>%@ and %@ reposted your profile</target>
<note>Notification that 2 users reposted the current user's profile</note>
</trans-unit>
<trans-unit id="required" xml:space="preserve">
<source>required</source>
<target>required</target>
<note>Label indicating that a form input is required.</note>
</trans-unit>
<trans-unit id="self" xml:space="preserve">
<source>self</source>
<target>self</target>
@@ -2818,7 +2701,7 @@ This will reset your contact list, including the list of everyone you follow and
</file>
<file original="damus/en-US.lproj/Localizable.stringsdict" source-language="en-US" target-language="en-US" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.3" build-num="15E204a"/>
</header>
<body>
<trans-unit id="/followed_by_three_and_others:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
@@ -3200,7 +3083,7 @@ This will reset your contact list, including the list of everyone you follow and
</file>
<file original="DamusNotificationService/InfoPlist.xcstrings" source-language="en-US" target-language="en-US" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.3" build-num="15E204a"/>
</header>
<body>
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
@@ -3222,7 +3105,7 @@ This will reset your contact list, including the list of everyone you follow and
</file>
<file original="DamusNotificationService/Localizable.xcstrings" source-language="en-US" target-language="en-US" datatype="plaintext">
<header>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.4" build-num="15F31d"/>
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.3" build-num="15E204a"/>
</header>
<body>
<trans-unit id="" xml:space="preserve">
@@ -3275,11 +3158,6 @@ This will reset your contact list, including the list of everyone you follow and
<target state="new">LibreTranslate (Open Source)</target>
<note>Dropdown option for selecting LibreTranslate as the translation service.</note>
</trans-unit>
<trans-unit id="Local" xml:space="preserve">
<source>Local</source>
<target state="new">Local</target>
<note>Option for notification mode setting: Local notification mode</note>
</trans-unit>
<trans-unit id="Local default" xml:space="preserve">
<source>Local default</source>
<target state="new">Local default</target>
@@ -3290,11 +3168,6 @@ This will reset your contact list, including the list of everyone you follow and
<target state="new">Mentioned by %@</target>
<note>Mentioned by heading in local notification</note>
</trans-unit>
<trans-unit id="Muted event" xml:space="preserve">
<source>Muted event</source>
<target state="new">Muted event</target>
<note>Title for a push notification which has been muted</note>
</trans-unit>
<trans-unit id="New encrypted direct message" xml:space="preserve">
<source>New encrypted direct message</source>
<target state="new">New encrypted direct message</target>
@@ -3330,11 +3203,6 @@ This will reset your contact list, including the list of everyone you follow and
<target state="new">Production</target>
<note>Label indicating the production environment for Damus Purple</note>
</trans-unit>
<trans-unit id="Push" xml:space="preserve">
<source>Push</source>
<target state="new">Push</target>
<note>Option for notification mode setting: Push notification mode</note>
</trans-unit>
<trans-unit id="Reposted by %@" xml:space="preserve">
<source>Reposted by %@</source>
<target state="new">Reposted by %@</target>
@@ -3370,11 +3238,6 @@ This will reset your contact list, including the list of everyone you follow and
<target state="new">Test (local)</target>
<note>Label indicating a local test environment for Damus Purple functionality (Developer feature)</note>
</trans-unit>
<trans-unit id="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" xml:space="preserve">
<source>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</source>
<target state="new">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</target>
<note>Description for a push notification which has been muted, and explanation that we cannot suppress it</note>
</trans-unit>
<trans-unit id="This note contains too many items and cannot be rendered" xml:space="preserve">
<source>This note contains too many items and cannot be rendered</source>
<target state="new">This note contains too many items and cannot be rendered</target>
@@ -39,18 +39,12 @@
"LibreTranslate (Open Source)" : {
"comment" : "Dropdown option for selecting LibreTranslate as the translation service."
},
"Local" : {
"comment" : "Option for notification mode setting: Local notification mode"
},
"Local default" : {
"comment" : "Dropdown option label for system default for Lightning wallet."
},
"Mentioned by %@" : {
"comment" : "Mentioned by heading in local notification"
},
"Muted event" : {
"comment" : "Title for a push notification which has been muted"
},
"New encrypted direct message" : {
"comment" : "Notification that the user has received a new direct message"
},
@@ -84,9 +78,6 @@
"Production" : {
"comment" : "Label indicating the production environment for Damus Purple"
},
"Push" : {
"comment" : "Option for notification mode setting: Push notification mode"
},
"Reposted by %@" : {
"comment" : "Reposted by heading in local notification"
},
@@ -108,9 +99,6 @@
"Test (local)" : {
"comment" : "Label indicating a local test environment for Damus Purple functionality (Developer feature)"
},
"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"
},
"This note contains too many items and cannot be rendered" : {
"comment" : "Error message indicating that a note is too big and cannot be rendered"
},
+2 -2
View File
@@ -3,10 +3,10 @@
"project" : "damus.xcodeproj",
"targetLocale" : "en-US",
"toolInfo" : {
"toolBuildNumber" : "15F31d",
"toolBuildNumber" : "15E204a",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "15.4"
"toolVersion" : "15.3"
},
"version" : "1.0"
}
Binary file not shown.
-16
View File
@@ -226,22 +226,6 @@
<string>Megosztások</string>
</dict>
</dict>
<key>quoted_reposts_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@QUOTE_REPOSTS@</string>
<key>QUOTE_REPOSTS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Idézet</string>
<key>other</key>
<string>Idézetek</string>
</dict>
</dict>
<key>sats</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
-14
View File
@@ -198,20 +198,6 @@
<string>リポスト</string>
</dict>
</dict>
<key>quoted_reposts_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@QUOTE_REPOSTS@</string>
<key>QUOTE_REPOSTS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>引用</string>
</dict>
</dict>
<key>sats</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
Binary file not shown.
Binary file not shown.
-16
View File
@@ -226,22 +226,6 @@
<string>Delningar</string>
</dict>
</dict>
<key>quoted_reposts_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@QUOTE_REPOSTS@</string>
<key>QUOTE_REPOSTS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Citat</string>
<key>other</key>
<string>Citat</string>
</dict>
</dict>
<key>sats</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+24 -11
View File
@@ -10,15 +10,7 @@ import XCTest
class LikeTests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testLikeHasNotification() throws {
func testReactionTextNote() throws {
let cindy = Pubkey(hex: "9d9181f0aea6500e1f360e07b9f37e25c72169b5158ae78df53f295272b6b71c")!
let bob = Pubkey(hex: "218837fe8c94a66ae33af277bcbda45a0319e7726220cd76171b9dd1a468af91")!
let liked = NostrEvent(content: "awesome #[0] post",
@@ -28,9 +20,30 @@ class LikeTests: XCTestCase {
let like_ev = make_like_event(keypair: test_keypair_full, liked: liked)!
XCTAssertTrue(like_ev.referenced_pubkeys.contains(test_keypair.pubkey))
XCTAssertTrue(like_ev.referenced_pubkeys.contains(cindy))
XCTAssertTrue(like_ev.referenced_pubkeys.contains(bob))
XCTAssertFalse(like_ev.referenced_pubkeys.contains(cindy))
XCTAssertFalse(like_ev.referenced_pubkeys.contains(bob))
XCTAssertEqual(like_ev.last_refid()!, id)
XCTAssertTrue(like_ev.tags.allSatisfy { !$0[0].matches_char("a") })
let kindTag = try XCTUnwrap(like_ev.tags.first(where: { $0.count >= 2 && $0[0].matches_char("k") }))
XCTAssertTrue(kindTag[1].matches_str("1"))
}
func testReactionLongFormNote() throws {
let liked = test_longform_event.event
let id = liked.id
let like_ev = make_like_event(keypair: test_keypair_full, liked: liked)!
XCTAssertTrue(like_ev.referenced_pubkeys.contains(test_keypair.pubkey))
XCTAssertEqual(like_ev.last_refid()!, id)
let dTagValue = try XCTUnwrap(liked.tags.first { $0[0].matches_char("d") })
let aTag = try XCTUnwrap(like_ev.tags.first { $0[0].matches_char("a") })
XCTAssertTrue(aTag[1].matches_str("30023:\(test_keypair.pubkey.hex()):\(dTagValue[1])"))
let kindTag = try XCTUnwrap(like_ev.tags.first(where: { $0.count >= 2 && $0[0].matches_char("k") }))
// FIXME(tyiu) the assertion below fails for some reason even though in a different test, I was able to assert kind 1 just fine.
// XCTAssertTrue(kindTag[1].matches_str("30023"))
}
func testToReactionEmoji() {
+1 -4
View File
@@ -7,7 +7,6 @@
import Foundation
@testable import damus
import EmojiPicker
// Generates a test damus state with configurable mock parameters
func generate_test_damus_state(
@@ -51,9 +50,7 @@ func generate_test_damus_state(
music: .init(onChange: {_ in }),
video: .init(),
ndb: ndb,
quote_reposts: .init(our_pubkey: our_pubkey),
emoji_provider: DefaultEmojiProvider(showAllVariations: false)
)
quote_reposts: .init(our_pubkey: our_pubkey) )
return damus
}
+79
View File
@@ -0,0 +1,79 @@
//
// TrieTests.swift
// damusTests
//
// Created by Terry Yiu on 6/26/23.
//
import XCTest
@testable import damus
final class TrieTests: XCTestCase {
func testFind() throws {
let trie = Trie<String>()
let keys = ["foobar", "food", "foo", "somethingelse", "duplicate", "duplicate"]
keys.forEach {
trie.insert(key: $0, value: $0)
}
let allResults = trie.find(key: "")
XCTAssertEqual(Set(allResults), Set(["foobar", "food", "foo", "somethingelse", "duplicate"]))
let fooResults = trie.find(key: "foo")
XCTAssertEqual(fooResults.first, "foo")
XCTAssertEqual(Set(fooResults), Set(["foobar", "food", "foo"]))
let foodResults = trie.find(key: "food")
XCTAssertEqual(foodResults, ["food"])
let ooResults = trie.find(key: "oo")
XCTAssertEqual(Set(ooResults), Set(["foobar", "food", "foo"]))
let aResults = trie.find(key: "a")
XCTAssertEqual(Set(aResults), Set(["foobar", "duplicate"]))
let notFoundResults = trie.find(key: "notfound")
XCTAssertEqual(notFoundResults, [])
// Sanity check that the root node has children.
XCTAssertTrue(trie.hasChildren)
// Sanity check that the root node has no values.
XCTAssertFalse(trie.hasValues)
}
func testRemove() {
let trie = Trie<String>()
let keys = ["foobar", "food", "foo", "somethingelse", "duplicate", "duplicate"]
keys.forEach {
trie.insert(key: $0, value: $0)
}
keys.forEach {
trie.remove(key: $0, value: $0)
}
let allResults = trie.find(key: "")
XCTAssertTrue(allResults.isEmpty)
let fooResults = trie.find(key: "foo")
XCTAssertTrue(fooResults.isEmpty)
let foodResults = trie.find(key: "food")
XCTAssertTrue(foodResults.isEmpty)
let ooResults = trie.find(key: "oo")
XCTAssertTrue(ooResults.isEmpty)
let aResults = trie.find(key: "a")
XCTAssertTrue(aResults.isEmpty)
// Verify that removal of values from all the keys that were inserted in the trie previously also resulted in the cleanup of the trie.
XCTAssertFalse(trie.hasChildren)
XCTAssertFalse(trie.hasValues)
}
}
+136
View File
@@ -0,0 +1,136 @@
//
// UserSearchCacheTests.swift
// damusTests
//
// Created by Terry Yiu on 6/30/23.
//
import XCTest
@testable import damus
// TODO: Update these tests to work with NostrDB Profile changes (https://github.com/damus-io/damus/issues/1586)
final class UserSearchCacheTests: XCTestCase {
/*
var keypair: FullKeypair? = nil
let damusState = test_damus_state
let nip05 = "_@somedomain.com"
@MainActor
override func setUpWithError() throws {
keypair = try XCTUnwrap(generate_new_keypair())
if let keypair {
let pubkey = keypair.pubkey
let validatedNip05 = try XCTUnwrap(NIP05.parse(nip05))
damusState.profiles.set_validated(pubkey, nip05: validatedNip05)
let profile = Profile(name: "tyiu", display_name: "Terry Yiu", about: nil, picture: nil, banner: nil, website: nil, lud06: nil, lud16: nil, nip05: nip05, damus_donation: nil)
// Lookup to synchronize access on profiles dictionary to avoid race conditions.
let _ = damusState.profiles.lookup(id: pubkey)
}
}
override func tearDown() {
keypair = nil
}
func testSearch() throws {
let keypair = try XCTUnwrap(keypair)
XCTAssertEqual(damusState.user_search_cache.search(key: "tyiu"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "ty"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "terry yiu"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "rry"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "somedomain"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "dom"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "i"), [keypair.pubkey])
}
@MainActor
func testUpdateProfile() throws {
let keypair = try XCTUnwrap(keypair)
let newNip05 = "_@other.xyz"
_ = try XCTUnwrap(NIP05.parse(newNip05))
damusState.profiles.set_validated(keypair.pubkey, nip05: NIP05.parse(newNip05))
// Old profile attributes are removed from cache.
XCTAssertEqual(damusState.user_search_cache.search(key: "tyiu"), [])
XCTAssertEqual(damusState.user_search_cache.search(key: "ty"), [])
XCTAssertEqual(damusState.user_search_cache.search(key: "Terry Yiu"), [])
XCTAssertEqual(damusState.user_search_cache.search(key: "rry"), [])
XCTAssertEqual(damusState.user_search_cache.search(key: "somedomain"), [])
XCTAssertEqual(damusState.user_search_cache.search(key: "dom"), [])
// New profile attributes are added to cache.
XCTAssertEqual(damusState.user_search_cache.search(key: "whoami"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "hoa"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "t-dawg"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "daw"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "other"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "xyz"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "the"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "y"), [keypair.pubkey])
}
*/
/*
func testUpdateOwnContactsPetnames() throws {
let keypair = try XCTUnwrap(keypair)
let damus = Pubkey(hex: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681")!
let jb55 = Pubkey(hex: "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245")!
var pubkeysToPetnames = [Pubkey: String]()
pubkeysToPetnames[damus] = "damus"
pubkeysToPetnames[jb55] = "jb55"
let contactsEvent = try createContactsEventWithPetnames(pubkeysToPetnames: pubkeysToPetnames)
// Initial own contacts event caching on searchable petnames.
damusState.user_search_cache.updateOwnContactsPetnames(id: keypair.pubkey, oldEvent: nil, newEvent: contactsEvent)
XCTAssertEqual(damusState.user_search_cache.search(key: "damus"), [damus])
XCTAssertEqual(damusState.user_search_cache.search(key: "jb55"), [jb55])
XCTAssertEqual(damusState.user_search_cache.search(key: "5"), [jb55])
// Replace one of the petnames and verify if the cache updates accordingly.
pubkeysToPetnames.removeValue(forKey: jb55)
pubkeysToPetnames[jb55] = "bill"
let newContactsEvent = try createContactsEventWithPetnames(pubkeysToPetnames: pubkeysToPetnames)
damusState.user_search_cache.updateOwnContactsPetnames(id: keypair.pubkey, oldEvent: contactsEvent, newEvent: newContactsEvent)
XCTAssertEqual(damusState.user_search_cache.search(key: "damus"), [damus])
XCTAssertEqual(damusState.user_search_cache.search(key: "jb55"), [])
XCTAssertEqual(damusState.user_search_cache.search(key: "5"), [])
XCTAssertEqual(damusState.user_search_cache.search(key: "bill"), [jb55])
XCTAssertEqual(damusState.user_search_cache.search(key: "l"), [jb55])
}
*/
/*
private func createContactsEventWithPetnames(pubkeysToPetnames: [Pubkey: String]) throws -> NostrEvent {
let keypair = try XCTUnwrap(keypair)
let bootstrapRelays = load_bootstrap_relays(pubkey: keypair.pubkey)
let relayInfo = RelayInfo(read: true, write: true)
var relays: [String: RelayInfo] = [:]
for relay in bootstrapRelays {
relays[relay] = relayInfo
}
let relayJson = encode_json(relays)!
let tags: [[String]] = pubkeysToPetnames.enumerated().map {
["p", $0.element.key.description, "", $0.element.value]
}
return NostrEvent(content: relayJson, keypair: keypair.to_keypair(), kind: NostrKind.contacts.rawValue, tags: tags)!
}
*/
}

Some files were not shown because too many files have changed in this diff Show More