Compare commits

...

40 Commits

Author SHA1 Message Date
tyiu a8b6b5f10e Remove image, video, and icon from non-media link previews if media links are present to reduce screen clutter
Changelog-Changed: Removed media from regular link previews if media is already being shown
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-06-22 22:21:50 -04:00
tyiu 1bedb6b2bd Fix note rendering to include non-media link previews with image, video, and icon removed when media previews are disabled
Closes: https://github.com/damus-io/damus/issues/3099

Changelog-Fixed: Fixed note rendering to include regular link previews with media removed when media previews are disabled
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-06-22 22:20:06 -04:00
Daniel D’Aquino 8d9f728cf0 Display wallet response error if available
This commit improves error handling in the wallet's "send" feature, by
displaying more specific wallet response error messages when available.

Closes: https://github.com/damus-io/damus/issues/3095
Changelog-Fixed: Improve error handling on wallet send feature
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-06-20 19:02:16 -07:00
tyiu 2c62741e25 Remove incorrect Thai translation for notes_from_three_and_others
Closes: https://github.com/damus-io/damus/issues/3093
Fixes: cfb6f07c67a8 ("Remove Thai translation with incorrect arguments")

Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-06-20 18:53:01 -07:00
transifex-integration[bot] 1f612f7fde Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-06-20 15:04:06 -07:00
transifex-integration[bot] 0e9e102d0f Translate Localizable.stringsdict in th
100% translated source file: 'Localizable.stringsdict'
on 'th'.
2025-06-20 15:04:06 -07:00
transifex-integration[bot] b94e8765a1 Translate Localizable.strings in nl
100% translated source file: 'Localizable.strings'
on 'nl'.
2025-06-20 15:04:06 -07:00
transifex-integration[bot] 53964f5c1a Translate Localizable.stringsdict in nl
100% translated source file: 'Localizable.stringsdict'
on 'nl'.
2025-06-20 15:04:06 -07:00
tyiu bd574d93c3 Fix localizable strings in FollowPackView
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-06-20 15:04:06 -07:00
tyiu 47514ace79 Export strings for translation
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-06-20 15:04:06 -07:00
transifex-integration[bot] 298b43733f Translate Localizable.stringsdict in nl
100% translated source file: 'Localizable.stringsdict'
on 'nl'.
2025-06-20 15:04:06 -07:00
transifex-integration[bot] 02116c0af5 Translate Localizable.strings in nl
100% translated source file: 'Localizable.strings'
on 'nl'.
2025-06-20 15:04:06 -07:00
tyiu 92121e3b2d Export strings for translation
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-06-20 15:04:06 -07:00
Daniel D’Aquino c92094823e Add send feature
Closes: https://github.com/damus-io/damus/issues/2988
Changelog-Added: Added send feature to the wallet view
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-06-20 14:12:50 -07:00
Daniel D’Aquino f4b1a504a5 Fix issue with balance loading appearance
During the implementation of the "hide balance" feature, the balance
view was refactored in a way that caused it to not be redacted anymore,
making it show the "??" instead of the intended skeleton loader.

This commit fixes that issue without reverting the hide balance feature.

Changelog-Fixed: Fixed issue where the text "??" would appear on the balance while loading
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
2025-06-20 14:12:50 -07:00
tyiu 99ae7de5eb Rename Friends of Friends to Trusted Network and add popover tips to DMs and Notifications toolbars on Trusted Network button
Changelog-Changed: Renamed Friends of Friends to Trusted Network

Changelog-Added: Added popover tips to DMs and Notifications toolbars on Trusted Network button
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-06-16 13:32:02 -07:00
tyiu b3d9ee3fc0 Add tip in threads to inform users what trusted network means
Changelog-Added: Added tip in threads to inform users what trusted network means
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-06-16 13:32:02 -07:00
tyiu e65219ee3e Add web of trust reply sorting in threads to mitigate spam
Changelog-Added: Added web of trust reply sorting in threads to mitigate spam
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-06-16 13:32:02 -07:00
ericholguin 414c67a919 Follow Packs
This PR adds and enables follow packs in the universe view.

Closes: #3012

Changelog-Added: Added follow list kind 39089
Changelog-Added: Added follow pack preview
Changelog-Added: Added follow pack timeline to Universe View
Changelog-Removed: Removed hashtags in Universe View

Signed-off-by: ericholguin <ericholguin@apache.org>
2025-06-16 10:34:18 -07:00
tyiu f436291209 Hide end previewables when hashtags are present
Changelog-Fixed: Hide end previewables when hashtags are present
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-06-02 11:44:59 -07:00
tyiu a9196a39df Fix wallet transactions to always show profile display name unless there is no pubkey
Changelog-Fixed: Fixed wallet transactions to always show profile display name unless there is no pubkey
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-06-02 11:41:51 -07:00
William Casarin 6a8ee9c360 Merge remote-tracking branches 'github/pr/3066' and 'github/pr/3065' 2025-06-02 07:01:35 -07:00
tyiu 947e24864e Add privacy-based redaction to nsec in key settings view
Changelog-Changed: Added privacy-based redaction to nsec in key settings view
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-06-01 19:54:46 -04:00
tyiu b9198d6bd7 Add privacy-based redaction to wallet view
Changelog-Changed: Added privacy-based redaction to wallet view
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-06-01 18:36:33 -04:00
William Casarin 14bf187a6e Merge remote-tracking branches 'github/pr/30{62,57,55,51,50}'
Merge a bunch of changes from terry, translations, and me

Terry Yiu (4):
      Add NIP-05 favicon to profile names and NIP-05 web of trust feed
      Fix quotes view header alignment
      Export strings for translation
      Rename Bitcoin Beach wallet to Blink

Transifex (11):
      Translate Localizable.strings in th
      Translate Localizable.strings in th
      Translate Localizable.strings in nl
      Translate Localizable.strings in de
      Translate Localizable.stringsdict in de
      Translate Localizable.stringsdict in de
      Translate Localizable.strings in th
      Translate Localizable.strings in th
      Translate Localizable.strings in th
      Translate Localizable.strings in th
      Translate Localizable.strings in th

William Casarin (2):
      perf: don't use regex in trim_{prefix,suffix}
2025-06-01 00:36:19 +02:00
William Casarin c996e5f8b3 perf: don't use regex in trim_{prefix,suffix}
regex is overkill for this, and performance is quite bad

Fixes: b131c74ee3 ("Add prefix and suffix string trimming functions")
Signed-off-by: William Casarin <jb55@jb55.com>
2025-05-31 20:17:14 +02:00
transifex-integration[bot] 56dde30cf6 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-05-29 09:24:57 -07:00
transifex-integration[bot] 95bfbae131 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-05-29 09:24:57 -07:00
transifex-integration[bot] 3da0ff7ecc Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-05-29 09:24:57 -07:00
transifex-integration[bot] b8f846ded8 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-05-29 09:24:57 -07:00
transifex-integration[bot] e74c45ad39 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-05-29 09:24:57 -07:00
transifex-integration[bot] e6a03522c6 Translate Localizable.stringsdict in de
100% translated source file: 'Localizable.stringsdict'
on 'de'.
2025-05-29 09:24:57 -07:00
transifex-integration[bot] dbc7d79ecd Translate Localizable.stringsdict in de
100% translated source file: 'Localizable.stringsdict'
on 'de'.
2025-05-29 09:24:57 -07:00
transifex-integration[bot] d2b5a65eca Translate Localizable.strings in de
100% translated source file: 'Localizable.strings'
on 'de'.
2025-05-29 09:24:57 -07:00
transifex-integration[bot] 16b19d3a96 Translate Localizable.strings in nl
100% translated source file: 'Localizable.strings'
on 'nl'.
2025-05-29 09:24:56 -07:00
transifex-integration[bot] 70edb8d7c5 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-05-29 09:24:56 -07:00
tyiu ea04ebe95c Export strings for translation
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-05-29 09:24:56 -07:00
transifex-integration[bot] 44cf47faa4 Translate Localizable.strings in th
100% translated source file: 'Localizable.strings'
on 'th'.
2025-05-29 09:24:55 -07:00
tyiu 612abfd862 Fix quotes view header alignment
Changelog-Fixed: Fixed quotes view header alignment
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-05-27 17:40:07 -04:00
tyiu 20af086273 Add NIP-05 favicon to profile names and NIP-05 web of trust feed
Changelog-Added: Added NIP-05 favicon to profile names and NIP-05 web of trust feed
Signed-off-by: Terry Yiu <git@tyiu.xyz>
2025-05-27 00:54:03 -04:00
80 changed files with 3737 additions and 420 deletions
+185 -8
View File
@@ -14,6 +14,12 @@
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 */; };
3A2BAC5A2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */; };
3A2BAC5B2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */; };
3A2BAC5C2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */; };
3A2BAC5E2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */; };
3A2BAC5F2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */; };
3A2BAC602DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */; };
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */; };
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */; };
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; };
@@ -21,10 +27,23 @@
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 */; };
3A515C502DF4E100002D3B34 /* TrustedNetworkRepliesTip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A515C4F2DF4E100002D3B34 /* TrustedNetworkRepliesTip.swift */; };
3A515C512DF4E100002D3B34 /* TrustedNetworkRepliesTip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A515C4F2DF4E100002D3B34 /* TrustedNetworkRepliesTip.swift */; };
3A515C522DF4E100002D3B34 /* TrustedNetworkRepliesTip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A515C4F2DF4E100002D3B34 /* TrustedNetworkRepliesTip.swift */; };
3A515C542DF5371D002D3B34 /* TrustedNetworkButtonTipViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A515C532DF5371D002D3B34 /* TrustedNetworkButtonTipViewStyle.swift */; };
3A515C552DF5371D002D3B34 /* TrustedNetworkButtonTipViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A515C532DF5371D002D3B34 /* TrustedNetworkButtonTipViewStyle.swift */; };
3A515C562DF5371D002D3B34 /* TrustedNetworkButtonTipViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A515C532DF5371D002D3B34 /* TrustedNetworkButtonTipViewStyle.swift */; };
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; };
3A92C0FE2DE16E9800CEEBAC /* FaviconCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A92C0FD2DE16E9800CEEBAC /* FaviconCache.swift */; };
3A92C0FF2DE16E9800CEEBAC /* FaviconCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A92C0FD2DE16E9800CEEBAC /* FaviconCache.swift */; };
3A92C1002DE16E9800CEEBAC /* FaviconCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A92C0FD2DE16E9800CEEBAC /* FaviconCache.swift */; };
3A92C1022DE17ACA00CEEBAC /* NIP05DomainTimelineHeaderViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A92C1012DE17ACA00CEEBAC /* NIP05DomainTimelineHeaderViewTests.swift */; };
3A96E3FE2D6BCE3800AE1630 /* RepostedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A96E3FD2D6BCE3800AE1630 /* RepostedTests.swift */; };
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
3AA2F4E82DF1467A00B18606 /* TrustedNetworkButtonTip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA2F4E72DF1467A00B18606 /* TrustedNetworkButtonTip.swift */; };
3AA2F4E92DF1467A00B18606 /* TrustedNetworkButtonTip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA2F4E72DF1467A00B18606 /* TrustedNetworkButtonTip.swift */; };
3AA2F4EA2DF1467A00B18606 /* TrustedNetworkButtonTip.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA2F4E72DF1467A00B18606 /* TrustedNetworkButtonTip.swift */; };
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA59D1C2999B0400061C48E /* DraftsModel.swift */; };
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95C9298DF87B00F3D526 /* TranslationService.swift */; };
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */; };
@@ -33,6 +52,15 @@
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685A297633BC00C46468 /* InfoPlist.strings */; };
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 3ACB685D297633BC00C46468 /* Localizable.strings */; };
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */; };
3ACF94382DA9A52F00971A4E /* FaviconFinder in Frameworks */ = {isa = PBXBuildFile; productRef = 3ACF94372DA9A52F00971A4E /* FaviconFinder */; };
3ACF943E2DA9B10800971A4E /* FaviconFinder in Frameworks */ = {isa = PBXBuildFile; productRef = 3ACF943D2DA9B10800971A4E /* FaviconFinder */; };
3ACF94402DA9B11200971A4E /* FaviconFinder in Frameworks */ = {isa = PBXBuildFile; productRef = 3ACF943F2DA9B11200971A4E /* FaviconFinder */; };
3ACF94422DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94412DA9FCAB00971A4E /* NIP05DomainTimelineView.swift */; };
3ACF94432DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94412DA9FCAB00971A4E /* NIP05DomainTimelineView.swift */; };
3ACF94442DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94412DA9FCAB00971A4E /* NIP05DomainTimelineView.swift */; };
3ACF94462DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94452DAA006500971A4E /* NIP05DomainEventsModel.swift */; };
3ACF94472DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94452DAA006500971A4E /* NIP05DomainEventsModel.swift */; };
3ACF94482DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3ACF94452DAA006500971A4E /* NIP05DomainEventsModel.swift */; };
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; };
3CCD1E6A2A874C4E0099A953 /* Nip98HTTPAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCD1E692A874C4E0099A953 /* Nip98HTTPAuth.swift */; };
4C011B5E2BD0A56A002F2F9B /* ChatEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C011B5C2BD0A56A002F2F9B /* ChatEventView.swift */; };
@@ -47,6 +75,7 @@
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; };
4C0C03992A61E27B0098B3B8 /* primal.wasm in Resources */ = {isa = PBXBuildFile; fileRef = 4C0C03972A61E27B0098B3B8 /* primal.wasm */; };
4C0C039A2A61E27B0098B3B8 /* bool_setting.wasm in Resources */ = {isa = PBXBuildFile; fileRef = 4C0C03982A61E27B0098B3B8 /* bool_setting.wasm */; };
4C0ED07F2D7A1E260020D8A2 /* Benchmarking.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0ED07E2D7A1E260020D8A2 /* Benchmarking.swift */; };
4C1253502A76C5B20004F4B8 /* UnfollowedNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C12534F2A76C5B20004F4B8 /* UnfollowedNotify.swift */; };
4C1253522A76C6130004F4B8 /* ComposeNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1253512A76C6130004F4B8 /* ComposeNotify.swift */; };
4C1253542A76C7D60004F4B8 /* LogoutNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1253532A76C7D60004F4B8 /* LogoutNotify.swift */; };
@@ -244,7 +273,7 @@
4C8D00CF29E38B950036AF10 /* nostr_bech32.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00CE29E38B950036AF10 /* nostr_bech32.c */; };
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */; };
4C8D1A6C29F1DFC200ACDF75 /* FriendIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D1A6B29F1DFC200ACDF75 /* FriendIcon.swift */; };
4C8D1A6F29F31E5000ACDF75 /* FriendsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */; };
4C8D1A6F29F31E5000ACDF75 /* TrustedNetworkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D1A6E29F31E5000ACDF75 /* TrustedNetworkButton.swift */; };
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */; };
4C8FA7242BED58A900798A6A /* ThreadReply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C45E5012BED4D000025A428 /* ThreadReply.swift */; };
4C9054852A6AEAA000811EEC /* NdbTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9054842A6AEAA000811EEC /* NdbTests.swift */; };
@@ -398,11 +427,26 @@
5C0567592C8FBDE30073F23A /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; };
5C05675A2C8FBDE70073F23A /* NDBSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0567572C8FBC560073F23A /* NDBSearchView.swift */; };
5C0707D12A1ECB38004E7B51 /* DamusLogoGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */; };
5C09FD122DF283D700823661 /* FollowPackModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C09FD112DF283D200823661 /* FollowPackModel.swift */; };
5C09FD132DF283D700823661 /* FollowPackModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C09FD112DF283D200823661 /* FollowPackModel.swift */; };
5C09FD142DF283D700823661 /* FollowPackModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C09FD112DF283D200823661 /* FollowPackModel.swift */; };
5C14C29B2BBBA29C00079FD2 /* RelaySoftwareDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C14C29A2BBBA29C00079FD2 /* RelaySoftwareDetail.swift */; };
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 /* HighlightDraftContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4D9EA62C042FA5005EA0F7 /* HighlightDraftContentView.swift */; };
5C4FA7EC2DC29AE900CE658C /* FollowPackEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA7EB2DC29AE900CE658C /* FollowPackEvent.swift */; };
5C4FA7ED2DC29AE900CE658C /* FollowPackEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA7EB2DC29AE900CE658C /* FollowPackEvent.swift */; };
5C4FA7EE2DC29AE900CE658C /* FollowPackEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA7EB2DC29AE900CE658C /* FollowPackEvent.swift */; };
5C4FA7FB2DC29C3800CE658C /* FollowPackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA7F92DC29C3800CE658C /* FollowPackView.swift */; };
5C4FA7FC2DC29C3800CE658C /* FollowPackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA7F92DC29C3800CE658C /* FollowPackView.swift */; };
5C4FA7FD2DC29C3800CE658C /* FollowPackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA7F92DC29C3800CE658C /* FollowPackView.swift */; };
5C4FA7FF2DC5119300CE658C /* FollowPackPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA7FE2DC5119300CE658C /* FollowPackPreview.swift */; };
5C4FA8002DC5119300CE658C /* FollowPackPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA7FE2DC5119300CE658C /* FollowPackPreview.swift */; };
5C4FA8012DC5119300CE658C /* FollowPackPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA7FE2DC5119300CE658C /* FollowPackPreview.swift */; };
5C4FA8032DCAF80E00CE658C /* FollowPackTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA8022DCAF80400CE658C /* FollowPackTimeline.swift */; };
5C4FA8042DCAF80E00CE658C /* FollowPackTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA8022DCAF80400CE658C /* FollowPackTimeline.swift */; };
5C4FA8052DCAF80E00CE658C /* FollowPackTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4FA8022DCAF80400CE658C /* FollowPackTimeline.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 */; };
@@ -743,7 +787,7 @@
82D6FBD52CD99F7900C925F4 /* ConnectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D095C2A098C5D00943473 /* ConnectWalletView.swift */; };
82D6FBD62CD99F7900C925F4 /* WalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D095D2A098C5D00943473 /* WalletView.swift */; };
82D6FBD72CD99F7900C925F4 /* NWCScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09672A0AE9B200943473 /* NWCScannerView.swift */; };
82D6FBD82CD99F7900C925F4 /* FriendsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */; };
82D6FBD82CD99F7900C925F4 /* TrustedNetworkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D1A6E29F31E5000ACDF75 /* TrustedNetworkButton.swift */; };
82D6FBD92CD99F7900C925F4 /* GradientFollowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71694F32A6732B7001F4053 /* GradientFollowButton.swift */; };
82D6FBDA2CD99F7900C925F4 /* AlbyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09652A0AE62100943473 /* AlbyButton.swift */; };
82D6FBDC2CD99F7900C925F4 /* DamusVideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2929DDF54400516EAC /* DamusVideoPlayerView.swift */; };
@@ -1285,7 +1329,7 @@
D73E5ECC2C6A97F4007EB227 /* SuggestedUsersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71694EB2A662292001F4053 /* SuggestedUsersViewModel.swift */; };
D73E5ED22C6A97F4007EB227 /* WalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D095D2A098C5D00943473 /* WalletView.swift */; };
D73E5ED32C6A97F4007EB227 /* NWCScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09672A0AE9B200943473 /* NWCScannerView.swift */; };
D73E5ED42C6A97F4007EB227 /* FriendsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */; };
D73E5ED42C6A97F4007EB227 /* TrustedNetworkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D1A6E29F31E5000ACDF75 /* TrustedNetworkButton.swift */; };
D73E5ED52C6A97F4007EB227 /* GradientFollowButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71694F32A6732B7001F4053 /* GradientFollowButton.swift */; };
D73E5ED62C6A97F4007EB227 /* AlbyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09652A0AE62100943473 /* AlbyButton.swift */; };
D73E5ED82C6A97F4007EB227 /* DamusVideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2929DDF54400516EAC /* DamusVideoPlayerView.swift */; };
@@ -1546,6 +1590,9 @@
D7A0D8752D1FE67900DCBE59 /* EditPictureControlTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7A0D8742D1FE66A00DCBE59 /* EditPictureControlTests.swift */; };
D7A343EE2AD0D77C00CED48B /* InlineSnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D7A343ED2AD0D77C00CED48B /* InlineSnapshotTesting */; };
D7A343F02AD0D77C00CED48B /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D7A343EF2AD0D77C00CED48B /* SnapshotTesting */; };
D7AACFFF2E0387B800FB7699 /* LnurlAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AACFFE2E0387B800FB7699 /* LnurlAmountView.swift */; };
D7AAD0002E0387B800FB7699 /* LnurlAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AACFFE2E0387B800FB7699 /* LnurlAmountView.swift */; };
D7AAD0012E0387B800FB7699 /* LnurlAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AACFFE2E0387B800FB7699 /* LnurlAmountView.swift */; };
D7ADD3DE2B53854300F104C4 /* DamusPurpleURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADD3DD2B53854300F104C4 /* DamusPurpleURL.swift */; };
D7ADD3E02B538D4200F104C4 /* DamusPurpleURLSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADD3DF2B538D4200F104C4 /* DamusPurpleURLSheetView.swift */; };
D7ADD3E22B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADD3E12B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift */; };
@@ -1676,6 +1723,9 @@
D7DB930C2D69486700DA1EE5 /* NIP65.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93092D69485A00DA1EE5 /* NIP65.swift */; };
D7DBD41F2B02F15E002A6197 /* NostrKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */; };
D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */; };
D7DF58322DFCF18D00E9AD28 /* SendPaymentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DF58312DFCF18800E9AD28 /* SendPaymentView.swift */; };
D7DF58332DFCF18D00E9AD28 /* SendPaymentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DF58312DFCF18800E9AD28 /* SendPaymentView.swift */; };
D7DF58342DFCF18D00E9AD28 /* SendPaymentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DF58312DFCF18800E9AD28 /* SendPaymentView.swift */; };
D7EB00B02CD59C8D00660C07 /* PresentFullScreenItemNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */; };
D7EB00B12CD59C8D00660C07 /* PresentFullScreenItemNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */; };
D7EDED152B11776B0018B19C /* LibreTranslateServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */; };
@@ -1808,6 +1858,8 @@
3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A2B8B0A296A8982009CC16D /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "en-US"; path = "en-US.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainTimelineHeaderView.swift; sourceTree = "<group>"; };
3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainPubkeysView.swift; sourceTree = "<group>"; };
3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyDescriptionTests.swift; sourceTree = "<group>"; };
3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationUtil.swift; sourceTree = "<group>"; };
3A3040F229A91366008A0F29 /* ProfileViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewTests.swift; sourceTree = "<group>"; };
@@ -1833,6 +1885,8 @@
3A47CB782BDA05A200728A7C /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = "<group>"; };
3A47CB792BDA05A200728A7C /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fi; path = fi.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutedThreadsManager.swift; sourceTree = "<group>"; };
3A515C4F2DF4E100002D3B34 /* TrustedNetworkRepliesTip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedNetworkRepliesTip.swift; sourceTree = "<group>"; };
3A515C532DF5371D002D3B34 /* TrustedNetworkButtonTipViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedNetworkButtonTipViewStyle.swift; sourceTree = "<group>"; };
3A5C4575296A879E0032D398 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-419"; path = "es-419.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A5CAE1E298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/Localizable.strings"; sourceTree = "<group>"; };
@@ -1853,6 +1907,8 @@
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>"; };
3A92C0FD2DE16E9800CEEBAC /* FaviconCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconCache.swift; sourceTree = "<group>"; };
3A92C1012DE17ACA00CEEBAC /* NIP05DomainTimelineHeaderViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainTimelineHeaderViewTests.swift; sourceTree = "<group>"; };
3A93342929884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pl-PL"; path = "pl-PL.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A93342A29884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pl-PL"; path = "pl-PL.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A93342B29884CA600D6A8F3 /* pl-PL */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pl-PL"; path = "pl-PL.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -1865,6 +1921,7 @@
3A994C4E2BE5B9370019F632 /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = "<group>"; };
3AA247FE297E3D900090C62D /* RepostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostsView.swift; sourceTree = "<group>"; };
3AA24801297E3DC20090C62D /* RepostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostView.swift; sourceTree = "<group>"; };
3AA2F4E72DF1467A00B18606 /* TrustedNetworkButtonTip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedNetworkButtonTip.swift; sourceTree = "<group>"; };
3AA59D1C2999B0400061C48E /* DraftsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsModel.swift; sourceTree = "<group>"; };
3AA5E70229B682A5002701ED /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = "<group>"; };
3AA5E70329B682AD002701ED /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@@ -1891,6 +1948,8 @@
3ACB685B297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3ACB685E297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = "<group>"; };
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeAgoTests.swift; sourceTree = "<group>"; };
3ACF94412DA9FCAB00971A4E /* NIP05DomainTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainTimelineView.swift; sourceTree = "<group>"; };
3ACF94452DAA006500971A4E /* NIP05DomainEventsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP05DomainEventsModel.swift; sourceTree = "<group>"; };
3AD14EB529C40F38009D2D9C /* hu-HU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "hu-HU"; path = "hu-HU.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3AD14EB629C40F38009D2D9C /* hu-HU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hu-HU"; path = "hu-HU.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AD14EB729C40F38009D2D9C /* hu-HU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hu-HU"; path = "hu-HU.lproj/Localizable.strings"; sourceTree = "<group>"; };
@@ -1925,6 +1984,7 @@
4C0A3F92280F66F5000448DE /* ReplyMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyMap.swift; sourceTree = "<group>"; };
4C0C03972A61E27B0098B3B8 /* primal.wasm */ = {isa = PBXFileReference; lastKnownFileType = file; name = primal.wasm; path = nostrscript/primal.wasm; sourceTree = SOURCE_ROOT; };
4C0C03982A61E27B0098B3B8 /* bool_setting.wasm */ = {isa = PBXFileReference; lastKnownFileType = file; name = bool_setting.wasm; path = nostrscript/bool_setting.wasm; sourceTree = SOURCE_ROOT; };
4C0ED07E2D7A1E260020D8A2 /* Benchmarking.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Benchmarking.swift; sourceTree = "<group>"; };
4C12534F2A76C5B20004F4B8 /* UnfollowedNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnfollowedNotify.swift; sourceTree = "<group>"; };
4C1253512A76C6130004F4B8 /* ComposeNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeNotify.swift; sourceTree = "<group>"; };
4C1253532A76C7D60004F4B8 /* LogoutNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogoutNotify.swift; sourceTree = "<group>"; };
@@ -2249,7 +2309,7 @@
4C8D00D229E3C19F0036AF10 /* str_block.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = str_block.h; sourceTree = "<group>"; };
4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP19Tests.swift; sourceTree = "<group>"; };
4C8D1A6B29F1DFC200ACDF75 /* FriendIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FriendIcon.swift; sourceTree = "<group>"; };
4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FriendsButton.swift; sourceTree = "<group>"; };
4C8D1A6E29F31E5000ACDF75 /* TrustedNetworkButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedNetworkButton.swift; sourceTree = "<group>"; };
4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusColors.swift; sourceTree = "<group>"; };
4C9054842A6AEAA000811EEC /* NdbTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NdbTests.swift; sourceTree = "<group>"; };
4C9054882A6AED4700811EEC /* NdbTagIterator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NdbTagIterator.swift; sourceTree = "<group>"; };
@@ -2410,11 +2470,16 @@
5C0567542C8B60C20073F23A /* OffsetExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OffsetExtension.swift; sourceTree = "<group>"; };
5C0567572C8FBC560073F23A /* NDBSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NDBSearchView.swift; sourceTree = "<group>"; };
5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLogoGradient.swift; sourceTree = "<group>"; };
5C09FD112DF283D200823661 /* FollowPackModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowPackModel.swift; sourceTree = "<group>"; };
5C14C29A2BBBA29C00079FD2 /* RelaySoftwareDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelaySoftwareDetail.swift; sourceTree = "<group>"; };
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 /* HighlightDraftContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDraftContentView.swift; sourceTree = "<group>"; };
5C4FA7EB2DC29AE900CE658C /* FollowPackEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowPackEvent.swift; sourceTree = "<group>"; };
5C4FA7F92DC29C3800CE658C /* FollowPackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowPackView.swift; sourceTree = "<group>"; };
5C4FA7FE2DC5119300CE658C /* FollowPackPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowPackPreview.swift; sourceTree = "<group>"; };
5C4FA8022DCAF80400CE658C /* FollowPackTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowPackTimeline.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>"; };
@@ -2550,6 +2615,7 @@
D79C4C182AFEB061003A41B4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
D79C4C1C2AFEB061003A41B4 /* DamusNotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DamusNotificationService.entitlements; sourceTree = "<group>"; };
D7A0D8742D1FE66A00DCBE59 /* EditPictureControlTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditPictureControlTests.swift; sourceTree = "<group>"; };
D7AACFFE2E0387B800FB7699 /* LnurlAmountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LnurlAmountView.swift; sourceTree = "<group>"; };
D7ADD3DD2B53854300F104C4 /* DamusPurpleURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleURL.swift; sourceTree = "<group>"; };
D7ADD3DF2B538D4200F104C4 /* DamusPurpleURLSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleURLSheetView.swift; sourceTree = "<group>"; };
D7ADD3E12B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleVerifyNpubView.swift; sourceTree = "<group>"; };
@@ -2574,6 +2640,7 @@
D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Undistractor.swift; sourceTree = "<group>"; };
D7DB93092D69485A00DA1EE5 /* NIP65.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP65.swift; sourceTree = "<group>"; };
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = "<group>"; };
D7DF58312DFCF18800E9AD28 /* SendPaymentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendPaymentView.swift; sourceTree = "<group>"; };
D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentFullScreenItemNotify.swift; sourceTree = "<group>"; };
D7EDED1B2B1178FE0018B19C /* NoteContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContent.swift; sourceTree = "<group>"; };
D7EDED1D2B11797D0018B19C /* LongformEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LongformEvent.swift; sourceTree = "<group>"; };
@@ -2617,6 +2684,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
3ACF94382DA9A52F00971A4E /* FaviconFinder in Frameworks */,
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
D7DB1FE42D5A9AC900CF06DA /* CryptoSwift in Frameworks */,
3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */,
@@ -2648,6 +2716,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
3ACF94402DA9B11200971A4E /* FaviconFinder in Frameworks */,
82D6FC862CD9A4A600C925F4 /* MarkdownUI in Frameworks */,
D7DB1FEC2D5A9F6500CF06DA /* CryptoSwift in Frameworks */,
82D6FC8A2CD9A54600C925F4 /* SwipeActions in Frameworks */,
@@ -2664,6 +2733,7 @@
buildActionMask = 2147483647;
files = (
D703D7AF2C670FB700A400EA /* MarkdownUI in Frameworks */,
3ACF943E2DA9B10800971A4E /* FaviconFinder in Frameworks */,
D73E5F9D2C6AA8E3007EB227 /* SwipeActions in Frameworks */,
D7DB1FE82D5A9F5300CF06DA /* CryptoSwift in Frameworks */,
D73E5F762C6A997E007EB227 /* EmojiPicker in Frameworks */,
@@ -2698,6 +2768,16 @@
path = "Empty Views";
sourceTree = "<group>";
};
3A515C4E2DF4E0E6002D3B34 /* Tips */ = {
isa = PBXGroup;
children = (
3AA2F4E72DF1467A00B18606 /* TrustedNetworkButtonTip.swift */,
3A515C532DF5371D002D3B34 /* TrustedNetworkButtonTipViewStyle.swift */,
3A515C4F2DF4E100002D3B34 /* TrustedNetworkRepliesTip.swift */,
);
path = Tips;
sourceTree = "<group>";
};
3AA24800297E3DAE0090C62D /* Reposts */ = {
isa = PBXGroup;
children = (
@@ -2778,6 +2858,8 @@
4C0A3F8D280F63FF000448DE /* Models */ = {
isa = PBXGroup;
children = (
5C09FD112DF283D200823661 /* FollowPackModel.swift */,
5C4FA7EB2DC29AE900CE658C /* FollowPackEvent.swift */,
D73BDB122D71212600D69970 /* NostrNetworkManager */,
D74F43082B23F09300425B75 /* Purple */,
BA3759882ABCCDE30018D73B /* Camera */,
@@ -2841,6 +2923,7 @@
5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */,
D773BC5E2C6D538500349F0A /* CommentItem.swift */,
D767066E2C8BB3CE00F09726 /* URLHandler.swift */,
3ACF94452DAA006500971A4E /* NIP05DomainEventsModel.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -3192,6 +3275,7 @@
4C190F232A547D1700027FD5 /* NostrScript */,
4C7D095A2A098C5C00943473 /* Wallet */,
4C8D1A6D29F31E4100ACDF75 /* Buttons */,
3A515C4E2DF4E0E6002D3B34 /* Tips */,
4C1A9A2829DDF53B00516EAC /* Video */,
4C1A9A1B29DDCF8B00516EAC /* Settings */,
4CFF8F6129CC9A80008DB934 /* Images */,
@@ -3255,6 +3339,9 @@
D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */,
D71AD8FC2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift */,
D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */,
3ACF94412DA9FCAB00971A4E /* NIP05DomainTimelineView.swift */,
3A2BAC592DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift */,
3A2BAC5D2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift */,
);
path = Views;
sourceTree = "<group>";
@@ -3291,6 +3378,8 @@
4C7D095A2A098C5C00943473 /* Wallet */ = {
isa = PBXGroup;
children = (
D7AACFFE2E0387B800FB7699 /* LnurlAmountView.swift */,
D7DF58312DFCF18800E9AD28 /* SendPaymentView.swift */,
5C8498012D5D14FA00F74FEB /* ZapExplainer.swift */,
5CB017302D4422D600A9ED05 /* NWCSettings.swift */,
5CB0172C2D42C76600A9ED05 /* BalanceView.swift */,
@@ -3377,6 +3466,7 @@
D74AAFCE2B155D8C006CF0F4 /* ZapDataModel.swift */,
D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */,
D7D09AB42DADCA5600AB170D /* CoinosDeterministicAccountClient.swift */,
3A92C0FD2DE16E9800CEEBAC /* FaviconCache.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -3385,7 +3475,7 @@
isa = PBXGroup;
children = (
5CB017202D2D985800A9ED05 /* CoinosButton.swift */,
4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */,
4C8D1A6E29F31E5000ACDF75 /* TrustedNetworkButton.swift */,
F71694F32A6732B7001F4053 /* GradientFollowButton.swift */,
4C7D09652A0AE62100943473 /* AlbyButton.swift */,
);
@@ -3579,6 +3669,7 @@
4CC7AAEE297F11B300430951 /* Events */ = {
isa = PBXGroup;
children = (
5C4FA7FA2DC29C3800CE658C /* FollowPack */,
5CC852A02BDED9970039FFC5 /* Highlight */,
4CA927682A290F8F0098A105 /* Components */,
4CC7AAEF297F11C700430951 /* SelectedEventView.swift */,
@@ -3772,6 +3863,8 @@
4C2D34402BDAF1B300F9FB44 /* NIP10Tests.swift */,
D72E12792BEEEED000F4F781 /* NostrFilterTests.swift */,
3A96E3FD2D6BCE3800AE1630 /* RepostedTests.swift */,
4C0ED07E2D7A1E260020D8A2 /* Benchmarking.swift */,
3A92C1012DE17ACA00CEEBAC /* NIP05DomainTimelineHeaderViewTests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@@ -3892,6 +3985,16 @@
path = Images;
sourceTree = "<group>";
};
5C4FA7FA2DC29C3800CE658C /* FollowPack */ = {
isa = PBXGroup;
children = (
5C4FA8022DCAF80400CE658C /* FollowPackTimeline.swift */,
5C4FA7FE2DC5119300CE658C /* FollowPackPreview.swift */,
5C4FA7F92DC29C3800CE658C /* FollowPackView.swift */,
);
path = FollowPack;
sourceTree = "<group>";
};
5CC852A02BDED9970039FFC5 /* Highlight */ = {
isa = PBXGroup;
children = (
@@ -4173,6 +4276,7 @@
D70D90972CDED61800CD0534 /* CodeScanner */,
D7C48C0A2D12DE0C00A3BACF /* SwiftyCrop */,
D7DB1FE32D5A9AC900CF06DA /* CryptoSwift */,
3ACF94372DA9A52F00971A4E /* FaviconFinder */,
);
productName = damus;
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
@@ -4240,6 +4344,7 @@
D7F360282CEBBE34009D34DA /* CodeScanner */,
D7C48C0C2D12E34900A3BACF /* SwiftyCrop */,
D7DB1FEB2D5A9F6500CF06DA /* CryptoSwift */,
3ACF943F2DA9B11200971A4E /* FaviconFinder */,
);
productName = "share extension";
productReference = 82D6FA972CD9820500C925F4 /* ShareExtension.appex */;
@@ -4269,6 +4374,7 @@
D70D909B2CDED7B200CD0534 /* CodeScanner */,
D7C48C0E2D12E35600A3BACF /* SwiftyCrop */,
D7DB1FE72D5A9F5300CF06DA /* CryptoSwift */,
3ACF943D2DA9B10800971A4E /* FaviconFinder */,
);
productName = "highlighter action extension";
productReference = D703D7172C66E47100A400EA /* HighlighterActionExtension.appex */;
@@ -4381,6 +4487,7 @@
D70D90962CDED61800CD0534 /* XCRemoteSwiftPackageReference "CodeScanner" */,
D7C48C092D12DE0C00A3BACF /* XCRemoteSwiftPackageReference "SwiftyCrop" */,
D7DB1FE22D5A9AC900CF06DA /* XCRemoteSwiftPackageReference "CryptoSwift" */,
3ACF94362DA9A52F00971A4E /* XCRemoteSwiftPackageReference "FaviconFinder" */,
);
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
projectDirPath = "";
@@ -4523,6 +4630,7 @@
4C3EA64428FF558100C48A62 /* sha256.c in Sources */,
5C8498032D5D150000F74FEB /* ZapExplainer.swift in Sources */,
504323A72A34915F006AE6DC /* RelayModel.swift in Sources */,
3A2BAC5C2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */,
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */,
4C32B9542A9AD44700DC3548 /* FlatBuffersUtils.swift in Sources */,
D7EDED1C2B1178FE0018B19C /* NoteContent.swift in Sources */,
@@ -4561,6 +4669,7 @@
4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */,
4C363AA228296A7E006E126D /* SearchView.swift in Sources */,
D798D2282B085CDA00234419 /* NdbNote+.swift in Sources */,
3ACF94422DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */,
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */,
4C1253662A76D0FF0004F4B8 /* OnlyZapsNotify.swift in Sources */,
4CA927652A290F1A0098A105 /* TimeDot.swift in Sources */,
@@ -4593,6 +4702,7 @@
4C363A9028247A1D006E126D /* NostrLink.swift in Sources */,
4C3D52B6298DB4E6001C5831 /* ZapEvent.swift in Sources */,
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */,
D7AAD0012E0387B800FB7699 /* LnurlAmountView.swift in Sources */,
F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */,
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */,
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */,
@@ -4603,7 +4713,7 @@
4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */,
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
D74AAFCF2B155D8C006CF0F4 /* ZapDataModel.swift in Sources */,
4C8D1A6F29F31E5000ACDF75 /* FriendsButton.swift in Sources */,
4C8D1A6F29F31E5000ACDF75 /* TrustedNetworkButton.swift in Sources */,
D7100C562B76F8E600C59298 /* PurpleViewPrimitives.swift in Sources */,
B57B4C642B312BFA00A232C0 /* RelayAuthenticationDetail.swift in Sources */,
D7EDED2E2B128E8A0018B19C /* CollectionExtension.swift in Sources */,
@@ -4612,6 +4722,7 @@
BA3759972ABCCF360018D73B /* CameraPreview.swift in Sources */,
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
4CA927672A290F8B0098A105 /* RelativeTime.swift in Sources */,
5C4FA8032DCAF80E00CE658C /* FollowPackTimeline.swift in Sources */,
4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */,
D7CB5D4B2B11721600AD4105 /* ZapType.swift in Sources */,
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */,
@@ -4689,6 +4800,7 @@
D7100C5A2B76FD5100C59298 /* LogoView.swift in Sources */,
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */,
3A515C502DF4E100002D3B34 /* TrustedNetworkRepliesTip.swift in Sources */,
D7CB5D3B2B112FBB00AD4105 /* NotificationFormatter.swift in Sources */,
4C4E137B2A76D5FB00BDD832 /* MuteThreadNotify.swift in Sources */,
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */,
@@ -4756,7 +4868,9 @@
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
4C12535E2A76CA870004F4B8 /* SwitchedTimelineNotify.swift in Sources */,
D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */,
5C4FA7ED2DC29AE900CE658C /* FollowPackEvent.swift in Sources */,
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */,
3ACF94462DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */,
D734B1452CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */,
4C4E137D2A76D63600BDD832 /* UnmuteThreadNotify.swift in Sources */,
D706C5B72D602A110027C627 /* QueueableNotify.swift in Sources */,
@@ -4782,6 +4896,7 @@
4CA352A22A76AEC5003BB08B /* LikedNotify.swift in Sources */,
5CC8529F2BD744F60039FFC5 /* HighlightView.swift in Sources */,
BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */,
3A515C562DF5371D002D3B34 /* TrustedNetworkButtonTipViewStyle.swift in Sources */,
5CC8529D2BD741CD0039FFC5 /* HighlightEvent.swift in Sources */,
4C9146FD2A2A87C200DDEA40 /* wasm.c in Sources */,
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
@@ -4828,6 +4943,7 @@
4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */,
BA3759942ABCCEBA0018D73B /* CameraService.swift in Sources */,
4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */,
3AA2F4E82DF1467A00B18606 /* TrustedNetworkButtonTip.swift in Sources */,
4CF0ABD82981980C00D66079 /* Lists.swift in Sources */,
F71694EA2A662232001F4053 /* OnboardingSuggestionsView.swift in Sources */,
4C12536A2A76D3850004F4B8 /* RelaysChangedNotify.swift in Sources */,
@@ -4835,6 +4951,7 @@
D7373BAA2B68A65A00F7783D /* PurpleAccountUpdateNotify.swift in Sources */,
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */,
3CCD1E6A2A874C4E0099A953 /* Nip98HTTPAuth.swift in Sources */,
5C4FA7FF2DC5119300CE658C /* FollowPackPreview.swift in Sources */,
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */,
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */,
5C14C29B2BBBA29C00079FD2 /* RelaySoftwareDetail.swift in Sources */,
@@ -4865,6 +4982,7 @@
3165648B295B70D500C64604 /* LinkView.swift in Sources */,
4C8D00CF29E38B950036AF10 /* nostr_bech32.c in Sources */,
D7CB5D5C2B1176B200AD4105 /* MediaUploader.swift in Sources */,
5C4FA7FD2DC29C3800CE658C /* FollowPackView.swift in Sources */,
4C1253562A76C8C60004F4B8 /* BroadcastNotify.swift in Sources */,
4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */,
B533694E2B66D791008A805E /* MutelistManager.swift in Sources */,
@@ -4892,6 +5010,7 @@
4C3AC7A728369BA200E1F516 /* SearchHomeView.swift in Sources */,
E0EE9DD42B8E5FEA00F3002D /* ImageProcessing.swift in Sources */,
4CB883B0297705DD00DC99E7 /* NoteZapButton.swift in Sources */,
D7DF58342DFCF18D00E9AD28 /* SendPaymentView.swift in Sources */,
4C363A922825FCF2006E126D /* ProfileUpdate.swift in Sources */,
4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */,
4C32B9502A9AD44700DC3548 /* FlatBufferBuilder.swift in Sources */,
@@ -4904,6 +5023,7 @@
4C32B9512A9AD44700DC3548 /* FlatbuffersErrors.swift in Sources */,
5CC852A62BE00F180039FFC5 /* HighlightEventRef.swift in Sources */,
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
5C09FD132DF283D700823661 /* FollowPackModel.swift in Sources */,
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
D71AD8FF2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */,
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
@@ -4916,6 +5036,7 @@
4CE4F0F229D4FCFA005914DB /* DebouncedOnChange.swift in Sources */,
4C32B9592A9AD44700DC3548 /* Table.swift in Sources */,
4C5D5C9D2A6B2CB40024563C /* AsciiCharacter.swift in Sources */,
3A2BAC5E2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */,
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
4C9146FE2A2A87C200DDEA40 /* nostrscript.c in Sources */,
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
@@ -4963,6 +5084,7 @@
4C32B95A2A9AD44700DC3548 /* Verifiable.swift in Sources */,
4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */,
501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */,
3A92C0FE2DE16E9800CEEBAC /* FaviconCache.swift in Sources */,
4C1A9A1D29DDCF9B00516EAC /* NotificationSettingsView.swift in Sources */,
5CC868DD2AA29B3200FB22BA /* NeutralButtonStyle.swift in Sources */,
4C75EFB528049D790006080F /* Relay.swift in Sources */,
@@ -5002,6 +5124,7 @@
4C9B0DEE2A65A75F00CBDA21 /* AttrStringTestExtensions.swift in Sources */,
4C19AE552A5D977400C90DB7 /* HashtagTests.swift in Sources */,
D72927AD2BAB515C00F93E90 /* RelayURLTests.swift in Sources */,
4C0ED07F2D7A1E260020D8A2 /* Benchmarking.swift in Sources */,
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
D71DC1EC2A9129C3006E207C /* PostViewTests.swift in Sources */,
3AAC7A022A60FE72002B50DF /* LocalizationUtilTests.swift in Sources */,
@@ -5045,6 +5168,7 @@
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */,
4C684A552A7E91FE005E6031 /* LargeEventTests.swift in Sources */,
E02B54182B4DFADA0077FF42 /* Bech32ObjectTests.swift in Sources */,
3A92C1022DE17ACA00CEEBAC /* NIP05DomainTimelineHeaderViewTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -5061,6 +5185,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5C4FA7FB2DC29C3800CE658C /* FollowPackView.swift in Sources */,
D7F360262CEBBD8B009D34DA /* PresentFullScreenItemNotify.swift in Sources */,
82D6FA9A2CD9820500C925F4 /* ShareViewController.swift in Sources */,
82D6FAA92CD99F7900C925F4 /* FbConstants.swift in Sources */,
@@ -5077,6 +5202,7 @@
82D6FAB42CD99F7900C925F4 /* Verifiable.swift in Sources */,
82D6FAB52CD99F7900C925F4 /* NativeObject.swift in Sources */,
82D6FAB62CD99F7900C925F4 /* String+extension.swift in Sources */,
3A515C552DF5371D002D3B34 /* TrustedNetworkButtonTipViewStyle.swift in Sources */,
82D6FAB72CD99F7900C925F4 /* FlatBufferObject.swift in Sources */,
82D6FAB82CD99F7900C925F4 /* Enum.swift in Sources */,
82D6FAB92CD99F7900C925F4 /* builder.c in Sources */,
@@ -5127,9 +5253,11 @@
82D6FAE42CD99F7900C925F4 /* FollowNotify.swift in Sources */,
82D6FAE52CD99F7900C925F4 /* LikedNotify.swift in Sources */,
82D6FAE62CD99F7900C925F4 /* LocalNotificationNotify.swift in Sources */,
5C4FA8012DC5119300CE658C /* FollowPackPreview.swift in Sources */,
82D6FAE72CD99F7900C925F4 /* LoginNotify.swift in Sources */,
82D6FAE82CD99F7900C925F4 /* LogoutNotify.swift in Sources */,
D706C5B12D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */,
3ACF94482DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */,
82D6FAE92CD99F7900C925F4 /* NewMutesNotify.swift in Sources */,
82D6FAEA2CD99F7900C925F4 /* NewUnmutesNotify.swift in Sources */,
82D6FAEB2CD99F7900C925F4 /* Notify.swift in Sources */,
@@ -5148,13 +5276,16 @@
82D6FAF62CD99F7900C925F4 /* ZappingNotify.swift in Sources */,
82D6FAF72CD99F7900C925F4 /* MuteNotify.swift in Sources */,
82D6FAF82CD99F7900C925F4 /* RelaysChangedNotify.swift in Sources */,
3A2BAC5B2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */,
82D6FAF92CD99F7900C925F4 /* MuteThreadNotify.swift in Sources */,
82D6FAFA2CD99F7900C925F4 /* UnmuteThreadNotify.swift in Sources */,
82D6FAFB2CD99F7900C925F4 /* ReconnectRelaysNotify.swift in Sources */,
3ACF94432DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */,
82D6FAFC2CD99F7900C925F4 /* PurpleAccountUpdateNotify.swift in Sources */,
82D6FAFD2CD99F7900C925F4 /* IdType.swift in Sources */,
82D6FAFE2CD99F7900C925F4 /* Pubkey.swift in Sources */,
82D6FAFF2CD99F7900C925F4 /* NoteId.swift in Sources */,
D7DF58332DFCF18D00E9AD28 /* SendPaymentView.swift in Sources */,
82D6FB002CD99F7900C925F4 /* Referenced.swift in Sources */,
5CB0172D2D42C76A00A9ED05 /* BalanceView.swift in Sources */,
82D6FB012CD99F7900C925F4 /* Block.swift in Sources */,
@@ -5176,6 +5307,7 @@
82D6FB0F2CD99F7900C925F4 /* DamusLogoGradient.swift in Sources */,
82D6FB102CD99F7900C925F4 /* DamusBackground.swift in Sources */,
82D6FB112CD99F7900C925F4 /* DamusLightGradient.swift in Sources */,
5C4FA8042DCAF80E00CE658C /* FollowPackTimeline.swift in Sources */,
82D6FB132CD99F7900C925F4 /* Shimmer.swift in Sources */,
82D6FB142CD99F7900C925F4 /* EndBlock.swift in Sources */,
82D6FB152CD99F7900C925F4 /* ImageCarousel.swift in Sources */,
@@ -5322,7 +5454,9 @@
82D6FB9B2CD99F7900C925F4 /* MutedThreadsManager.swift in Sources */,
82D6FB9C2CD99F7900C925F4 /* WalletModel.swift in Sources */,
82D6FB9D2CD99F7900C925F4 /* ZapButtonModel.swift in Sources */,
5C09FD142DF283D700823661 /* FollowPackModel.swift in Sources */,
82D6FB9E2CD99F7900C925F4 /* ContentFilters.swift in Sources */,
3A515C512DF4E100002D3B34 /* TrustedNetworkRepliesTip.swift in Sources */,
82D6FB9F2CD99F7900C925F4 /* DamusCacheManager.swift in Sources */,
82D6FBA02CD99F7900C925F4 /* NotificationsManager.swift in Sources */,
D755B28E2D3E7D8800BBEEFA /* NIP37Draft.swift in Sources */,
@@ -5373,13 +5507,14 @@
82D6FBCC2CD99F7900C925F4 /* CameraPreview.swift in Sources */,
82D6FBCD2CD99F7900C925F4 /* CameraController.swift in Sources */,
82D6FBCE2CD99F7900C925F4 /* OnboardingSuggestionsView.swift in Sources */,
3AA2F4EA2DF1467A00B18606 /* TrustedNetworkButtonTip.swift in Sources */,
82D6FBCF2CD99F7900C925F4 /* SuggestedUserView.swift in Sources */,
82D6FBD02CD99F7900C925F4 /* SuggestedUsersViewModel.swift in Sources */,
82D6FBD12CD99F7900C925F4 /* LoadScript.swift in Sources */,
82D6FBD52CD99F7900C925F4 /* ConnectWalletView.swift in Sources */,
82D6FBD62CD99F7900C925F4 /* WalletView.swift in Sources */,
82D6FBD72CD99F7900C925F4 /* NWCScannerView.swift in Sources */,
82D6FBD82CD99F7900C925F4 /* FriendsButton.swift in Sources */,
82D6FBD82CD99F7900C925F4 /* TrustedNetworkButton.swift in Sources */,
82D6FBD92CD99F7900C925F4 /* GradientFollowButton.swift in Sources */,
82D6FBDA2CD99F7900C925F4 /* AlbyButton.swift in Sources */,
82D6FBDC2CD99F7900C925F4 /* DamusVideoPlayerView.swift in Sources */,
@@ -5403,6 +5538,7 @@
82D6FBED2CD99F7900C925F4 /* MediaView.swift in Sources */,
82D6FBEE2CD99F7900C925F4 /* PurpleViewPrimitives.swift in Sources */,
82D6FBEF2CD99F7900C925F4 /* MarketingContentView.swift in Sources */,
3A2BAC602DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */,
82D6FBF02CD99F7900C925F4 /* LogoView.swift in Sources */,
82D6FBF12CD99F7900C925F4 /* IAPProductStateView.swift in Sources */,
82D6FBF22CD99F7900C925F4 /* PurpleBackdrop.swift in Sources */,
@@ -5421,6 +5557,7 @@
82D6FBFF2CD99F7900C925F4 /* NotificationItemView.swift in Sources */,
82D6FC002CD99F7900C925F4 /* ProfilePicturesView.swift in Sources */,
82D6FC012CD99F7900C925F4 /* DamusAppNotificationView.swift in Sources */,
3A92C1002DE16E9800CEEBAC /* FaviconCache.swift in Sources */,
82D6FC022CD99F7900C925F4 /* InnerTimelineView.swift in Sources */,
82D6FC032CD99F7900C925F4 /* PostingTimelineView.swift in Sources */,
82D6FC042CD99F7900C925F4 /* ZapsView.swift in Sources */,
@@ -5457,6 +5594,7 @@
82D6FC202CD99F7900C925F4 /* RelayType.swift in Sources */,
82D6FC212CD99F7900C925F4 /* SignalView.swift in Sources */,
82D6FC222CD99F7900C925F4 /* RelayPicView.swift in Sources */,
5C4FA7EC2DC29AE900CE658C /* FollowPackEvent.swift in Sources */,
82D6FC232CD99F7900C925F4 /* UserSearch.swift in Sources */,
82D6FC242CD99F7900C925F4 /* AddMuteItemView.swift in Sources */,
82D6FC252CD99F7900C925F4 /* MuteDurationMenu.swift in Sources */,
@@ -5477,6 +5615,7 @@
82D6FC342CD99F7900C925F4 /* BuilderEventView.swift in Sources */,
82D6FC352CD99F7900C925F4 /* EventProfile.swift in Sources */,
82D6FC362CD99F7900C925F4 /* EventMenu.swift in Sources */,
D7AAD0002E0387B800FB7699 /* LnurlAmountView.swift in Sources */,
82D6FC372CD99F7900C925F4 /* EventMutingContainerView.swift in Sources */,
82D6FC382CD99F7900C925F4 /* ZapEvent.swift in Sources */,
82D6FC392CD99F7900C925F4 /* TextEvent.swift in Sources */,
@@ -5575,6 +5714,7 @@
D73E5E2B2C6A97F4007EB227 /* PostNotify.swift in Sources */,
D73E5E2C2C6A97F4007EB227 /* PresentSheetNotify.swift in Sources */,
D73E5E2D2C6A97F4007EB227 /* ProfileUpdatedNotify.swift in Sources */,
3A515C522DF4E100002D3B34 /* TrustedNetworkRepliesTip.swift in Sources */,
D73E5E2E2C6A97F4007EB227 /* ReportNotify.swift in Sources */,
D73E5E2F2C6A97F4007EB227 /* ScrollToTopNotify.swift in Sources */,
D73E5E302C6A97F4007EB227 /* SwitchedTimelineNotify.swift in Sources */,
@@ -5627,6 +5767,7 @@
D73E5E602C6A97F4007EB227 /* ImageMetadata.swift in Sources */,
D73E5E612C6A97F4007EB227 /* ImageProcessing.swift in Sources */,
D73E5E622C6A97F4007EB227 /* BlurHashEncode.swift in Sources */,
5C09FD122DF283D700823661 /* FollowPackModel.swift in Sources */,
D73E5E632C6A97F4007EB227 /* BlurHashDecode.swift in Sources */,
D73E5F952C6AA753007EB227 /* FullScreenCarouselView.swift in Sources */,
D73E5E642C6A97F4007EB227 /* PostBox.swift in Sources */,
@@ -5636,14 +5777,17 @@
D73E5E682C6A97F4007EB227 /* VectorMath.swift in Sources */,
D73E5E692C6A97F4007EB227 /* RelayBootstrap.swift in Sources */,
D73E5E6A2C6A97F4007EB227 /* RelayModel.swift in Sources */,
3A2BAC5A2DD7E4C400EBB4CC /* NIP05DomainTimelineHeaderView.swift in Sources */,
D73E5E6B2C6A97F4007EB227 /* AnyCodable.swift in Sources */,
D73E5E6C2C6A97F4007EB227 /* AnyDecodable.swift in Sources */,
D73E5E6D2C6A97F4007EB227 /* AnyEncodable.swift in Sources */,
D73E5F782C6A9A5C007EB227 /* NdbNote+.swift in Sources */,
D73E5E6E2C6A97F4007EB227 /* NIPURLBuilder.swift in Sources */,
3ACF94472DAA006500971A4E /* NIP05DomainEventsModel.swift in Sources */,
D73E5E6F2C6A97F4007EB227 /* TimeAgo.swift in Sources */,
D73E5E702C6A97F4007EB227 /* Parser.swift in Sources */,
D73E5E722C6A97F4007EB227 /* LinkView.swift in Sources */,
5C4FA7EE2DC29AE900CE658C /* FollowPackEvent.swift in Sources */,
D73E5F922C6AA720007EB227 /* QRCodeView.swift in Sources */,
D73E5E742C6A97F4007EB227 /* Lists.swift in Sources */,
D73E5E752C6A97F4007EB227 /* CoreSVG.swift in Sources */,
@@ -5661,6 +5805,7 @@
D73E5E802C6A97F4007EB227 /* CredentialHandler.swift in Sources */,
D73E5E812C6A97F4007EB227 /* KeyboardVisible.swift in Sources */,
D73E5E832C6A97F4007EB227 /* AVPlayer+Additions.swift in Sources */,
5C4FA7FC2DC29C3800CE658C /* FollowPackView.swift in Sources */,
D73E5E842C6A97F4007EB227 /* Zaps+.swift in Sources */,
D73E5E852C6A97F4007EB227 /* WalletConnect+.swift in Sources */,
D73E5E862C6A97F4007EB227 /* DamusPurpleNotificationManagement.swift in Sources */,
@@ -5679,6 +5824,7 @@
D73E5E922C6A97F4007EB227 /* EventGroup.swift in Sources */,
D73E5E932C6A97F4007EB227 /* ZapGroup.swift in Sources */,
D73E5E942C6A97F4007EB227 /* NotificationStatusModel.swift in Sources */,
3A515C542DF5371D002D3B34 /* TrustedNetworkButtonTipViewStyle.swift in Sources */,
D73E5E952C6A97F4007EB227 /* ThreadModel.swift in Sources */,
D73E5E962C6A97F4007EB227 /* ReplyMap.swift in Sources */,
D73E5E972C6A97F4007EB227 /* ProfileModel.swift in Sources */,
@@ -5727,6 +5873,7 @@
D73E5EB92C6A97F4007EB227 /* RelayLog.swift in Sources */,
D73E5EBA2C6A97F4007EB227 /* NostrFilter.swift in Sources */,
D73E5EBB2C6A97F4007EB227 /* Nip98HTTPAuth.swift in Sources */,
3A92C0FF2DE16E9800CEEBAC /* FaviconCache.swift in Sources */,
D73E5EBC2C6A97F4007EB227 /* Relay.swift in Sources */,
D73E5EBD2C6A97F4007EB227 /* NostrRequest.swift in Sources */,
5CB017222D2D985E00A9ED05 /* CoinosButton.swift in Sources */,
@@ -5736,6 +5883,7 @@
D73E5EC02C6A97F4007EB227 /* NostrEvent+.swift in Sources */,
D73E5EC12C6A97F4007EB227 /* NIP98AuthenticatedRequest.swift in Sources */,
D73E5EC22C6A97F4007EB227 /* NostrAuth.swift in Sources */,
3AA2F4E92DF1467A00B18606 /* TrustedNetworkButtonTip.swift in Sources */,
D73E5EC42C6A97F4007EB227 /* ReplyQuoteView.swift in Sources */,
D73E5EC62C6A97F4007EB227 /* ChatBubbleView.swift in Sources */,
D73E5EC72C6A97F4007EB227 /* VisibilityTracker.swift in Sources */,
@@ -5747,7 +5895,7 @@
D73E5ED22C6A97F4007EB227 /* WalletView.swift in Sources */,
D73E5ED32C6A97F4007EB227 /* NWCScannerView.swift in Sources */,
D74E64132DC95CC7004C7892 /* HumanReadableErrors.swift in Sources */,
D73E5ED42C6A97F4007EB227 /* FriendsButton.swift in Sources */,
D73E5ED42C6A97F4007EB227 /* TrustedNetworkButton.swift in Sources */,
D73E5ED52C6A97F4007EB227 /* GradientFollowButton.swift in Sources */,
D73E5ED62C6A97F4007EB227 /* AlbyButton.swift in Sources */,
D73E5ED82C6A97F4007EB227 /* DamusVideoPlayerView.swift in Sources */,
@@ -5796,6 +5944,7 @@
D73E5EFF2C6A97F4007EB227 /* ZapsView.swift in Sources */,
D73E5F002C6A97F4007EB227 /* CustomizeZapView.swift in Sources */,
D73E5F012C6A97F4007EB227 /* ZapTypePicker.swift in Sources */,
5C4FA8052DCAF80E00CE658C /* FollowPackTimeline.swift in Sources */,
D73E5F022C6A97F4007EB227 /* ZapUserView.swift in Sources */,
D73E5F032C6A97F4007EB227 /* ProfileZapLinkView.swift in Sources */,
D7DB930C2D69486700DA1EE5 /* NIP65.swift in Sources */,
@@ -5815,6 +5964,7 @@
D73E5F0F2C6A97F4007EB227 /* CondensedProfilePicturesView.swift in Sources */,
D73E5F102C6A97F4007EB227 /* ProfileEditButton.swift in Sources */,
D73E5F112C6A97F4007EB227 /* RelayPaidDetail.swift in Sources */,
D7AACFFF2E0387B800FB7699 /* LnurlAmountView.swift in Sources */,
D73E5F122C6A97F4007EB227 /* RelayAuthenticationDetail.swift in Sources */,
D73E5F132C6A97F4007EB227 /* RelaySoftwareDetail.swift in Sources */,
D73E5F142C6A97F4007EB227 /* RelayAdminDetail.swift in Sources */,
@@ -5825,6 +5975,7 @@
D73E5F192C6A97F4007EB227 /* RelayToggle.swift in Sources */,
D73E5F1A2C6A97F4007EB227 /* RelayStatusView.swift in Sources */,
D73E5F1B2C6A97F4007EB227 /* RelayType.swift in Sources */,
3A2BAC5F2DE02E8600EBB4CC /* NIP05DomainPubkeysView.swift in Sources */,
D73E5F1C2C6A97F4007EB227 /* SignalView.swift in Sources */,
D73E5F1D2C6A97F4007EB227 /* RelayPicView.swift in Sources */,
D73E5F1E2C6A97F4007EB227 /* UserSearch.swift in Sources */,
@@ -5891,6 +6042,7 @@
D73E5F512C6A97F5007EB227 /* EventDetailView.swift in Sources */,
D73E5F522C6A97F5007EB227 /* FollowButtonView.swift in Sources */,
D73E5F532C6A97F5007EB227 /* FollowingView.swift in Sources */,
D7DF58322DFCF18D00E9AD28 /* SendPaymentView.swift in Sources */,
D73E5F542C6A97F5007EB227 /* LoginView.swift in Sources */,
D73E5F552C6A97F5007EB227 /* QRScanNSECView.swift in Sources */,
D73E5F562C6A97F5007EB227 /* NoteContentView.swift in Sources */,
@@ -5933,6 +6085,7 @@
D703D7552C670A3700A400EA /* DamusUserDefaults.swift in Sources */,
D703D7A32C670E1D00A400EA /* nostr_bech32.c in Sources */,
D703D7992C670DF900A400EA /* sha256.c in Sources */,
5C4FA8002DC5119300CE658C /* FollowPackPreview.swift in Sources */,
D703D7972C670DED00A400EA /* wasm.c in Sources */,
5C8498042D5D150000F74FEB /* ZapExplainer.swift in Sources */,
D703D7842C670C4700A400EA /* SequenceUtils.swift in Sources */,
@@ -5957,6 +6110,7 @@
D73E5E1B2C6A9672007EB227 /* LikeCounter.swift in Sources */,
D703D7A92C670E5A00A400EA /* refmap.c in Sources */,
D703D77B2C670BF000A400EA /* TableVerifier.swift in Sources */,
3ACF94442DA9FCAB00971A4E /* NIP05DomainTimelineView.swift in Sources */,
D703D76D2C670B4500A400EA /* ZapDataModel.swift in Sources */,
D703D79D2C670E0700A400EA /* node_id.c in Sources */,
D703D79B2C670E0000A400EA /* bech32_util.c in Sources */,
@@ -6939,6 +7093,14 @@
minimumVersion = 0.2.0;
};
};
3ACF94362DA9A52F00971A4E /* XCRemoteSwiftPackageReference "FaviconFinder" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/will-lumley/FaviconFinder.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 5.1.4;
};
};
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/onevcat/Kingfisher";
@@ -7019,6 +7181,21 @@
package = 3A0A30B92C21397A00F8C9BC /* XCRemoteSwiftPackageReference "EmojiPicker" */;
productName = EmojiPicker;
};
3ACF94372DA9A52F00971A4E /* FaviconFinder */ = {
isa = XCSwiftPackageProductDependency;
package = 3ACF94362DA9A52F00971A4E /* XCRemoteSwiftPackageReference "FaviconFinder" */;
productName = FaviconFinder;
};
3ACF943D2DA9B10800971A4E /* FaviconFinder */ = {
isa = XCSwiftPackageProductDependency;
package = 3ACF94362DA9A52F00971A4E /* XCRemoteSwiftPackageReference "FaviconFinder" */;
productName = FaviconFinder;
};
3ACF943F2DA9B11200971A4E /* FaviconFinder */ = {
isa = XCSwiftPackageProductDependency;
package = 3ACF94362DA9A52F00971A4E /* XCRemoteSwiftPackageReference "FaviconFinder" */;
productName = FaviconFinder;
};
4C06670328FC7EC500038D2A /* Kingfisher */ = {
isa = XCSwiftPackageProductDependency;
package = 4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */;
@@ -1,5 +1,5 @@
{
"originHash" : "06318d35ee2e6bd681b95591e67da33a9461b48a3c652e58bd9d1a6f0d82bdac",
"originHash" : "1fc7e0b44329ba72cd285eeb022b5b92582cd01586b920d243cb0485c2e69dcc",
"pins" : [
{
"identity" : "codescanner",
@@ -35,6 +35,15 @@
"version" : "0.2.0"
}
},
{
"identity" : "faviconfinder",
"kind" : "remoteSourceControl",
"location" : "https://github.com/will-lumley/FaviconFinder.git",
"state" : {
"revision" : "9279f4371f4877ca302ba3bf1015f3f58ae4a56c",
"version" : "5.1.4"
}
},
{
"identity" : "gsplayer",
"kind" : "remoteSourceControl",
@@ -105,6 +114,15 @@
"version" : "0.1.2"
}
},
{
"identity" : "swiftsoup",
"kind" : "remoteSourceControl",
"location" : "https://github.com/scinfu/SwiftSoup.git",
"state" : {
"revision" : "bba848db50462894e7fc0891d018dfecad4ef11e",
"version" : "2.8.7"
}
},
{
"identity" : "swiftycrop",
"kind" : "remoteSourceControl",
+38 -23
View File
@@ -5,27 +5,27 @@
// Created by William Casarin on 2023-01-11.
//
import FaviconFinder
import Kingfisher
import SwiftUI
struct NIP05Badge: View {
let nip05: NIP05
let pubkey: Pubkey
let contacts: Contacts
let damus_state: DamusState
let show_domain: Bool
let profiles: Profiles
let nip05_domain_favicon: FaviconURL?
@Environment(\.openURL) var openURL
init(nip05: NIP05, pubkey: Pubkey, contacts: Contacts, show_domain: Bool, profiles: Profiles) {
init(nip05: NIP05, pubkey: Pubkey, damus_state: DamusState, show_domain: Bool, nip05_domain_favicon: FaviconURL?) {
self.nip05 = nip05
self.pubkey = pubkey
self.contacts = contacts
self.damus_state = damus_state
self.show_domain = show_domain
self.profiles = profiles
self.nip05_domain_favicon = nip05_domain_favicon
}
var nip05_color: Bool {
return use_nip05_color(pubkey: pubkey, contacts: contacts)
return use_nip05_color(pubkey: pubkey, contacts: damus_state.contacts)
}
var Seal: some View {
@@ -44,8 +44,23 @@ struct NIP05Badge: View {
}
}
var domainBadge: some View {
Group {
if let nip05_domain_favicon {
KFImage(nip05_domain_favicon.source)
.imageContext(.favicon, disable_animation: true)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 18, height: 18)
.clipped()
} else {
EmptyView()
}
}
}
var username_matches_nip05: Bool {
guard let name = profiles.lookup(id: pubkey)?.map({ p in p?.name }).value
guard let name = damus_state.profiles.lookup(id: pubkey)?.map({ p in p?.name }).value
else {
return false
}
@@ -65,14 +80,18 @@ struct NIP05Badge: View {
HStack(spacing: 2) {
Seal
if show_domain {
Text(nip05_string)
.nip05_colorized(gradient: nip05_color)
.onTapGesture {
if let nip5url = nip05.siteUrl {
openURL(nip5url)
}
}
Group {
if show_domain {
Text(nip05_string)
.nip05_colorized(gradient: nip05_color)
}
if nip05_domain_favicon != nil {
domainBadge
}
}
.onTapGesture {
damus_state.nav.push(route: Route.NIP05DomainEvents(events: NIP05DomainEventsModel(state: damus_state, domain: nip05.host), nip05_domain_favicon: nip05_domain_favicon))
}
}
@@ -98,13 +117,9 @@ struct NIP05Badge_Previews: PreviewProvider {
static var previews: some View {
let test_state = test_damus_state
VStack {
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, damus_state: test_state, show_domain: true, nip05_domain_favicon: nil)
NIP05Badge(nip05: NIP05(username: "_", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: test_state.contacts, show_domain: true, profiles: test_state.profiles)
NIP05Badge(nip05: NIP05(username: "jb55", host: "jb55.com"), pubkey: test_state.pubkey, contacts: Contacts(our_pubkey: test_pubkey), show_domain: true, profiles: test_state.profiles)
NIP05Badge(nip05: NIP05(username: "_", host: "jb55.com"), pubkey: test_state.pubkey, damus_state: test_state, show_domain: true, nip05_domain_favicon: nil)
}
}
}
+19 -2
View File
@@ -9,6 +9,7 @@ import SwiftUI
import AVKit
import MediaPlayer
import EmojiPicker
import TipKit
struct ZapSheet {
let target: ZapTarget
@@ -178,7 +179,7 @@ struct ContentView: View {
NotificationsView(state: damus, notifications: home.notifications, subtitle: $menu_subtitle)
case .dms:
DirectMessagesView(damus_state: damus_state!, model: damus_state!.dms, settings: damus_state!.settings)
DirectMessagesView(damus_state: damus_state!, model: damus_state!.dms, settings: damus_state!.settings, subtitle: $menu_subtitle)
}
}
.background(DamusColors.adaptableWhite)
@@ -686,7 +687,8 @@ struct ContentView: View {
video: DamusVideoCoordinator(),
ndb: ndb,
quote_reposts: .init(our_pubkey: pubkey),
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
emoji_provider: DefaultEmojiProvider(showAllVariations: true),
favicon_cache: FaviconCache()
)
home.damus_state = self.damus_state!
@@ -704,6 +706,21 @@ struct ContentView: View {
damus_state.nostrNetwork.pool.register_handler(sub_id: sub_id, handler: home.handle_event)
damus_state.nostrNetwork.connect()
if #available(iOS 17, *) {
if damus_state.settings.developer_mode && damus_state.settings.reset_tips_on_launch {
do {
try Tips.resetDatastore()
} catch {
Log.error("Failed to reset tips datastore: %s", for: .tips, error.localizedDescription)
}
}
do {
try Tips.configure()
} catch {
Log.error("Failed to configure tips: %s", for: .tips, error.localizedDescription)
}
}
}
func music_changed(_ state: MusicState) {
+4
View File
@@ -38,6 +38,10 @@ class Contacts {
return friends
}
func get_friend_of_friends_list() -> Set<Pubkey> {
return friend_of_friends
}
func get_followed_hashtags() -> Set<String> {
guard let ev = self.event else { return Set() }
return Set(ev.referenced_hashtags.map({ $0.hashtag }))
+3
View File
@@ -13,6 +13,7 @@ enum FilterState : Int {
case posts = 0
case posts_and_replies = 1
case conversations = 2
case follow_list = 3
func filter(ev: NostrEvent) -> Bool {
switch self {
@@ -22,6 +23,8 @@ enum FilterState : Int {
return true
case .conversations:
return true
case .follow_list:
return ev.known_kind == .follow_list
}
}
}
+8 -4
View File
@@ -36,9 +36,10 @@ class DamusState: HeadlessDamusState {
var purple: DamusPurple
var push_notification_client: PushNotificationClient
let emoji_provider: EmojiProvider
let favicon_cache: FaviconCache
private(set) var nostrNetwork: NostrNetworkManager
init(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, replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider) {
init(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, replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: DamusVideoCoordinator, ndb: Ndb, purple: DamusPurple? = nil, quote_reposts: EventCounter, emoji_provider: EmojiProvider, favicon_cache: FaviconCache) {
self.keypair = keypair
self.likes = likes
self.boosts = boosts
@@ -68,7 +69,8 @@ class DamusState: HeadlessDamusState {
self.quote_reposts = quote_reposts
self.push_notification_client = PushNotificationClient(keypair: keypair, settings: settings)
self.emoji_provider = emoji_provider
self.favicon_cache = FaviconCache()
let networkManagerDelegate = NostrNetworkManagerDelegate(settings: settings, contacts: contacts, ndb: ndb, keypair: keypair, relayModelCache: relay_model_cache, relayFilters: relay_filters)
self.nostrNetwork = NostrNetworkManager(delegate: networkManagerDelegate)
}
@@ -126,7 +128,8 @@ class DamusState: HeadlessDamusState {
video: DamusVideoCoordinator(),
ndb: ndb,
quote_reposts: .init(our_pubkey: pubkey),
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
emoji_provider: DefaultEmojiProvider(showAllVariations: true),
favicon_cache: FaviconCache()
)
}
@@ -194,7 +197,8 @@ class DamusState: HeadlessDamusState {
video: DamusVideoCoordinator(),
ndb: .empty,
quote_reposts: .init(our_pubkey: empty_pub),
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
emoji_provider: DefaultEmojiProvider(showAllVariations: true),
favicon_cache: FaviconCache()
)
}
}
+39
View File
@@ -0,0 +1,39 @@
//
// FollowPackEvent.swift
// damus
//
// Created by eric on 4/30/25.
//
import Foundation
struct FollowPackEvent {
let event: NostrEvent
var title: String? = nil
var uuid: String? = nil
var image: URL? = nil
var description: String? = nil
var publicKeys: [Pubkey] = []
static func parse(from ev: NostrEvent) -> FollowPackEvent {
var followlist = FollowPackEvent(event: ev)
for tag in ev.tags {
guard tag.count >= 2 else { continue }
switch tag[0].string() {
case "title": followlist.title = tag[1].string()
case "d": followlist.uuid = tag[1].string()
case "image": followlist.image = URL(string: tag[1].string())
case "description": followlist.description = tag[1].string()
case "p":
followlist.publicKeys.append(Pubkey(Data(hex: tag[1].string())))
default:
break
}
}
return followlist
}
}
+77
View File
@@ -0,0 +1,77 @@
//
// FollowPackModel.swift
// damus
//
// Created by eric on 6/5/25.
//
import Foundation
class FollowPackModel: ObservableObject {
var events: EventHolder
@Published var loading: Bool = false
let damus_state: DamusState
let subid = UUID().description
let limit: UInt32 = 500
init(damus_state: DamusState) {
self.damus_state = damus_state
self.events = EventHolder(on_queue: { ev in
preload_events(state: damus_state, events: [ev])
})
}
func subscribe(follow_pack_users: [Pubkey]) {
loading = true
let to_relays = determine_to_relays(pool: damus_state.nostrNetwork.pool, filters: damus_state.relay_filters)
var filter = NostrFilter(kinds: [.text, .chat])
filter.until = UInt32(Date.now.timeIntervalSince1970)
filter.authors = follow_pack_users
filter.limit = 500
damus_state.nostrNetwork.pool.subscribe(sub_id: subid, filters: [filter], handler: handle_event, to: to_relays)
}
func unsubscribe(to: RelayURL? = nil) {
loading = false
damus_state.nostrNetwork.pool.unsubscribe(sub_id: subid, to: to.map { [$0] })
}
func handle_event(relay_id: RelayURL, conn_ev: NostrConnectionEvent) {
guard case .nostr_event(let event) = conn_ev else {
return
}
switch event {
case .event(let sub_id, let ev):
guard sub_id == self.subid else {
return
}
if ev.is_textlike && should_show_event(state: damus_state, ev: ev) && !ev.is_reply()
{
if self.events.insert(ev) {
self.objectWillChange.send()
}
}
case .notice(let msg):
print("follow pack notice: \(msg)")
case .ok:
break
case .eose(let sub_id):
loading = false
if sub_id == self.subid {
unsubscribe(to: relay_id)
guard let txn = NdbTxn(ndb: damus_state.ndb) else { return }
}
break
case .auth:
break
}
}
}
+2 -2
View File
@@ -35,9 +35,9 @@ enum FriendFilter: String, StringCodable {
func description() -> String {
switch self {
case .all:
return NSLocalizedString("All", comment: "Human-readable short description of the 'friends filter' when it is set to 'all'")
return NSLocalizedString("All", comment: "Human-readable short description of the 'trusted network filter' when it is disabled, and therefore is showing all content.")
case .friends_of_friends:
return NSLocalizedString("Friends of friends", comment: "Human-readable short description of the 'friends filter' when it is set to 'friends-of-friends'")
return NSLocalizedString("Trusted Network", comment: "Human-readable short description of the 'trusted network filter' when it is enabled, and therefore showing content from only the trusted network.")
}
}
}
+5 -15
View File
@@ -227,6 +227,8 @@ class HomeModel: ContactsDelegate {
break
case .relay_list:
break // This will be handled by `UserRelayListManager`
case .follow_list:
break
}
}
@@ -290,24 +292,12 @@ class HomeModel: ContactsDelegate {
Log.debug("HomeModel: got NWC response, %s not found in the postbox, nothing to remove [%s]", for: .nwc, resp.req_id.hex(), relay.absoluteString)
}
damus_state.wallet.handle_nwc_response(response: resp) // This can handle success or error cases
guard resp.response.error == nil else {
Log.error("HomeModel: NWC wallet raised an error: %s", for: .nwc, String(describing: resp.response))
WalletConnect.handle_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
if let humanReadableError = resp.response.error?.humanReadableError {
present_sheet(.error(humanReadableError))
}
return
}
if resp.response.result_type == .list_transactions {
Log.info("Received NWC transaction list from %s", for: .nwc, relay.absoluteString)
damus_state.wallet.handle_nwc_response(response: resp)
return
}
if resp.response.result_type == .get_balance {
Log.info("Received NWC balance information from %s", for: .nwc, relay.absoluteString)
damus_state.wallet.handle_nwc_response(response: resp)
return
}
+22
View File
@@ -163,6 +163,10 @@ struct LightningInvoice<T> {
let payment_hash: Data
let created_at: UInt64
var abbreviated: String {
return self.string.prefix(8) + "" + self.string.suffix(8)
}
var description_string: String {
switch description {
case .description(let string):
@@ -171,6 +175,17 @@ struct LightningInvoice<T> {
return ""
}
}
static func from(string: String) -> Invoice? {
// This feels a bit hacky at first, but it is actually clean
// because it reuses the same well-tested parsing logic as the rest of the app,
// avoiding code duplication and utilizing the guarantees acquired from age and testing.
// We could also use the C function `parse_invoice`, but it requires extra C bridging logic.
// NDBTODO: This may need updating on the nostrdb upgrade.
let parsedBlocks = parse_note_content(content: .content(string,nil)).blocks
guard parsedBlocks.count == 1 else { return nil }
return parsedBlocks[0].asInvoice
}
}
func maybe_pointee<T>(_ p: UnsafeMutablePointer<T>!) -> T? {
@@ -192,6 +207,13 @@ enum Amount: Equatable {
return format_msats(amt)
}
}
func amount_sats() -> Int64? {
switch self {
case .any: nil
case .specific(let amount): amount / 1000
}
}
}
func format_msats_abbrev(_ msats: Int64) -> String {
+97
View File
@@ -0,0 +1,97 @@
//
// NIP05DomainEventsModel.swift
// damus
//
// Created by Terry Yiu on 4/11/25.
//
import FaviconFinder
import Foundation
class NIP05DomainEventsModel: ObservableObject {
let state: DamusState
var events: EventHolder
@Published var loading: Bool = false
let domain: String
var filter: NostrFilter
let sub_id = UUID().description
let profiles_subid = UUID().description
let limit: UInt32 = 500
init(state: DamusState, domain: String) {
self.state = state
self.domain = domain
self.events = EventHolder(on_queue: { ev in
preload_events(state: state, events: [ev])
})
self.filter = NostrFilter()
}
@MainActor func subscribe() {
filter.limit = self.limit
filter.kinds = [.text, .longform, .highlight]
var authors = Set<Pubkey>()
for pubkey in state.contacts.get_friend_of_friends_list() {
let profile_txn = state.profiles.lookup(id: pubkey)
guard let profile = profile_txn?.unsafeUnownedValue,
let nip05_str = profile.nip05,
let nip05 = NIP05.parse(nip05_str),
nip05.host.caseInsensitiveCompare(domain) == .orderedSame else {
continue
}
authors.insert(pubkey)
}
if authors.isEmpty {
return
}
filter.authors = Array(authors)
print("subscribing to notes from friends of friends with '\(domain)' NIP-05 domain with sub_id \(sub_id)")
state.nostrNetwork.pool.register_handler(sub_id: sub_id, handler: handle_event)
loading = true
state.nostrNetwork.pool.send(.subscribe(.init(filters: [filter], sub_id: sub_id)))
}
func unsubscribe() {
state.nostrNetwork.pool.unsubscribe(sub_id: sub_id)
loading = false
print("unsubscribing from notes from friends of friends with '\(domain)' NIP-05 domain with sub_id \(sub_id)")
}
func add_event(_ ev: NostrEvent) {
if !event_matches_filter(ev, filter: filter) {
return
}
guard should_show_event(state: state, ev: ev) else {
return
}
if self.events.insert(ev) {
objectWillChange.send()
}
}
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
let (sub_id, done) = handle_subid_event(pool: state.nostrNetwork.pool, relay_id: relay_id, ev: ev) { sub_id, ev in
if sub_id == self.sub_id && ev.is_textlike && ev.should_show_event {
self.add_event(ev)
}
}
guard done else {
return
}
self.loading = false
if sub_id == self.sub_id {
guard let txn = NdbTxn(ndb: state.ndb) else { return }
load_profiles(context: "search", profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(self.events.all_events), damus_state: state, txn: txn)
}
}
}
+23 -2
View File
@@ -117,6 +117,10 @@ func render_blocks(blocks bs: Blocks, profiles: Profiles, can_hide_last_previewa
}
hide_text_index = i
} else if case .text(let txt) = block, txt.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
// We should hide whitespace at the end sequence.
hide_text_index = i
} else if case .hashtag = block {
// We should keep hashtags at the end sequence but hide all the other previewables around it.
hide_text_index = i
} else {
break
@@ -149,7 +153,16 @@ func render_blocks(blocks bs: Blocks, profiles: Profiles, can_hide_last_previewa
// No need to show the text representation of the block if the only previewables are the sequence of them
// found at the end of the content.
// This is to save unnecessary use of screen space.
// The only exception is that if there are hashtags embedded in the end sequence, which is not uncommon,
// then we still want to show those hashtags but hide everything else that is previewable in the end sequence.
if ind >= hide_text_index {
if case .text(let txt) = block, txt.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
if case .hashtag = blocks[safe: ind+1] {
return str + CompatibleText(stringLiteral: reduce_text_block(ind: ind, hide_text_index: -1, txt: txt))
}
} else if case .hashtag(let htag) = block {
return str + hashtag_str(htag)
}
return str
}
}
@@ -257,12 +270,20 @@ func mention_str(_ m: Mention<MentionRef>, profiles: Profiles) -> CompatibleText
// trim suffix whitespace and newlines
func trim_suffix(_ str: String) -> String {
return str.replacingOccurrences(of: "\\s+$", with: "", options: .regularExpression)
var result = str
while result.last?.isWhitespace == true {
result.removeLast()
}
return result
}
// trim prefix whitespace and newlines
func trim_prefix(_ str: String) -> String {
return str.replacingOccurrences(of: "^\\s+", with: "", options: .regularExpression)
var result = str
while result.first?.isWhitespace == true {
result.removeFirst()
}
return result
}
struct LongformContent {
+8 -2
View File
@@ -1,4 +1,3 @@
//
// SearchHomeModel.swift
// damus
//
@@ -16,6 +15,7 @@ class SearchHomeModel: ObservableObject {
var seen_pubkey: Set<Pubkey> = Set()
let damus_state: DamusState
let base_subid = UUID().description
let follow_pack_subid = UUID().description
let profiles_subid = UUID().description
let limit: UInt32 = 500
//let multiple_events_per_pubkey: Bool = false
@@ -42,12 +42,18 @@ class SearchHomeModel: ObservableObject {
func subscribe() {
loading = true
let to_relays = determine_to_relays(pool: damus_state.nostrNetwork.pool, filters: damus_state.relay_filters)
var follow_list_filter = NostrFilter(kinds: [.follow_list])
follow_list_filter.until = UInt32(Date.now.timeIntervalSince1970)
damus_state.nostrNetwork.pool.subscribe(sub_id: base_subid, filters: [get_base_filter()], handler: handle_event, to: to_relays)
damus_state.nostrNetwork.pool.subscribe(sub_id: follow_pack_subid, filters: [follow_list_filter], handler: handle_event, to: to_relays)
}
func unsubscribe(to: RelayURL? = nil) {
loading = false
damus_state.nostrNetwork.pool.unsubscribe(sub_id: base_subid, to: to.map { [$0] })
damus_state.nostrNetwork.pool.unsubscribe(sub_id: follow_pack_subid, to: to.map { [$0] })
}
func handle_event(relay_id: RelayURL, conn_ev: NostrConnectionEvent) {
@@ -57,7 +63,7 @@ class SearchHomeModel: ObservableObject {
switch event {
case .event(let sub_id, let ev):
guard sub_id == self.base_subid || sub_id == self.profiles_subid else {
guard sub_id == self.base_subid || sub_id == self.profiles_subid || sub_id == self.follow_pack_subid else {
return
}
if ev.is_textlike && should_show_event(state: damus_state, ev: ev) && !ev.is_reply()
+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, .highlight, .follow_list]
//likes_filter.ids = ref_events.referenced_ids!
+11 -2
View File
@@ -115,7 +115,10 @@ class UserSettingsStore: ObservableObject {
@Setting(key: "dismiss_wallet_high_balance_warning", default_value: false)
var dismiss_wallet_high_balance_warning: Bool
@Setting(key: "hide_wallet_balance", default_value: false)
var hide_wallet_balance: Bool
@Setting(key: "left_handed", default_value: false)
var left_handed: Bool
@@ -124,7 +127,13 @@ class UserSettingsStore: ObservableObject {
@Setting(key: "media_previews", default_value: true)
var media_previews: Bool
@Setting(key: "show_trusted_replies_first", default_value: true)
var show_trusted_replies_first: Bool
@Setting(key: "reset_tips_on_launch", default_value: false)
var reset_tips_on_launch: Bool
@Setting(key: "hide_nsfw_tagged_content", default_value: false)
var hide_nsfw_tagged_content: Bool
+50 -4
View File
@@ -27,6 +27,11 @@ class WalletModel: ObservableObject {
@Published private(set) var connect_state: WalletConnectState
/// A dictionary listing continuations waiting for a response for each request note id.
///
/// Please see the `waitForResponse` method for context.
private var continuations: [NoteId: CheckedContinuation<WalletConnect.Response.Result, any Error>] = [:]
init(state: WalletConnectState, settings: UserSettingsStore) {
self.connect_state = state
self.previous_state = .none
@@ -75,12 +80,16 @@ class WalletModel: ObservableObject {
///
/// - Parameter response: The NWC response received from the network
func handle_nwc_response(response: WalletConnect.FullWalletResponse) {
switch response.response.result {
if let error = response.response.error {
self.resume(request: response.req_id, throwing: error)
return
}
guard let result = response.response.result else { return }
self.resume(request: response.req_id, with: result)
switch result {
case .get_balance(let balanceResp):
self.balance = balanceResp.balance / 1000
case .none:
return
case .some(.pay_invoice(_)):
case .pay_invoice(_):
return
case .list_transactions(let transactionsResp):
self.transactions = transactionsResp.transactions
@@ -91,4 +100,41 @@ class WalletModel: ObservableObject {
self.transactions = nil
self.balance = nil
}
// MARK: - Async wallet response waiting mechanism
func waitForResponse(for requestId: NoteId, timeout: Duration = .seconds(10)) async throws -> WalletConnect.Response.Result {
return try await withCheckedThrowingContinuation({ continuation in
self.continuations[requestId] = continuation
let timeoutTask = Task {
try? await Task.sleep(for: timeout)
self.resume(request: requestId, throwing: WaitError.timeout) // Must resume the continuation exactly once even if there is no response
}
})
}
private func resume(request requestId: NoteId, with result: WalletConnect.Response.Result) {
continuations[requestId]?.resume(returning: result)
continuations[requestId] = nil // Never resume a continuation twice
}
private func resume(request requestId: NoteId, throwing error: any Error) {
if let continuation = continuations[requestId] {
continuation.resume(throwing: error)
continuations[requestId] = nil // Never resume a continuation twice
return // Error will be handled by the listener, no need for the generic error sheet
}
// No listeners to catch the error, show generic error sheet
if let error = error as? WalletConnect.WalletResponseErr,
let humanReadableError = error.humanReadableError {
present_sheet(.error(humanReadableError))
}
}
enum WaitError: Error {
case timeout
}
}
+1
View File
@@ -30,4 +30,5 @@ enum NostrKind: UInt32, Codable {
case nwc_response = 23195
case http_auth = 27235
case status = 30315
case follow_list = 39089
}
+1
View File
@@ -35,6 +35,7 @@ class Profiles {
@MainActor
private var profiles: [Pubkey: ProfileData] = [:]
// Map of validated NIP-05 address to pubkey.
@MainActor
var nip05_pubkey: [String: Pubkey] = [:]
+2 -1
View File
@@ -106,7 +106,8 @@ var test_damus_state: DamusState = ({
video: .init(),
ndb: ndb,
quote_reposts: .init(our_pubkey: our_pubkey),
emoji_provider: DefaultEmojiProvider(showAllVariations: true)
emoji_provider: DefaultEmojiProvider(showAllVariations: true),
favicon_cache: .init()
)
/*
+10
View File
@@ -202,3 +202,13 @@ extension Block {
}
}
}
extension Block {
var asInvoice: Invoice? {
switch self {
case .invoice(let invoice):
return invoice
default:
return nil
}
}
}
+16 -11
View File
@@ -29,15 +29,15 @@ extension KFOptionSetter {
options.onlyLoadFirstFrame = disable_animation
switch imageContext {
case .pfp:
options.diskCacheExpiration = .days(60)
break
case .banner:
options.diskCacheExpiration = .days(5)
break
case .note:
options.diskCacheExpiration = .days(1)
break
case .pfp, .favicon:
options.diskCacheExpiration = .days(60)
break
case .banner:
options.diskCacheExpiration = .days(5)
break
case .note:
options.diskCacheExpiration = .days(1)
break
}
return self
@@ -82,11 +82,14 @@ enum ImageContext {
case pfp
case banner
case note
case favicon
func maxMebibyteSize() -> Int {
switch self {
case .favicon:
return 512_000 // 500KiB
case .pfp:
return 5_242_880 // 5Mib
return 5_242_880 // 5MiB
case .banner, .note:
return 20_971_520 // 20MiB
}
@@ -94,6 +97,8 @@ enum ImageContext {
func downsampleSize() -> CGSize {
switch self {
case .favicon:
return CGSize(width: 18, height: 18)
case .pfp:
return CGSize(width: 200, height: 200)
case .banner:
+41
View File
@@ -0,0 +1,41 @@
//
// FaviconCache.swift
// damus
//
// Created by Terry Yiu on 5/23/25.
//
import Foundation
import FaviconFinder
class FaviconCache {
private var nip05DomainFavicons: [String: [FaviconURL]] = [:]
@MainActor
func lookup(_ domain: String) async -> [FaviconURL] {
let lowercasedDomain = domain.lowercased()
if let faviconURLs = nip05DomainFavicons[lowercasedDomain] {
return faviconURLs
}
guard let siteURL = URL(string: "https://\(lowercasedDomain)"),
let faviconURLs = try? await FaviconFinder(
url: siteURL,
configuration: .init(
preferredSource: .ico, // Prefer using common favicon .ico filenames at root level to avoid scraping HTML when possible.
preferences: [
.html: FaviconFormatType.appleTouchIcon.rawValue,
.ico: "favicon.ico",
.webApplicationManifestFile: FaviconFormatType.launcherIcon4x.rawValue
]
)
).fetchFaviconURLs()
else {
return []
}
nip05DomainFavicons[lowercasedDomain] = faviconURLs
return faviconURLs
}
}
+1
View File
@@ -21,6 +21,7 @@ enum LogCategory: String {
case damus_purple
case image_uploading
case video_coordination
case tips
}
/// Damus structured logger
+19
View File
@@ -5,6 +5,7 @@
// Created by Scott Penrose on 5/7/23.
//
import FaviconFinder
import SwiftUI
enum Route: Hashable {
@@ -46,6 +47,9 @@ enum Route: Hashable {
case Wallet(wallet: WalletModel)
case WalletScanner(result: Binding<WalletScanResult>)
case FollowersYouKnow(friendedFollowers: [Pubkey], followers: FollowersModel)
case NIP05DomainEvents(events: NIP05DomainEventsModel, nip05_domain_favicon: FaviconURL?)
case NIP05DomainPubkeys(domain: String, nip05_domain_favicon: FaviconURL?, pubkeys: [Pubkey])
case FollowPack(followPack: NostrEvent, model: FollowPackModel, blur_imgs: Bool)
@ViewBuilder
func view(navigationCoordinator: NavigationCoordinator, damusState: DamusState) -> some View {
@@ -127,6 +131,12 @@ enum Route: Hashable {
FollowersYouKnowView(damus_state: damusState, friended_followers: friendedFollowers, followers: followers)
case .Script(let load_model):
LoadScript(pool: damusState.nostrNetwork.pool, model: load_model)
case .NIP05DomainEvents(let events, let nip05_domain_favicon):
NIP05DomainTimelineView(damus_state: damusState, model: events, nip05_domain_favicon: nip05_domain_favicon)
case .NIP05DomainPubkeys(let domain, let nip05_domain_favicon, let pubkeys):
NIP05DomainPubkeysView(damus_state: damusState, domain: domain, nip05_domain_favicon: nip05_domain_favicon, pubkeys: pubkeys)
case .FollowPack(let followPack, let followPackModel, let blur_imgs):
FollowPackView(state: damusState, ev: followPack, model: followPackModel, blur_imgs: blur_imgs)
}
}
@@ -231,6 +241,15 @@ enum Route: Hashable {
case .Script(let model):
hasher.combine("script")
hasher.combine(model.data.count)
case .NIP05DomainEvents(let events, _):
hasher.combine("nip05DomainEvents")
hasher.combine(events.domain)
case .NIP05DomainPubkeys(let domain, _, _):
hasher.combine("nip05DomainPubkeys")
hasher.combine(domain)
case .FollowPack(let followPack, let followPackModel, let blur_imgs):
hasher.combine("followPack")
hasher.combine(followPack.id)
}
}
}
+2 -2
View File
@@ -52,7 +52,7 @@ extension WalletConnect {
let req_id: NoteId
let response: Response
init(from event: NostrEvent, nwc: WalletConnect.ConnectURL) async throws(InitializationError) {
init(from event: NostrEvent, nwc: WalletConnect.ConnectURL) throws(InitializationError) {
guard event.pubkey == nwc.pubkey else { throw .incorrectAuthorPubkey }
guard let referencedNoteId = event.referenced_ids.first else { throw .missingRequestIdReference }
@@ -85,7 +85,7 @@ extension WalletConnect {
}
}
struct WalletResponseErr: Codable {
struct WalletResponseErr: Codable, Error {
let code: Code?
let message: String?
@@ -105,6 +105,28 @@ extension WalletConnect {
post.send(ev, to: [url.relay], skip_ephemeral: false, delay: delay, on_flush: on_flush)
return ev
}
@MainActor
static func refresh_wallet_information(damus_state: DamusState) async {
damus_state.wallet.resetWalletStateInformation()
await Self.update_wallet_information(damus_state: damus_state)
}
@MainActor
static func update_wallet_information(damus_state: DamusState) async {
guard let url = damus_state.settings.nostr_wallet_connect,
let nwc = WalletConnectURL(str: url) else {
return
}
let flusher: OnFlush? = nil
let delay = 0.0 // We don't need a delay when fetching a transaction list or balance
WalletConnect.request_transaction_list(url: nwc, pool: damus_state.nostrNetwork.pool, post: damus_state.nostrNetwork.postbox, delay: delay, on_flush: flusher)
WalletConnect.request_balance_information(url: nwc, pool: damus_state.nostrNetwork.pool, post: damus_state.nostrNetwork.postbox, delay: delay, on_flush: flusher)
return
}
static func handle_zap_success(state: DamusState, resp: WalletConnect.FullWalletResponse) {
// find the pending zap and mark it as pending-confirmed
-44
View File
@@ -1,44 +0,0 @@
//
// FriendsButton.swift
// damus
//
// Created by William Casarin on 2023-04-21.
//
import SwiftUI
struct FriendsButton: View {
@Binding var filter: FriendFilter
var body: some View {
Button(action: {
switch self.filter {
case .all:
self.filter = .friends_of_friends
case .friends_of_friends:
self.filter = .all
}
}) {
if filter == .friends_of_friends {
LINEAR_GRADIENT
.mask(Image("user-added")
.resizable()
).frame(width: 28, height: 28)
} else {
Image("user-added")
.resizable()
.frame(width: 28, height: 28)
.foregroundColor(.gray)
}
}
.buttonStyle(.plain)
}
}
struct FriendsButton_Previews: PreviewProvider {
@State static var enabled: FriendFilter = .all
static var previews: some View {
FriendsButton(filter: $enabled)
}
}
@@ -0,0 +1,54 @@
//
// TrustedNetworkButton.swift
// damus
//
// Created by William Casarin on 2023-04-21.
//
import SwiftUI
struct TrustedNetworkButton: View {
@Binding var filter: FriendFilter
var action: (@MainActor () -> Void)? = nil
var MainButton: some View {
Button(action: {
switch self.filter {
case .all:
self.filter = .friends_of_friends
case .friends_of_friends:
self.filter = .all
}
if let action {
action()
}
}) {
if filter == .friends_of_friends {
LINEAR_GRADIENT
.mask(Image(systemName: "network.badge.shield.half.filled")
.frame(width: 24, height: 24)
)
.scaledToFit()
.frame(width: 24, height: 24)
} else {
Image(systemName: "network.slash")
.frame(width: 24, height: 24)
.foregroundColor(.gray)
}
}
.buttonStyle(.plain)
}
var body: some View {
MainButton
}
}
struct TrustedNetworkButton_Previews: PreviewProvider {
@State static var enabled: FriendFilter = .all
static var previews: some View {
TrustedNetworkButton(filter: $enabled)
}
}
-6
View File
@@ -337,12 +337,6 @@ struct ChatEventView: View {
}
}
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)
+201 -94
View File
@@ -7,6 +7,7 @@
import SwiftUI
import SwipeActions
import TipKit
struct ChatroomThreadView: View {
@Environment(\.dismiss) var dismiss
@@ -15,11 +16,20 @@ struct ChatroomThreadView: View {
@ObservedObject var thread: ThreadModel
@State var highlighted_note_id: NoteId? = nil
@State var user_just_posted_flag: Bool = false
@State var untrusted_network_expanded: Bool = true
@Namespace private var animation
// Add state for sticky header
@State var showStickyHeader: Bool = false
@State var untrustedSectionOffset: CGFloat = 0
private static let untrusted_network_section_id = "untrusted-network-section"
private static let sticky_header_adjusted_anchor = UnitPoint(x: UnitPoint.top.x, y: 0.2)
func go_to_event(scroller: ScrollViewProxy, note_id: NoteId) {
scroll_to_event(scroller: scroller, id: note_id, delay: 0, animate: true, anchor: .top)
let adjustedAnchor: UnitPoint = showStickyHeader ? ChatroomThreadView.sticky_header_adjusted_anchor : .top
scroll_to_event(scroller: scroller, id: note_id, delay: 0, animate: true, anchor: adjustedAnchor)
highlighted_note_id = note_id
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
withAnimation {
@@ -27,7 +37,7 @@ struct ChatroomThreadView: View {
}
})
}
func set_active_event(scroller: ScrollViewProxy, ev: NdbNote) {
withAnimation {
self.thread.select(event: ev)
@@ -35,93 +45,202 @@ struct ChatroomThreadView: View {
}
}
func trusted_event_filter(_ event: NostrEvent) -> Bool {
!damus.settings.show_trusted_replies_first || damus.contacts.is_in_friendosphere(event.pubkey)
}
func ThreadedSwipeViewGroup(scroller: ScrollViewProxy, events: [NostrEvent]) -> some View {
SwipeViewGroup {
ForEach(Array(zip(events, events.indices)), id: \.0.id) { (ev, ind) in
ChatEventView(event: events[ind],
selected_event: self.thread.selected_event,
prev_ev: ind > 0 ? events[ind-1] : nil,
next_ev: ind == events.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: highlighted_note_id == ev.id,
bar: make_actionbar_model(ev: ev.id, damus: damus)
)
.id(ev.id)
.matchedGeometryEffect(id: ev.id.hex(), in: animation, anchor: .center)
.padding(.horizontal)
}
}
}
var OutsideTrustedNetworkLabel: some View {
HStack {
Label(
NSLocalizedString(
"Replies outside your trusted network",
comment: "Section title in thread for replies from outside of the current user's trusted network, which is their follows and follows of follows."),
systemImage: "network.slash"
)
Spacer()
Image(systemName: "chevron.right")
.rotationEffect(.degrees(untrusted_network_expanded ? 90 : 0))
.animation(.easeInOut(duration: 0.1), value: untrusted_network_expanded)
}
.foregroundColor(.secondary)
}
var StickyHeaderView: some View {
OutsideTrustedNetworkLabel
.padding(.horizontal)
.padding(.vertical, 12)
.background(
Color(UIColor.systemBackground)
.shadow(color: .black.opacity(0.15), radius: 3, x: 0, y: 2)
)
}
var body: some View {
ScrollViewReader { scroller in
ScrollView(.vertical) {
LazyVStack(alignment: .leading, spacing: 8) {
// MARK: - Parents events view
ForEach(thread.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.selected_event,
muteBox: { event_shown, muted_reason in
AnyView(
EventMutedBoxView(shown: event_shown, reason: muted_reason)
.padding(5)
)
}
) {
SelectedEventView(damus: damus, event: self.thread.selected_event, size: .selected)
.matchedGeometryEffect(id: self.thread.selected_event.id.hex(), in: animation, anchor: .center)
}
.id(self.thread.selected_event.id)
// MARK: - Children view
let events = thread.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.selected_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: highlighted_note_id == ev.id,
bar: make_actionbar_model(ev: ev.id, damus: damus)
)
let sorted_child_events = thread.sorted_child_events
let untrusted_events = sorted_child_events.filter { !trusted_event_filter($0) }
let trusted_events = sorted_child_events.filter { trusted_event_filter($0) }
ZStack(alignment: .top) {
ScrollView(.vertical) {
LazyVStack(alignment: .leading, spacing: 8) {
// MARK: - Parents events view
ForEach(thread.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)
.id(ev.id)
.matchedGeometryEffect(id: ev.id.hex(), in: animation, anchor: .center)
.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
let eventHeight = geometry.frame(in: .global).height
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.selected_event,
muteBox: { event_shown, muted_reason in
AnyView(
EventMutedBoxView(shown: event_shown, reason: muted_reason)
.padding(5)
)
}
) {
SelectedEventView(damus: damus, event: self.thread.selected_event, size: .selected)
.matchedGeometryEffect(id: self.thread.selected_event.id.hex(), in: animation, anchor: .center)
}
.id(self.thread.selected_event.id)
// MARK: - Children view - inside trusted network
if !trusted_events.isEmpty {
ThreadedSwipeViewGroup(scroller: scroller, events: trusted_events)
}
}
.padding(.top)
// MARK: - Children view - outside trusted network
if !untrusted_events.isEmpty {
if #available(iOS 17, *) {
TipView(TrustedNetworkRepliesTip.shared, arrowEdge: .bottom)
.padding(.top, 10)
.padding(.horizontal)
}
VStack(alignment: .leading, spacing: 0) {
// Track this section's position
Color.clear
.frame(height: 1)
.background(
GeometryReader { proxy in
Color.clear
.onAppear {
untrustedSectionOffset = proxy.frame(in: .global).minY
}
.onChange(of: proxy.frame(in: .global).minY) { newY in
let shouldShow = newY <= 100 // Adjust this threshold as needed
if shouldShow != showStickyHeader {
withAnimation(.easeInOut(duration: 0.3)) {
showStickyHeader = shouldShow
}
}
}
}
)
Button(action: {
withAnimation {
untrusted_network_expanded.toggle()
if #available(iOS 17, *) {
TrustedNetworkRepliesTip.shared.invalidate(reason: .actionPerformed)
}
scroll_to_event(scroller: scroller, id: ChatroomThreadView.untrusted_network_section_id, delay: 0.1, animate: true, anchor: ChatroomThreadView.sticky_header_adjusted_anchor)
}
}) {
OutsideTrustedNetworkLabel
}
.id(ChatroomThreadView.untrusted_network_section_id)
.buttonStyle(PlainButtonStyle())
.padding(.horizontal)
if untrusted_network_expanded {
withAnimation {
LazyVStack(alignment: .leading, spacing: 8) {
ThreadedSwipeViewGroup(scroller: scroller, events: untrusted_events)
}
.padding(.top, 10)
}
}
}
}
EndBlock()
HStack {}
.frame(height: tabHeight + getSafeAreaBottom())
}
if showStickyHeader && !untrusted_events.isEmpty {
VStack {
StickyHeaderView
.onTapGesture {
withAnimation {
untrusted_network_expanded.toggle()
}
}
Spacer()
}
.transition(.move(edge: .top).combined(with: .opacity))
.zIndex(1)
}
.padding(.top)
EndBlock()
HStack {}
.frame(height: tabHeight + getSafeAreaBottom())
}
.onReceive(handle_notify(.post), perform: { notify in
switch notify {
case .post(_):
user_just_posted_flag = true
case .cancel:
return
case .post(_):
user_just_posted_flag = true
case .cancel:
return
}
})
.onReceive(thread.objectWillChange) {
@@ -139,15 +258,8 @@ struct ChatroomThreadView: View {
}
}
}
func toggle_thread_view() {
NotificationCenter.default.post(name: .toggle_thread_view, object: nil)
}
}
struct ChatroomView_Previews: PreviewProvider {
static var previews: some View {
Group {
@@ -167,8 +279,3 @@ struct ChatroomView_Previews: PreviewProvider {
}
}
}
@MainActor
func scroll_after_load(thread: ThreadModel, proxy: ScrollViewProxy) {
scroll_to_event(scroller: proxy, id: thread.selected_event.id, delay: 0.1, animate: false)
}
+24 -4
View File
@@ -6,6 +6,7 @@
//
import SwiftUI
import TipKit
enum DMType: Hashable {
case rando
@@ -18,6 +19,7 @@ struct DirectMessagesView: View {
@State var dm_type: DMType = .friend
@ObservedObject var model: DirectMessagesModel
@ObservedObject var settings: UserSettingsStore
@Binding var subtitle: String?
func MainContent(requests: Bool) -> some View {
ScrollView {
@@ -72,7 +74,15 @@ struct DirectMessagesView: View {
}
var body: some View {
let showTrustedButton = would_filter_non_friends_from_dms(contacts: damus_state.contacts, dms: self.model.dms)
VStack(spacing: 0) {
if #available(iOS 17, *), showTrustedButton {
TipView(TrustedNetworkButtonTip.shared)
.tipBackground(.clear)
.tipViewStyle(TrustedNetworkButtonTipViewStyle())
.padding(.horizontal)
}
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),
@@ -92,12 +102,22 @@ struct DirectMessagesView: View {
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
if would_filter_non_friends_from_dms(contacts: damus_state.contacts, dms: self.model.dms) {
FriendsButton(filter: $settings.friend_filter)
if showTrustedButton {
TrustedNetworkButton(filter: $settings.friend_filter) {
if #available(iOS 17, *) {
TrustedNetworkButtonTip.shared.invalidate(reason: .actionPerformed)
}
}
}
}
}
.onAppear {
self.subtitle = settings.friend_filter.description()
}
.onChange(of: settings.friend_filter) { val in
self.subtitle = val.description()
}
.navigationTitle(NSLocalizedString("DMs", comment: "Navigation title for view of DMs, where DM is an English abbreviation for Direct Message."))
}
}
@@ -115,6 +135,6 @@ func would_filter_non_friends_from_dms(contacts: Contacts, dms: [DirectMessageMo
struct DirectMessagesView_Previews: PreviewProvider {
static var previews: some View {
let ds = test_damus_state
DirectMessagesView(damus_state: ds, model: ds.dms, settings: ds.settings)
DirectMessagesView(damus_state: ds, model: ds.dms, settings: ds.settings, subtitle: .constant(nil))
}
}
@@ -0,0 +1,242 @@
//
// FollowPackPreview.swift
// damus
//
// Created by eric on 4/30/25.
//
import SwiftUI
import Kingfisher
struct FollowPackUsers: View {
let state: DamusState
var publicKeys: [Pubkey]
var body: some View {
HStack(alignment: .center) {
if !publicKeys.isEmpty {
CondensedProfilePicturesView(state: state, pubkeys: publicKeys, maxPictures: 5)
}
let followPackUserCount = publicKeys.count
let nounString = pluralizedString(key: "follow_pack_user_count", count: followPackUserCount)
let nounText = Text(verbatim: nounString).font(.subheadline).foregroundColor(.gray)
Text("\(Text(verbatim: followPackUserCount.formatted()).font(.subheadline.weight(.medium))) \(nounText)", comment: "Sentence composed of 2 variables to describe how many people are in the follow pack. In source English, the first variable is the number of users, and the second variable is 'user' or 'users'.")
}
}
}
struct FollowPackBannerImage: View {
let state: DamusState
let options: EventViewOptions
var image: URL? = nil
var preview: Bool
@State var blur_imgs: Bool
func Placeholder(url: URL, preview: Bool) -> some View {
Group {
if let meta = state.events.lookup_img_metadata(url: url),
case .processed(let blurhash) = meta.state {
Image(uiImage: blurhash)
.resizable()
.frame(maxWidth: preview ? 350 : UIScreen.main.bounds.width, minHeight: preview ? 180 : 200, maxHeight: preview ? 180 : 200)
} else {
DamusColors.adaptableWhite
}
}
}
func titleImage(url: URL, preview: Bool) -> some View {
KFAnimatedImage(url)
.callbackQueue(.dispatch(.global(qos:.background)))
.backgroundDecode(true)
.imageContext(.note, disable_animation: state.settings.disable_animation)
.image_fade(duration: 0.25)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
.background {
Placeholder(url: url, preview: preview)
}
.aspectRatio(contentMode: .fill)
.frame(maxWidth: preview ? 350 : UIScreen.main.bounds.width, minHeight: preview ? 180 : 200, maxHeight: preview ? 180 : 200)
.kfClickable()
.cornerRadius(1)
}
var body: some View {
if let url = image {
if (self.options.contains(.no_media)) {
EmptyView()
} else if !blur_imgs {
titleImage(url: url, preview: preview)
} else {
ZStack {
titleImage(url: url, preview: preview)
BlurOverlayView(blur_images: $blur_imgs, artifacts: nil, size: nil, damus_state: nil, parentView: .longFormView)
.frame(maxWidth: preview ? 350 : UIScreen.main.bounds.width, minHeight: preview ? 180 : 200, maxHeight: preview ? 180 : 200)
}
}
} else {
Text(NSLocalizedString("No cover image", comment: "Text letting user know there is no cover image."))
.foregroundColor(.gray)
.frame(width: 350, height: 180)
Divider()
}
}
}
struct FollowPackPreviewBody: View {
let state: DamusState
let event: FollowPackEvent
let options: EventViewOptions
let header: Bool
@State var blur_imgs: Bool
@ObservedObject var artifacts: NoteArtifactsModel
init(state: DamusState, ev: FollowPackEvent, options: EventViewOptions, header: Bool, blur_imgs: Bool) {
self.state = state
self.event = ev
self.options = options
self.header = header
self.blur_imgs = blur_imgs
self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.event.id).artifacts_model)
}
init(state: DamusState, ev: NostrEvent, options: EventViewOptions, header: Bool, blur_imgs: Bool) {
self.state = state
self.event = FollowPackEvent.parse(from: ev)
self.options = options
self.header = header
self.blur_imgs = blur_imgs
self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.id).artifacts_model)
}
var body: some View {
Group {
if options.contains(.wide) {
Main.padding(.horizontal)
} else {
Main
}
}
}
var Main: some View {
VStack(alignment: .leading, spacing: 10) {
if state.settings.media_previews {
FollowPackBannerImage(state: state, options: options, image: event.image, preview: true, blur_imgs: blur_imgs)
}
Text(event.title ?? NSLocalizedString("Untitled", comment: "Title of follow list event if it is untitled."))
.font(header ? .title : .headline)
.padding(.horizontal, 10)
.padding(.top, 5)
if let description = event.description {
Text(description)
.font(header ? .body : .caption)
.foregroundColor(.gray)
.padding(.horizontal, 10)
} else {
Text("")
.font(header ? .body : .caption)
.foregroundColor(.gray)
.padding(.horizontal, 10)
}
HStack(alignment: .center) {
ProfilePicView(pubkey: event.event.pubkey, size: 25, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation, show_zappability: true)
.onTapGesture {
state.nav.push(route: Route.ProfileByKey(pubkey: event.event.pubkey))
}
let profile_txn = state.profiles.lookup(id: event.event.pubkey)
let profile = profile_txn?.unsafeUnownedValue
let displayName = Profile.displayName(profile: profile, pubkey: event.event.pubkey)
switch displayName {
case .one(let one):
Text(one)
.font(.subheadline).foregroundColor(.gray)
case .both(username: let username, displayName: let displayName):
HStack(spacing: 6) {
Text(verbatim: displayName)
.font(.subheadline).foregroundColor(.gray)
Text(verbatim: "@\(username)")
.font(.subheadline).foregroundColor(.gray)
}
}
}
.padding(.horizontal, 10)
FollowPackUsers(state: state, publicKeys: event.publicKeys)
.padding(.horizontal, 10)
.padding(.bottom, 20)
}
.frame(width: 350, height: state.settings.media_previews ? 330 : 150, alignment: .leading)
.background(DamusColors.neutral3)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(DamusColors.neutral1, lineWidth: 1)
)
.padding(.top, 10)
}
}
struct FollowPackPreview: View {
let state: DamusState
let event: FollowPackEvent
let options: EventViewOptions
@State var blur_imgs: Bool
init(state: DamusState, ev: NostrEvent, options: EventViewOptions, blur_imgs: Bool) {
self.state = state
self.event = FollowPackEvent.parse(from: ev)
self.options = options.union(.no_mentions)
self.blur_imgs = blur_imgs
}
var body: some View {
FollowPackPreviewBody(state: state, ev: event, options: options, header: false, blur_imgs: blur_imgs)
}
}
let test_follow_list_event = FollowPackEvent.parse(from: NostrEvent(
content: "",
keypair: test_keypair,
kind: NostrKind.longform.rawValue,
tags: [
["title", "DAMUSES"],
["description", "Damus Team"],
["published_at", "1685638715"],
["p", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"],
["p", "8b2be0a0ad34805d76679272c28a77dbede9adcbfdca48c681ec8b624a1208a6"],
["p", "17538dc2a62769d09443f18c37cbe358fab5bbf981173542aa7c5ff171ed77c4"],
["p", "520830c334a3f79f88cac934580d26f91a7832c6b21fb9625690ea2ed81b5626"],
["p", "2779f3d9f42c7dee17f0e6bcdcf89a8f9d592d19e3b1bbd27ef1cffd1a7f98d1"],
["p", "bd1e19980e2c91e6dc657e92c25762ca882eb9272d2579e221f037f93788de91"],
["p", "e7424ad457e512fdf4764a56bf6d428a06a13a1006af1fb8e0fe32f6d03265c7"],
["p", "b88c7f007bbf3bc2fcaeff9e513f186bab33782c0baa6a6cc12add78b9110ba3"],
["p", "4a0510f26880d40e432f4865cb5714d9d3c200ca6ebb16b418ae6c555f574967"],
["image", "https://damus.io/img/logo.png"],
])!
)
struct FollowPackPreview_Previews: PreviewProvider {
static var previews: some View {
VStack {
FollowPackPreview(state: test_damus_state, ev: test_follow_list_event.event, options: [], blur_imgs: false)
}
.frame(height: 400)
}
}
@@ -0,0 +1,135 @@
//
// FollowPackTimeline.swift
// damus
//
// Created by eric on 5/6/25.
//
import SwiftUI
struct FollowPackTimelineView<Content: View>: View {
@ObservedObject var events: EventHolder
@Binding var loading: Bool
let damus: DamusState
let show_friend_icon: Bool
let filter: (NostrEvent) -> Bool
let content: Content?
let apply_mute_rules: Bool
init(events: EventHolder, loading: Binding<Bool>, headerHeight: Binding<CGFloat>, headerOffset: Binding<CGFloat>, damus: DamusState, show_friend_icon: Bool, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true, content: (() -> Content)? = nil) {
self.events = events
self._loading = loading
self.damus = damus
self.show_friend_icon = show_friend_icon
self.filter = filter
self.apply_mute_rules = apply_mute_rules
self.content = content?()
}
init(events: EventHolder, loading: Binding<Bool>, damus: DamusState, show_friend_icon: Bool, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true, content: (() -> Content)? = nil) {
self.events = events
self._loading = loading
self.damus = damus
self.show_friend_icon = show_friend_icon
self.filter = filter
self.apply_mute_rules = apply_mute_rules
self.content = content?()
}
var body: some View {
MainContent
}
var MainContent: some View {
ScrollViewReader { scroller in
ScrollView(.horizontal) {
if let content {
content
}
Color.clear
.id("startblock")
.frame(height: 0)
FollowPackInnerView(events: events, damus: damus, filter: loading ? { _ in true } : filter, apply_mute_rules: self.apply_mute_rules)
.redacted(reason: loading ? .placeholder : [])
.shimmer(loading)
.disabled(loading)
.background {
GeometryReader { proxy -> Color in
handle_scroll_queue(proxy, queue: self.events)
return Color.clear
}
}
}
.coordinateSpace(name: "scroll")
.onReceive(handle_notify(.scroll_to_top)) { () in
events.flush()
self.events.should_queue = false
scroll_to_event(scroller: scroller, id: "startblock", delay: 0.0, animate: true, anchor: .top)
}
}
.onAppear {
events.flush()
}
}
}
struct FollowPackInnerView: View {
@ObservedObject var events: EventHolder
let state: DamusState
let filter: (NostrEvent) -> Bool
init(events: EventHolder, damus: DamusState, filter: @escaping (NostrEvent) -> Bool, apply_mute_rules: Bool = true) {
self.events = events
self.state = damus
self.filter = apply_mute_rules ? { filter($0) && !damus.mutelist_manager.is_event_muted($0) } : filter
}
var event_options: EventViewOptions {
if self.state.settings.truncate_timeline_text {
return [.wide, .truncate_content]
}
return [.wide]
}
var body: some View {
LazyHStack(spacing: 0) {
let events = self.events.events
if events.isEmpty {
EmptyTimelineView()
} else {
let evs = events.filter(filter)
let indexed = Array(zip(evs, 0...))
ForEach(indexed, id: \.0.id) { tup in
let ev = tup.0
let ind = tup.1
let blur_imgs = should_blur_images(settings: state.settings, contacts: state.contacts, ev: ev, our_pubkey: state.pubkey)
if ev.kind == NostrKind.follow_list.rawValue {
FollowPackPreview(state: state, ev: ev, options: event_options, blur_imgs: blur_imgs)
.onTapGesture {
state.nav.push(route: Route.FollowPack(followPack: ev, model: FollowPackModel(damus_state: state), blur_imgs: blur_imgs))
}
.padding(.top, 7)
.onAppear {
let to_preload =
Array([indexed[safe: ind+1]?.0,
indexed[safe: ind+2]?.0,
indexed[safe: ind+3]?.0,
indexed[safe: ind+4]?.0,
indexed[safe: ind+5]?.0
].compactMap({ $0 }))
preload_events(state: state, events: to_preload)
}
}
}
}
}
.padding(.bottom)
}
}
@@ -0,0 +1,176 @@
//
// FollowPackView.swift
// damus
//
// Created by eric on 4/30/25.
//
import SwiftUI
import Kingfisher
struct FollowPackView: View {
let state: DamusState
let event: FollowPackEvent
@StateObject var model: FollowPackModel
@State var blur_imgs: Bool
@Environment(\.colorScheme) var colorScheme
@ObservedObject var artifacts: NoteArtifactsModel
init(state: DamusState, ev: FollowPackEvent, model: FollowPackModel, blur_imgs: Bool) {
self.state = state
self.event = ev
self._model = StateObject(wrappedValue: model)
self.blur_imgs = blur_imgs
self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.event.id).artifacts_model)
}
init(state: DamusState, ev: NostrEvent, model: FollowPackModel, blur_imgs: Bool) {
self.state = state
self.event = FollowPackEvent.parse(from: ev)
self._model = StateObject(wrappedValue: model)
self.blur_imgs = blur_imgs
self._artifacts = ObservedObject(wrappedValue: state.events.get_cache_data(ev.id).artifacts_model)
}
func content_filter(_ pubkeys: [Pubkey]) -> ((NostrEvent) -> Bool) {
var filters = ContentFilters.defaults(damus_state: self.state)
filters.append({ pubkeys.contains($0.pubkey) })
return ContentFilters(filters: filters).filter
}
enum FollowPackTabSelection: Int {
case people = 0
case posts = 1
}
@State var tab_selection: FollowPackTabSelection = .people
var body: some View {
ZStack {
ScrollView {
FollowPackHeader
FollowPackTabs
}
}
.onAppear {
if model.events.events.isEmpty {
model.subscribe(follow_pack_users: event.publicKeys)
}
}
.onDisappear {
model.unsubscribe()
}
}
var tabs: [(String, FollowPackTabSelection)] {
let tabs = [
(NSLocalizedString("People", comment: "Label for filter for seeing the people in this follow pack."), FollowPackTabSelection.people),
(NSLocalizedString("Posts", comment: "Label for filter for seeing the posts from the people in this follow pack."), FollowPackTabSelection.posts)
]
return tabs
}
var FollowPackTabs: some View {
VStack(spacing: 0) {
VStack(spacing: 0) {
CustomPicker(tabs: tabs, selection: $tab_selection)
Divider()
.frame(height: 1)
}
.background(colorScheme == .dark ? Color.black : Color.white)
if tab_selection == FollowPackTabSelection.people {
LazyVStack(alignment: .leading) {
ForEach(event.publicKeys.reversed(), id: \.self) { pk in
FollowUserView(target: .pubkey(pk), damus_state: state)
}
}
.padding()
.padding(.bottom, 50)
.tag(FollowPackTabSelection.people)
.id(FollowPackTabSelection.people)
}
if tab_selection == FollowPackTabSelection.posts {
InnerTimelineView(events: model.events, damus: state, filter: content_filter(event.publicKeys))
}
}
.onAppear() {
model.subscribe(follow_pack_users: event.publicKeys)
}
.onDisappear {
model.unsubscribe()
}
}
var FollowPackHeader: some View {
VStack(alignment: .leading, spacing: 10) {
if state.settings.media_previews {
FollowPackBannerImage(state: state, options: EventViewOptions(), image: event.image, preview: false, blur_imgs: blur_imgs)
}
Text(event.title ?? NSLocalizedString("Untitled", comment: "Title of follow list event if it is untitled."))
.font(.title)
.padding(.horizontal, 10)
.padding(.top, 5)
if let description = event.description {
Text(description)
.font(.body)
.foregroundColor(.gray)
.padding(.horizontal, 10)
} else {
EmptyView()
}
HStack(alignment: .center) {
ProfilePicView(pubkey: event.event.pubkey, size: 25, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation, show_zappability: true)
.onTapGesture {
state.nav.push(route: Route.ProfileByKey(pubkey: event.event.pubkey))
}
let profile_txn = state.profiles.lookup(id: event.event.pubkey)
let profile = profile_txn?.unsafeUnownedValue
let displayName = Profile.displayName(profile: profile, pubkey: event.event.pubkey)
switch displayName {
case .one(let one):
Text("Created by \(one)", comment: "Lets the user know who created this follow pack.")
.font(.subheadline).foregroundColor(.gray)
case .both(username: let username, displayName: let displayName):
HStack(spacing: 6) {
Text("Created by \(displayName)", comment: "Lets the user know who created this follow pack.")
.font(.subheadline).foregroundColor(.gray)
Text(verbatim: "@\(username)")
.font(.subheadline).foregroundColor(.gray)
}
}
}
.padding(.horizontal, 10)
.padding(.bottom, 20)
HStack(alignment: .center) {
FollowPackUsers(state: state, publicKeys: event.publicKeys)
}
.padding(.horizontal, 10)
.padding(.bottom, 20)
}
}
}
struct FollowPackView_Previews: PreviewProvider {
static var previews: some View {
VStack {
FollowPackView(state: test_damus_state, ev: test_follow_list_event, model: FollowPackModel(damus_state: test_damus_state), blur_imgs: false)
}
.frame(height: 400)
}
}
+1 -1
View File
@@ -74,7 +74,7 @@ class LoadableNostrEventViewModel: ObservableObject {
case .zap, .zap_request:
guard let zap = await get_zap(from: ev, state: damus_state) else { return .not_found }
return .loaded(route: Route.Zaps(target: zap.target))
case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .nwc_request, .nwc_response, .http_auth, .status, .relay_list:
case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .nwc_request, .nwc_response, .http_auth, .status, .relay_list, .follow_list:
return .unknown_or_unsupported_kind
}
case .naddr(let naddr):
+51
View File
@@ -0,0 +1,51 @@
//
// NIP05DomainPubkeysView.swift
// damus
//
// Created by Terry Yiu on 5/23/25.
//
import FaviconFinder
import Kingfisher
import SwiftUI
struct NIP05DomainPubkeysView: View {
let damus_state: DamusState
let domain: String
let nip05_domain_favicon: FaviconURL?
let pubkeys: [Pubkey]
var body: some View {
ScrollView {
LazyVStack(alignment: .leading) {
ForEach(pubkeys, id: \.self) { pk in
FollowUserView(target: .pubkey(pk), damus_state: damus_state)
}
}
.padding(.horizontal)
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
HStack {
if let nip05_domain_favicon {
KFImage(nip05_domain_favicon.source)
.imageContext(.favicon, disable_animation: true)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 18, height: 18)
.clipped()
}
Text(domain)
.font(.headline)
}
}
}
}
}
#Preview {
let nip05_domain_favicon = FaviconURL(source: URL(string: "https://damus.io/favicon.ico")!, format: .ico, sourceType: .ico)
let pubkeys = [test_pubkey, test_pubkey_2]
NIP05DomainPubkeysView(damus_state: test_damus_state, domain: "damus.io", nip05_domain_favicon: nip05_domain_favicon, pubkeys: pubkeys)
}
@@ -0,0 +1,111 @@
//
// NIP05DomainTimelineHeaderView.swift
// damus
//
// Created by Terry Yiu on 5/16/25.
//
import FaviconFinder
import Kingfisher
import SwiftUI
struct NIP05DomainTimelineHeaderView: View {
let damus_state: DamusState
@ObservedObject var model: NIP05DomainEventsModel
let nip05_domain_favicon: FaviconURL?
@Environment(\.openURL) var openURL
var Icon: some View {
ZStack {
if let nip05_domain_favicon {
KFImage(nip05_domain_favicon.source)
.imageContext(.favicon, disable_animation: true)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 18, height: 18)
.clipped()
} else {
EmptyView()
}
}
}
var friendsOfFriends: [Pubkey] {
// Order it such that the pubkeys that have events come first in the array so that their profile pictures
// show first.
let pubkeys = model.events.all_events.map { $0.pubkey } + (model.filter.authors ?? [])
// Filter out duplicates but retain order, and filter out any that do not have a validated NIP-05.
return (NSMutableOrderedSet(array: pubkeys).array as? [Pubkey] ?? [])
.filter {
damus_state.contacts.is_in_friendosphere($0) && damus_state.profiles.is_validated($0) != nil
}
}
var body: some View {
VStack(alignment: .leading) {
HStack {
if nip05_domain_favicon != nil {
Icon
}
Text(model.domain)
.foregroundStyle(DamusLogoGradient.gradient)
.font(.title.bold())
.onTapGesture {
if let url = URL(string: "https://\(model.domain)") {
openURL(url)
}
}
}
let friendsOfFriends = friendsOfFriends
HStack {
CondensedProfilePicturesView(state: damus_state, pubkeys: friendsOfFriends, maxPictures: 3)
let friendsOfFriendsString = friendsOfFriendsString(friendsOfFriends, ndb: damus_state.ndb)
Text(friendsOfFriendsString)
.font(.subheadline)
.foregroundColor(.gray)
.multilineTextAlignment(.leading)
}
.onTapGesture {
if !friendsOfFriends.isEmpty {
damus_state.nav.push(route: Route.NIP05DomainPubkeys(domain: model.domain, nip05_domain_favicon: nip05_domain_favicon, pubkeys: friendsOfFriends))
}
}
}
}
}
func friendsOfFriendsString(_ friendsOfFriends: [Pubkey], ndb: Ndb, locale: Locale = Locale.current) -> String {
let bundle = bundleForLocale(locale: locale)
let names: [String] = friendsOfFriends.prefix(3).map { pk in
let profile = ndb.lookup_profile(pk)?.unsafeUnownedValue?.profile
return Profile.displayName(profile: profile, pubkey: pk).username.truncate(maxLength: 20)
}
switch friendsOfFriends.count {
case 0:
return "No one in your trusted network is associated with this domain."
case 1:
let format = NSLocalizedString("Notes from %@", bundle: bundle, comment: "Text to indicate that notes from one pubkey in our trusted network are shown below.")
return String(format: format, locale: locale, names[0])
case 2:
let format = NSLocalizedString("Notes from %@ & %@", bundle: bundle, comment: "Text to indicate that notes from two pubkeys in our trusted network are shown below.")
return String(format: format, locale: locale, names[0], names[1])
case 3:
let format = NSLocalizedString("Notes from %@, %@ & %@", bundle: bundle, comment: "Text to indicate that notes from three pubkeys in our trusted network are shown below.")
return String(format: format, locale: locale, names[0], names[1], names[2])
default:
let format = localizedStringFormat(key: "notes_from_three_and_others", locale: locale)
return String(format: format, locale: locale, friendsOfFriends.count - 3, names[0], names[1], names[2])
}
}
#Preview {
let model = NIP05DomainEventsModel(state: test_damus_state, domain: "damus.io")
let nip05_domain_favicon = FaviconURL(source: URL(string: "https://damus.io/favicon.ico")!, format: .ico, sourceType: .ico)
NIP05DomainTimelineHeaderView(damus_state: test_damus_state, model: model, nip05_domain_favicon: nip05_domain_favicon)
}
+64
View File
@@ -0,0 +1,64 @@
//
// NIP05DomainTimelineView.swift
// damus
//
// Created by Terry Yiu on 4/11/25.
//
import FaviconFinder
import Kingfisher
import SwiftUI
struct NIP05DomainTimelineView: View {
let damus_state: DamusState
@ObservedObject var model: NIP05DomainEventsModel
let nip05_domain_favicon: FaviconURL?
func nip05_filter(ev: NostrEvent) -> Bool {
damus_state.contacts.is_in_friendosphere(ev.pubkey) && damus_state.profiles.is_validated(ev.pubkey) != nil
}
var contentFilters: ContentFilters {
var filters = Array<(NostrEvent) -> Bool>()
filters.append(contentsOf: ContentFilters.defaults(damus_state: damus_state))
filters.append(nip05_filter)
return ContentFilters(filters: filters)
}
var body: some View {
let height: CGFloat = 250.0
TimelineView(events: model.events, loading: $model.loading, damus: damus_state, show_friend_icon: true, filter: contentFilters.filter(ev:)) {
ZStack(alignment: .leading) {
DamusBackground(maxHeight: height)
.mask(LinearGradient(gradient: Gradient(colors: [.black, .black, .black, .clear]), startPoint: .top, endPoint: .bottom))
NIP05DomainTimelineHeaderView(damus_state: damus_state, model: model, nip05_domain_favicon: nip05_domain_favicon)
.padding(.leading, 30)
.padding(.top, 30)
}
}
.ignoresSafeArea()
.padding(.bottom, tabHeight)
.onAppear {
guard model.events.all_events.isEmpty else { return }
model.subscribe()
if let pubkeys = model.filter.authors {
for pubkey in pubkeys {
check_nip05_validity(pubkey: pubkey, profiles: damus_state.profiles)
}
}
}
.onDisappear {
model.unsubscribe()
}
}
}
#Preview {
let damus_state = test_damus_state
let model = NIP05DomainEventsModel(state: damus_state, domain: "damus.io")
let nip05_domain_favicon = FaviconURL(source: URL(string: "https://damus.io/favicon.ico")!, format: .ico, sourceType: .ico)
NIP05DomainTimelineView(damus_state: test_damus_state, model: model, nip05_domain_favicon: nip05_domain_favicon)
}
+31 -6
View File
@@ -73,15 +73,40 @@ struct NoteContentView: View {
}
var preview: LinkViewRepresentable? {
guard !blur_images,
case .loaded(let preview) = preview_model.state,
guard case .loaded(let preview) = preview_model.state,
case .value(let cached) = preview else {
return nil
}
// If either
// (1) the blur images setting is enabled
// (2) the media previews setting is disabled
// (3) this note content view does not display media
// then do not show media in the link preview.
if blur_images || !damus_state.settings.media_previews || self.options.contains(.no_media) {
return linkPreviewWithNoMedia(cached)
}
// If media is already being shown, do not show media in the link preview
// to avoid taking up additional screen space.
if case let .separated(separated) = note_artifacts, !separated.media.isEmpty && !self.options.contains(.no_media) {
return linkPreviewWithNoMedia(cached)
}
return LinkViewRepresentable(meta: .linkmeta(cached))
}
// Creates a LinkViewRepresentable without media previews.
func linkPreviewWithNoMedia(_ cached: CachedMetadata) -> LinkViewRepresentable? {
let linkMetadata = LPLinkMetadata()
linkMetadata.originalURL = cached.meta.originalURL
linkMetadata.title = cached.meta.title
linkMetadata.url = cached.meta.url
return LinkViewRepresentable(meta: .linkmeta(CachedMetadata(meta: linkMetadata)))
}
func truncatedText(content: CompatibleText) -> some View {
Group {
if truncate_very_short {
@@ -108,7 +133,7 @@ struct NoteContentView: View {
func previewView(links: [URL]) -> some View {
Group {
if let preview = self.preview, !blur_images {
if let preview = self.preview {
if let preview_height {
preview
.frame(height: preview_height)
@@ -181,7 +206,7 @@ struct NoteContentView: View {
}
}
if damus_state.settings.media_previews, has_previews {
if has_previews {
if with_padding {
previewView(links: artifacts.links).padding(.horizontal)
} else {
@@ -6,6 +6,7 @@
//
import SwiftUI
import TipKit
class NotificationFilter: ObservableObject, Equatable {
@Published var state: NotificationFilterState
@@ -75,10 +76,11 @@ struct NotificationsView: View {
@StateObject var filter = NotificationFilter()
@SceneStorage("NotificationsView.filter_state") var filter_state: NotificationFilterState = .all
@Binding var subtitle: String?
@Environment(\.colorScheme) var colorScheme
var body: some View {
let showTrustedButton = would_filter_non_friends_from_notifications(contacts: state.contacts, state: filter_state, items: self.notifications.notifications)
TabView(selection: $filter_state) {
NotificationTab(
NotificationFilter(
@@ -115,14 +117,19 @@ struct NotificationsView: View {
Button(
action: { state.nav.push(route: Route.NotificationSettings(settings: state.settings)) },
label: {
Image("settings")
Image(systemName: "gearshape")
.frame(width: 24, height: 24)
.foregroundColor(.gray)
}
)
}
ToolbarItem(placement: .navigationBarTrailing) {
if would_filter_non_friends_from_notifications(contacts: state.contacts, state: filter_state, items: self.notifications.notifications) {
FriendsButton(filter: $filter.friend_filter)
if showTrustedButton {
TrustedNetworkButton(filter: $filter.friend_filter) {
if #available(iOS 17, *) {
TrustedNetworkButtonTip.shared.invalidate(reason: .actionPerformed)
}
}
}
}
}
@@ -140,6 +147,13 @@ struct NotificationsView: View {
}
.safeAreaInset(edge: .top, spacing: 0) {
VStack(spacing: 0) {
if #available(iOS 17, *), showTrustedButton {
TipView(TrustedNetworkButtonTip.shared)
.tipBackground(.clear)
.tipViewStyle(TrustedNetworkButtonTipViewStyle())
.padding(.horizontal)
}
CustomPicker(tabs: [
(NSLocalizedString("All", comment: "Label for filter for all notifications."), NotificationFilterState.all),
(NSLocalizedString("Zaps", comment: "Label for filter for zap notifications."), NotificationFilterState.zaps),
@@ -35,7 +35,6 @@ struct SuggestedUserView: View {
let target = FollowTarget.pubkey(user.pubkey)
InnerProfilePicView(url: user.pfp,
fallbackUrl: nil,
pubkey: target.pubkey,
size: 50,
highlight: .none,
disable_animation: false)
+31 -5
View File
@@ -5,6 +5,7 @@
// Created by William Casarin on 2022-04-16.
//
import FaviconFinder
import SwiftUI
enum FriendType {
@@ -43,6 +44,7 @@ struct ProfileName: View {
@State var nip05: NIP05?
@State var donation: Int?
@State var purple_account: DamusPurple.Account?
@State var nip05_domain_favicon: FaviconURL?
init(pubkey: Pubkey, prefix: String = "", damus: DamusState, show_nip5_domain: Bool = true, supporterBadgeStyle: SupporterBadge.Style = .compact) {
self.pubkey = pubkey
@@ -61,7 +63,7 @@ struct ProfileName: View {
var current_nip05: NIP05? {
nip05 ?? damus_state.profiles.is_validated(pubkey)
}
func current_display_name(profile: Profile?) -> DisplayName {
return display_name ?? Profile.displayName(profile: profile, pubkey: pubkey)
}
@@ -101,7 +103,7 @@ struct ProfileName: View {
.fontWeight(prefix == "@" ? .none : .bold)
if let nip05 = current_nip05 {
NIP05Badge(nip05: nip05, pubkey: pubkey, contacts: damus_state.contacts, show_domain: show_nip5_domain, profiles: damus_state.profiles)
NIP05Badge(nip05: nip05, pubkey: pubkey, damus_state: damus_state, show_domain: show_nip5_domain, nip05_domain_favicon: nip05_domain_favicon)
}
if let friend = friend_type, current_nip05 == nil {
@@ -118,9 +120,15 @@ struct ProfileName: View {
}
.task {
if damus_state.purple.enable_purple {
self.purple_account = try? await damus_state.purple.get_maybe_cached_account(pubkey: pubkey)
}
if damus_state.purple.enable_purple {
self.purple_account = try? await damus_state.purple.get_maybe_cached_account(pubkey: pubkey)
}
}
.task {
if let domain = current_nip05?.host {
self.nip05_domain_favicon = try? await damus_state.favicon_cache.lookup(domain)
.largest()
}
}
.onReceive(handle_notify(.profile_updated)) { update in
if update.pubkey != pubkey {
@@ -151,6 +159,24 @@ struct ProfileName: View {
let nip05 = damus_state.profiles.is_validated(pubkey)
if nip05 != self.nip05 {
self.nip05 = nip05
if let domain = nip05?.host {
Task {
let favicon = try? await damus_state.favicon_cache.lookup(domain)
.filter {
if let size = $0.size {
return size.width <= 128 && size.height <= 128
} else {
return true
}
}
.largest()
await MainActor.run {
self.nip05_domain_favicon = favicon
}
}
}
}
if donation != profile.damus_donation {
+16 -5
View File
@@ -31,7 +31,6 @@ func pfp_line_width(_ h: Highlight) -> CGFloat {
struct InnerProfilePicView: View {
let url: URL?
let fallbackUrl: URL?
let pubkey: Pubkey
let size: CGFloat
let highlight: Highlight
let disable_animation: Bool
@@ -65,16 +64,19 @@ struct InnerProfilePicView: View {
struct ProfilePicView: View {
@Environment(\.redactionReasons) var redactionReasons
let pubkey: Pubkey
let size: CGFloat
let highlight: Highlight
let profiles: Profiles
let disable_animation: Bool
let zappability_indicator: Bool
let privacy_sensitive: Bool
@State var picture: String?
init(pubkey: Pubkey, size: CGFloat, highlight: Highlight, profiles: Profiles, disable_animation: Bool, picture: String? = nil, show_zappability: Bool? = nil) {
init(pubkey: Pubkey, size: CGFloat, highlight: Highlight, profiles: Profiles, disable_animation: Bool, picture: String? = nil, show_zappability: Bool? = nil, privacy_sensitive: Bool = false) {
self.pubkey = pubkey
self.profiles = profiles
self.size = size
@@ -82,15 +84,24 @@ struct ProfilePicView: View {
self._picture = State(initialValue: picture)
self.disable_animation = disable_animation
self.zappability_indicator = show_zappability ?? false
self.privacy_sensitive = privacy_sensitive
}
var privacy_sensitive_pubkey: Pubkey {
if privacy_sensitive && redactionReasons.contains(.privacy) {
ANON_PUBKEY
} else {
pubkey
}
}
func get_lnurl() -> String? {
return profiles.lookup_with_timestamp(pubkey)?.unsafeUnownedValue?.lnurl
}
var body: some View {
ZStack (alignment: Alignment(horizontal: .trailing, vertical: .bottom)) {
InnerProfilePicView(url: get_profile_url(picture: picture, pubkey: pubkey, profiles: profiles), fallbackUrl: URL(string: robohash(pubkey)), pubkey: pubkey, size: size, highlight: highlight, disable_animation: disable_animation)
InnerProfilePicView(url: get_profile_url(picture: picture, pubkey: privacy_sensitive_pubkey, profiles: profiles), fallbackUrl: URL(string: robohash(privacy_sensitive_pubkey)), size: size, highlight: highlight, disable_animation: disable_animation)
.onReceive(handle_notify(.profile_updated)) { updated in
guard updated.pubkey == self.pubkey else {
return
+1 -1
View File
@@ -123,7 +123,7 @@ struct ProfileView: View {
var filters = ContentFilters.defaults(damus_state: damus_state)
filters.append(fstate.filter)
switch fstate {
case .posts, .posts_and_replies:
case .posts, .posts_and_replies, .follow_list:
filters.append({ profile.pubkey == $0.pubkey })
case .conversations:
filters.append({ profile.conversation_events.contains($0.id) } )
+12 -2
View File
@@ -12,9 +12,19 @@ struct QuoteRepostsView: View {
@ObservedObject var model: EventsModel
var body: some View {
TimelineView<AnyView>(events: model.events, loading: $model.loading, damus: damus_state, show_friend_icon: true, filter: ContentFilters.default_filters(damus_state: damus_state).filter(ev:))
TimelineView(events: model.events, loading: $model.loading, damus: damus_state, show_friend_icon: true, filter: ContentFilters.default_filters(damus_state: damus_state).filter(ev:)) {
ZStack(alignment: .leading) {
DamusBackground(maxHeight: 250)
.mask(LinearGradient(gradient: Gradient(colors: [.black, .black, .black, .clear]), startPoint: .top, endPoint: .bottom))
Text("Quotes", comment: "Navigation bar title for Quote Reposts view.")
.foregroundStyle(DamusLogoGradient.gradient)
.font(.title.bold())
.padding(.leading, 30)
.padding(.top, 30)
}
}
.ignoresSafeArea()
.padding(.bottom, tabHeight)
.navigationBarTitle(NSLocalizedString("Quotes", comment: "Navigation bar title for Quote Reposts view."))
.onAppear {
model.subscribe()
}
+16 -16
View File
@@ -15,8 +15,9 @@ struct SearchHomeView: View {
@State var search: String = ""
@FocusState private var isFocused: Bool
var content_filter: (NostrEvent) -> Bool {
let filters = ContentFilters.defaults(damus_state: self.damus_state)
func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
var filters = ContentFilters.defaults(damus_state: damus_state)
filters.append(fstate.filter)
return ContentFilters(filters: filters).filter
}
@@ -52,21 +53,20 @@ struct SearchHomeView: View {
loading: $model.loading,
damus: damus_state,
show_friend_icon: true,
filter: { ev in
if !content_filter(ev) {
return false
}
let event_muted = damus_state.mutelist_manager.is_event_muted(ev)
if event_muted {
return false
}
return true
},
filter:content_filter(FilterState.posts),
content: {
AnyView(VStack {
SuggestedHashtagsView(damus_state: damus_state, max_items: 5, events: model.events)
AnyView(VStack(alignment: .leading) {
HStack {
Image(systemName: "sparkles")
.foregroundStyle(PinkGradient)
Text("Follow Packs", comment: "A label indicating that the items below it are follow packs")
.foregroundStyle(PinkGradient)
}
.padding(.top)
.padding(.horizontal)
FollowPackTimelineView<AnyView>(events: model.events, loading: $model.loading, damus: damus_state, show_friend_icon: true,filter:content_filter(FilterState.follow_list)
).padding(.bottom)
Divider()
.frame(height: 1)
@@ -100,6 +100,8 @@ struct AppearanceSettingsView: View {
header: Text("Content filters", comment: "Section title for content filtering/moderation configuration."),
footer: Text("Notes with the #nsfw tag usually contains adult content or other \"Not safe for work\" content", comment: "Section footer clarifying what #nsfw (not safe for work) tags mean")
) {
Toggle(NSLocalizedString("Show replies from your trusted network first", comment: "Setting to show replies in threads from the current user's trusted network first."), isOn: $settings.show_trusted_replies_first)
.toggleStyle(.switch)
Toggle(NSLocalizedString("Hide notes with #nsfw tags", comment: "Setting to hide notes with the #nsfw (not safe for work) tags"), isOn: $settings.hide_nsfw_tagged_content)
.toggleStyle(.switch)
}
@@ -96,6 +96,11 @@ struct DeveloperSettingsView: View {
Toggle(NSLocalizedString("Enable experimental Purple In-app purchase support", comment: "Developer mode setting to enable experimental Purple In-app purchase support."), isOn: $settings.enable_experimental_purple_iap_support)
.toggleStyle(.switch)
if #available(iOS 17, *) {
Toggle(NSLocalizedString("Reset tips on launch", comment: "Developer mode setting to reset tips upon app first launch. Tips are visual contextual hints that highlight new, interesting, or unused features users have not discovered yet."), isOn: $settings.reset_tips_on_launch)
.toggleStyle(.switch)
}
}
}
}
@@ -89,6 +89,7 @@ struct KeySettingsView: View {
.disabled(true)
} else {
Text(sec)
.privacySensitive()
.clipShape(RoundedRectangle(cornerRadius: 5))
}
@@ -67,6 +67,8 @@ struct ZapSettingsView: View {
Section(NSLocalizedString("NWC wallet", comment: "Title for section in zap settings that controls general NWC wallet settings.")) {
Toggle(NSLocalizedString("Disable high balance warning", comment: "Setting to disable high balance warnings on the user's wallet"), isOn: $settings.dismiss_wallet_high_balance_warning)
.toggleStyle(.switch)
Toggle(NSLocalizedString("Hide balance", comment: "Setting to hide wallet balance."), isOn: $settings.hide_wallet_balance)
.toggleStyle(.switch)
}
}
.navigationTitle(NSLocalizedString("Zaps", comment: "Navigation title for zap settings."))
@@ -0,0 +1,25 @@
//
// TrustedNetworkButtonTip.swift
// damus
//
// Created by Terry Yiu on 6/4/25.
//
import TipKit
@available(iOS 17, *)
struct TrustedNetworkButtonTip: Tip {
static let shared = TrustedNetworkButtonTip()
var title: Text {
Text("Toggle visibility of content from outside your trusted network", comment: "Title of tip that informs users what trusted network means and that they can toggle the visibility of content from outside their trusted network.")
}
var message: Text? {
Text("Your trusted network is comprised of profiles you follow and profiles that they follow.", comment: "Description of the tip that informs users what trusted network means.")
}
var image: Image? {
Image(systemName: "network.badge.shield.half.filled")
}
}
@@ -0,0 +1,79 @@
//
// TrustedNetworkButtonTipViewStyle.swift
// damus
//
// Created by Terry Yiu on 6/7/25.
//
import TipKit
// (tyiu): Apple's native popover tips have a lot of rendering and race condition issues --
// text being rendered in the wrong locations or not at all, or the tip gets opened in full screen.
//
// Instead, we are introducing this custom popover tip view style to emulate a similar look and feel.
// The main thing needed from this view style is really just an arrow on the top right corner
// to point to the TrustedNetworkButton on the NotificationsView and DirectMessagesview.
@available(iOS 17, *)
struct TrustedNetworkButtonTipViewStyle: TipViewStyle {
func makeBody(configuration: Configuration) -> some View {
VStack(spacing: 0) {
// Arrow pointing up to the button (positioned at top right)
HStack {
Spacer()
Triangle()
.fill(Color(.secondarySystemBackground))
.frame(width: 24, height: 14)
}
HStack(alignment: .top, spacing: 12) {
// Icon
configuration.image
.foregroundStyle(.tint)
.font(.title2)
VStack(alignment: .leading, spacing: 4) {
configuration.title
.font(.headline)
.fontWeight(.semibold)
.foregroundStyle(.primary)
configuration.message
.font(.subheadline)
.foregroundStyle(.secondary)
}
Spacer()
Button(action: { configuration.tip.invalidate(reason: .tipClosed) }) {
Image(systemName: "xmark")
.fontWeight(.semibold)
.foregroundStyle(Color(.tertiaryLabel))
}
.buttonStyle(PlainButtonStyle())
}
.padding(.horizontal, 14)
.padding(.vertical, 14)
.background(Color(.secondarySystemBackground))
.clipShape(
.rect(
topLeadingRadius: 20,
bottomLeadingRadius: 20,
bottomTrailingRadius: 20,
topTrailingRadius: 0
)
)
}
}
}
// Custom triangle shape for the popover arrow
struct Triangle: Shape {
func path(in rect: CGRect) -> Path {
var path = Path()
path.move(to: CGPoint(x: rect.midX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.midX, y: rect.minY))
return path
}
}
@@ -0,0 +1,26 @@
//
// TrustedNetworkRepliesTip.swift
// damus
//
// Created by Terry Yiu on 6/7/25.
//
import Foundation
import TipKit
@available(iOS 17, *)
struct TrustedNetworkRepliesTip: Tip {
static let shared = TrustedNetworkRepliesTip()
var title: Text {
Text("Toggle visibility of replies from outside your trusted network", comment: "Title of tip that informs users what trusted network means and that they can toggle the visibility of threaded replies from outside their trusted network.")
}
var message: Text? {
Text("Your trusted network is comprised of profiles you follow and profiles that they follow.", comment: "Description of the tip that informs users what trusted network means.")
}
var image: Image? {
Image(systemName: "network.badge.shield.half.filled")
}
}
+46 -17
View File
@@ -9,48 +9,77 @@ import SwiftUI
struct BalanceView: View {
var balance: Int64?
@Binding var hide_balance: Bool
var body: some View {
VStack(spacing: 5) {
Text("Current balance", comment: "Label for displaying current wallet balance")
.foregroundStyle(DamusColors.neutral6)
if let balance {
self.numericalBalanceView(text: NumberFormatter.localizedString(from: NSNumber(integerLiteral: Int(balance)), number: .decimal))
NumericalBalanceView(text: NumberFormatter.localizedString(from: NSNumber(integerLiteral: Int(balance)), number: .decimal), hide_balance: $hide_balance)
}
else {
// Make sure we do not show any numeric value to the user when still loading (or when failed to load)
// This is important because if we show a numeric value like "zero" when things are not loaded properly, we risk scaring the user into thinking that they have lost funds.
self.numericalBalanceView(text: "??")
Text(verbatim: "??")
.lineLimit(1)
.minimumScaleFactor(0.70)
.font(.veryVeryLargeTitle)
.fontWeight(.heavy)
.foregroundStyle(PinkGradient)
.redacted(reason: .placeholder)
.shimmer(true)
}
}
}
}
struct NumericalBalanceView: View {
let text: String
@Binding var hide_balance: Bool
func numericalBalanceView(text: String) -> some View {
HStack {
Text(verbatim: text)
.lineLimit(1)
.minimumScaleFactor(0.70)
.font(.veryVeryLargeTitle)
.fontWeight(.heavy)
.foregroundStyle(PinkGradient)
HStack(alignment: .top) {
Text("SATS", comment: "Abbreviation for Satoshis, smallest bitcoin unit")
.font(.caption)
var body: some View {
Group {
if hide_balance {
Text(verbatim: "*****")
.lineLimit(1)
.minimumScaleFactor(0.70)
.font(.veryVeryLargeTitle)
.fontWeight(.heavy)
.foregroundStyle(PinkGradient)
} else {
HStack {
Text(verbatim: text)
.lineLimit(1)
.minimumScaleFactor(0.70)
.font(.veryVeryLargeTitle)
.fontWeight(.heavy)
.foregroundStyle(PinkGradient)
HStack(alignment: .top) {
Text("SATS", comment: "Abbreviation for Satoshis, smallest bitcoin unit")
.font(.caption)
.fontWeight(.heavy)
.foregroundStyle(PinkGradient)
}
}
}
}
.privacySensitive()
.padding(.bottom)
.onTapGesture {
hide_balance.toggle()
}
}
}
struct BalanceView_Previews: PreviewProvider {
@State private static var hide_balance: Bool = false
static var previews: some View {
BalanceView(balance: 100000000)
BalanceView(balance: nil)
BalanceView(balance: 100000000, hide_balance: $hide_balance)
BalanceView(balance: nil, hide_balance: $hide_balance)
}
}
+246
View File
@@ -0,0 +1,246 @@
//
// LnurlAmountView.swift
// damus
//
// Created by Daniel DAquino on 2025-06-18
//
import SwiftUI
import Combine
class LnurlAmountModel: ObservableObject {
@Published var custom_amount: String = "0"
@Published var custom_amount_sats: Int? = 0
@Published var processing: Bool = false
@Published var error: String? = nil
@Published var invoice: String? = nil
@Published var zap_amounts: [ZapAmountItem] = []
func set_defaults(settings: UserSettingsStore) {
let default_amount = settings.default_zap_amount
custom_amount = String(default_amount)
custom_amount_sats = default_amount
zap_amounts = get_zap_amount_items(default_amount)
}
}
/// Enables the user to enter a Bitcoin amount to be sent. Based on `CustomizeZapView`.
struct LnurlAmountView: View {
let damus_state: DamusState
let lnurlString: String
let onInvoiceFetched: (Invoice) -> Void
let onCancel: () -> Void
@StateObject var model: LnurlAmountModel = LnurlAmountModel()
@Environment(\.colorScheme) var colorScheme
@FocusState var isAmountFocused: Bool
init(damus_state: DamusState, lnurlString: String, onInvoiceFetched: @escaping (Invoice) -> Void, onCancel: @escaping () -> Void) {
self.damus_state = damus_state
self.lnurlString = lnurlString
self.onInvoiceFetched = onInvoiceFetched
self.onCancel = onCancel
}
func AmountButton(zapAmountItem: ZapAmountItem) -> some View {
let isSelected = model.custom_amount_sats == zapAmountItem.amount
return Button(action: {
model.custom_amount_sats = zapAmountItem.amount
model.custom_amount = String(zapAmountItem.amount)
}) {
let fmt = format_msats_abbrev(Int64(zapAmountItem.amount) * 1000)
Text(verbatim: "\(zapAmountItem.icon)\n\(fmt)")
.contentShape(Rectangle())
.font(.headline)
.frame(width: 70, height: 70)
.foregroundColor(DamusColors.adaptableBlack)
.background(isSelected ? DamusColors.adaptableWhite : DamusColors.adaptableGrey)
.cornerRadius(15)
.overlay(RoundedRectangle(cornerRadius: 15)
.stroke(DamusColors.purple.opacity(isSelected ? 1.0 : 0.0), lineWidth: 2))
}
}
func amount_parts(_ n: Int) -> [ZapAmountItem] {
var i: Int = -1
let start = n * 4
let end = start + 4
return model.zap_amounts.filter { _ in
i += 1
return i >= start && i < end
}
}
func AmountsPart(n: Int) -> some View {
HStack(alignment: .center, spacing: 15) {
ForEach(amount_parts(n)) { entry in
AmountButton(zapAmountItem: entry)
}
}
}
var AmountGrid: some View {
VStack {
AmountsPart(n: 0)
AmountsPart(n: 1)
}
.padding(10)
}
var CustomAmountTextField: some View {
VStack(alignment: .center, spacing: 0) {
TextField("", text: $model.custom_amount)
.focused($isAmountFocused)
.task {
self.isAmountFocused = true
}
.font(.system(size: 72, weight: .heavy))
.minimumScaleFactor(0.01)
.keyboardType(.numberPad)
.multilineTextAlignment(.center)
.onChange(of: model.custom_amount) { newValue in
if let parsed = handle_string_amount(new_value: newValue) {
model.custom_amount = parsed.formatted()
model.custom_amount_sats = parsed
} else {
model.custom_amount = "0"
model.custom_amount_sats = nil
}
}
let noun = pluralizedString(key: "sats", count: model.custom_amount_sats ?? 0)
Text(noun)
.font(.system(size: 18, weight: .heavy))
}
}
func fetchInvoice() {
guard let amount = model.custom_amount_sats, amount > 0 else {
model.error = NSLocalizedString("Please enter a valid amount", comment: "Error message when no valid amount is entered for LNURL payment")
return
}
model.processing = true
model.error = nil
Task { @MainActor in
// For LNURL payments without zaps, we use nil for zapreq and comment
// We just need the invoice for payment
let msats = Int64(amount) * 1000
// First get the payment request from the LNURL
guard let payreq = await fetch_static_payreq(lnurlString) else {
model.processing = false
model.error = NSLocalizedString("Error fetching LNURL payment information", comment: "Error message when LNURL fetch fails")
return
}
// Then fetch the invoice with the amount
guard let invoiceStr = await fetch_zap_invoice(payreq, zapreq: nil, msats: msats, zap_type: .non_zap, comment: nil) else {
model.processing = false
model.error = NSLocalizedString("Error fetching lightning invoice", comment: "Error message when there was an error fetching a lightning invoice")
return
}
// Decode the invoice to validate it
guard let invoice = decode_bolt11(invoiceStr) else {
model.processing = false
model.error = NSLocalizedString("Invalid lightning invoice received", comment: "Error message when the lightning invoice received from LNURL is invalid")
return
}
// All good, pass the invoice back to the parent view
model.processing = false
onInvoiceFetched(invoice)
}
}
var PayButton: some View {
VStack {
if model.processing {
Text("Processing...", comment: "Text to indicate that the app is in the process of fetching an invoice.")
.padding()
ProgressView()
} else {
Button(action: {
fetchInvoice()
}) {
HStack {
Text("Continue", comment: "Button to proceed with LNURL payment process.")
.font(.system(size: 20, weight: .bold))
}
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
.disabled(model.custom_amount_sats == 0 || model.custom_amount == "0")
.opacity(model.custom_amount_sats == 0 || model.custom_amount == "0" ? 0.5 : 1.0)
.padding(10)
}
if let error = model.error {
Text(error)
.foregroundColor(.red)
.padding()
}
}
}
var CancelButton: some View {
Button(action: onCancel) {
HStack {
Text("Cancel", comment: "Button to cancel the LNURL payment process.")
.font(.headline)
.padding()
}
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
}
.buttonStyle(NeutralButtonStyle())
.padding()
}
var body: some View {
VStack(alignment: .center, spacing: 20) {
ScrollView {
VStack {
Text("Enter Amount", comment: "Header text for LNURL payment amount entry screen")
.font(.title)
.fontWeight(.bold)
.padding()
Text("How much would you like to send?", comment: "Instruction text for LNURL payment amount")
.font(.headline)
.multilineTextAlignment(.center)
.padding(.bottom)
CustomAmountTextField
AmountGrid
PayButton
CancelButton
}
}
}
.onAppear {
model.set_defaults(settings: damus_state.settings)
}
.onTapGesture {
hideKeyboard()
}
}
}
struct LnurlAmountView_Previews: PreviewProvider {
static var previews: some View {
LnurlAmountView(
damus_state: test_damus_state,
lnurlString: "lnurl1dp68gurn8ghj7um9wfmxjcm99e3k7mf0v9cxj0m385ekvcenxc6r2c35xvukxefcv5mkvv34x5ekzd3ev56nyd3hxqurzepexejxxepnxscrvwfnv9nxzcn9xq6xyefhvgcxxcmyxymnserxfq5fns",
onInvoiceFetched: { _ in },
onCancel: {}
)
.frame(width: 400, height: 600)
}
}
+4 -1
View File
@@ -138,7 +138,10 @@ struct NWCSettings: View {
Toggle(NSLocalizedString("Disable high balance warning", comment: "Setting to disable high balance warnings on the user's wallet"), isOn: $settings.dismiss_wallet_high_balance_warning)
.toggleStyle(.switch)
Toggle(NSLocalizedString("Hide balance", comment: "Setting to hide wallet balance."), isOn: $settings.hide_wallet_balance)
.toggleStyle(.switch)
Button(action: {
self.model.disconnect()
dismiss()
+375
View File
@@ -0,0 +1,375 @@
//
// SendPaymentView.swift
// damus
//
// Created by Daniel DAquino on 2025-06-13.
//
import SwiftUI
import CodeScanner
fileprivate let SEND_PAYMENT_TIMEOUT: Duration = .seconds(10)
/// A view that allows a user to pay a lightning invoice
struct SendPaymentView: View {
// MARK: - Helper structures
/// Represents the state of the invoice payment process
enum SendState {
case enterInvoice(scannerMessage: String?)
case confirmPayment(invoice: Invoice)
case enterLnurlAmount(lnurl: String)
case processing
case completed
case failed(error: HumanReadableError)
}
typealias HumanReadableError = ErrorView.UserPresentableError
// MARK: - Immutable members
let damus_state: DamusState
let model: WalletModel
let nwc: WalletConnectURL
@Environment(\.dismiss) var dismiss
// MARK: - State management
@State private var sendState: SendState = .enterInvoice(scannerMessage: nil) {
didSet {
switch sendState {
case .enterInvoice, .confirmPayment, .processing, .enterLnurlAmount:
break
case .completed:
// Refresh wallet to reflect new balance after payment
Task { await WalletConnect.refresh_wallet_information(damus_state: damus_state) }
case .failed:
// Even when a wallet says it has failed, update balance just in case it is a false negative,
// This might prevent the user from accidentally sending a payment twice in case of a bug.
Task { await WalletConnect.refresh_wallet_information(damus_state: damus_state) }
}
}
}
var isShowingScanner: Bool {
if case .enterInvoice = sendState { true } else { false }
}
// MARK: - Views
var body: some View {
VStack(alignment: .center) {
switch sendState {
case .enterInvoice(let scannerMessage):
invoiceInputView(scannerMessage: scannerMessage)
.padding(40)
case .confirmPayment(let invoice):
confirmationView(invoice: invoice)
.padding(40)
case .enterLnurlAmount(let lnurl):
LnurlAmountView(
damus_state: damus_state,
lnurlString: lnurl,
onInvoiceFetched: { invoice in
sendState = .confirmPayment(invoice: invoice)
},
onCancel: {
sendState = .enterInvoice(scannerMessage: nil)
}
)
case .processing:
processingView
.padding(40)
case .completed:
completedView
.padding(40)
case .failed(error: let error):
failedView(error: error)
}
}
}
func invoiceInputView(scannerMessage: String?) -> some View {
VStack(spacing: 20) {
Text("Scan Lightning Invoice", comment: "Title for the invoice scanning screen")
.font(.title2)
.bold()
CodeScannerView(
codeTypes: [.qr],
scanMode: .continuous,
showViewfinder: true, // The scan only seems to work if it fits the bounding box, so let's make this visible to hint that to the users
simulatedData: "lightning:lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r",
completion: handleScan
)
.frame(height: 300)
.clipShape(RoundedRectangle(cornerRadius: 20))
.overlay(
RoundedRectangle(cornerRadius: 20)
.stroke(Color.accentColor, lineWidth: 2)
)
.padding(.horizontal)
VStack(spacing: 15) {
Button(action: {
if let pastedInvoice = getPasteboardContent() {
processUserInput(pastedInvoice)
}
}) {
HStack {
Image(systemName: "doc.on.clipboard")
Text("Paste from Clipboard", comment: "Button to paste invoice from clipboard")
}
.frame(minWidth: 250, maxWidth: .infinity, alignment: .center)
.padding()
}
.buttonStyle(NeutralButtonStyle())
.accessibilityLabel(NSLocalizedString("Paste invoice from clipboard", comment: "Accessibility label for the invoice paste button"))
}
.padding(.horizontal)
if let scannerMessage {
Text(scannerMessage)
.foregroundColor(.secondary)
.padding(.top, 10)
.multilineTextAlignment(.center)
}
Spacer()
}
}
func confirmationView(invoice: Invoice) -> some View {
let insufficientFunds: Bool = (invoice.amount.amount_sats() ?? 0) > (model.balance ?? 0)
return VStack(spacing: 20) {
Text("Confirm Payment", comment: "Title for payment confirmation screen")
.font(.title2)
.bold()
VStack(spacing: 15) {
Text("Amount", comment: "Label for invoice payment amount in confirmation screen")
.font(.headline)
.foregroundStyle(.secondary)
if case .specific(let amount) = invoice.amount {
NumericalBalanceView(text: NumberFormatter.localizedString(from: NSNumber(value: (Double(amount) / 1000.0)), number: .decimal), hide_balance: .constant(false))
}
Text("Bolt11 Invoice", comment: "Label for the bolt11 invoice string in confirmation screen")
.font(.headline)
.foregroundStyle(.secondary)
Text(verbatim: invoice.abbreviated)
.font(.system(.body, design: .monospaced))
.padding()
.background(DamusColors.adaptableGrey)
.cornerRadius(10)
.frame(maxWidth: .infinity)
}
HStack(spacing: 15) {
Button(action: {
sendState = .enterInvoice(scannerMessage: nil)
}) {
Text("Back", comment: "Button to go back to invoice input")
.font(.headline)
.frame(minWidth: 140)
.padding()
}
.buttonStyle(NeutralButtonStyle())
Button(action: {
sendState = .processing
// Process payment
guard let payRequestEv = WalletConnect.pay(url: nwc, pool: damus_state.nostrNetwork.pool, post: damus_state.nostrNetwork.postbox, invoice: invoice.string, zap_request: nil, delay: nil) else {
sendState = .failed(error: .init(
user_visible_description: NSLocalizedString("The payment request could not be made to your wallet provider.", comment: "A human-readable error message"),
tip: NSLocalizedString("Check if your wallet looks configured correctly and try again. If the error persists, please contact support.", comment: "A human-readable tip for an error when a payment request cannot be made to a wallet."),
technical_info: "Cannot form Nostr Event to send to the NWC provider when calling `pay` from the \"send payment\" feature. Wallet provider relay: \"\(nwc.relay)\""
))
return
}
Task {
do {
let result = try await model.waitForResponse(for: payRequestEv.id, timeout: SEND_PAYMENT_TIMEOUT)
guard case .pay_invoice(_) = result else {
sendState = .failed(error: .init(
user_visible_description: NSLocalizedString("Received an incorrect or unexpected response from the wallet provider. This looks like an issue with your wallet provider.", comment: "A human-readable error message"),
tip: NSLocalizedString("Try again. If the error persists, please contact your wallet provider and/or our support team.", comment: "A human-readable tip for an error when a payment request cannot be made to a wallet."),
technical_info: "Expected a `pay_invoice` response for the request, but received a different type of response from the NWC wallet provider. Wallet provider relay: \"\(nwc.relay)\""
))
return
}
sendState = .completed
}
catch {
if let error = error as? WalletModel.WaitError {
switch error {
case .timeout:
sendState = .failed(error: .init(
user_visible_description: NSLocalizedString("The payment request did not receive a response and the request timed-out.", comment: "A human-readable error message"),
tip: NSLocalizedString("Check if the invoice is valid, your wallet is online, configured correctly, and try again. If the error persists, please contact support and/or your wallet provider.", comment: "A human-readable tip guiding the user on what to do when seeing a timeout error while sending a wallet payment."),
technical_info: "Payment request timed-out. Wallet provider relay: \"\(nwc.relay)\""
))
}
}
else if let error = error as? WalletConnect.WalletResponseErr,
let humanReadableError = error.humanReadableError {
sendState = .failed(error: humanReadableError)
}
else {
sendState = .failed(error: .init(
user_visible_description: NSLocalizedString("An unexpected error occurred.", comment: "A human-readable error message"),
tip: NSLocalizedString("Please try again. If the error persists, please contact support.", comment: "A human-readable tip guiding the user on what to do when seeing an unexpected error while sending a wallet payment."),
technical_info: "Unexpected error thrown while waiting for payment request response. Wallet provider relay: \"\(nwc.relay)\"; Error: \(error)"
))
}
}
}
}) {
Text("Confirm", comment: "Button to confirm payment")
.font(.headline)
.frame(minWidth: 140)
.padding()
}
.buttonStyle(GradientButtonStyle(padding: 0))
.disabled(insufficientFunds)
.opacity(insufficientFunds ? 0.5 : 1.0)
}
if insufficientFunds {
Text("You do not have enough funds to pay for this invoice.", comment: "Label on invoice payment screen, indicating user has insufficient funds")
.foregroundColor(.secondary)
.padding(.top, 10)
.multilineTextAlignment(.center)
}
Spacer()
}
}
var processingView: some View {
VStack(spacing: 30) {
Text("Processing Payment", comment: "Title for payment processing screen")
.font(.title2)
.bold()
ProgressView()
.scaleEffect(1.5)
.padding()
Text("Please wait while your payment is being processed…", comment: "Message while payment is being processed")
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
.padding()
Spacer()
}
}
var completedView: some View {
VStack(spacing: 30) {
Image(systemName: "checkmark.circle.fill")
.resizable()
.frame(width: 80, height: 80)
.foregroundColor(.green)
Text("Payment Sent!", comment: "Title for successful payment screen")
.font(.title2)
.bold()
Text("Your payment has been successfully sent.", comment: "Message for successful payment")
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
.padding()
Button(action: {
dismiss()
}) {
Text("Done", comment: "Button to dismiss successful payment screen")
.font(.headline)
.frame(minWidth: 200)
}
.buttonStyle(GradientButtonStyle())
Spacer()
}
}
func failedView(error: HumanReadableError) -> some View {
ScrollView {
VStack {
ErrorView(damus_state: damus_state, error: error)
Button(action: {
sendState = .enterInvoice(scannerMessage: nil)
}) {
Text("Try Again", comment: "Button to retry payment")
.font(.headline)
.frame(minWidth: 200)
.padding()
}
.buttonStyle(GradientButtonStyle(padding: 0))
}
}
}
func handleScan(result: Result<ScanResult, ScanError>) {
switch result {
case .success(let result):
processUserInput(result.string)
case .failure(let error):
sendState = .enterInvoice(scannerMessage: NSLocalizedString("Failed to scan QR code, please try again.", comment: "Error message for failed QR scan"))
}
}
func processUserInput(_ text: String) {
if let result = parseScanData(text) {
switch result {
case .invoice(let invoice):
if invoice.amount == .any {
sendState = .enterInvoice(scannerMessage: NSLocalizedString("Sorry, we do not support paying invoices without amount yet. Please scan an invoice with an amount.", comment: "A user-readable message indicating that the lightning invoice they scanned or pasted is not supported and is missing an amount."))
} else {
sendState = .confirmPayment(invoice: invoice)
}
case .lnurl(let lnurlString):
sendState = .enterLnurlAmount(lnurl: lnurlString)
}
} else {
sendState = .enterInvoice(scannerMessage: NSLocalizedString("This does not appear to be a valid Lightning invoice or LNURL.", comment: "A user-readable message indicating that the scanned or pasted content was not a valid lightning invoice or LNURL."))
}
}
func parseScanData(_ text: String) -> ScanData? {
let processedString = text.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if let invoice = Invoice.from(string: processedString) {
return .invoice(invoice)
}
if let _ = processedString.range(of: "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$", options: .regularExpression) {
guard let lnurl = lnaddress_to_lnurl(processedString) else { return nil }
return .lnurl(lnurl)
}
if processedString.hasPrefix("lnurl") {
return .lnurl(processedString)
}
return nil
}
enum ScanData {
case invoice(Invoice)
case lnurl(String)
}
// Helper function to get pasteboard content
func getPasteboardContent() -> String? {
return UIPasteboard.general.string
}
}
+66 -46
View File
@@ -8,39 +8,47 @@
import SwiftUI
struct TransactionView: View {
@Environment(\.redactionReasons) var redactionReasons
let damus_state: DamusState
var transaction: WalletConnect.Transaction
@Binding var hide_balance: Bool
var body: some View {
let redactedForPrivacy = redactionReasons.contains(.privacy)
let isIncomingTransaction = transaction.type == "incoming"
let txType = isIncomingTransaction ? "arrow-bottom-left" : "arrow-top-right"
let txColor = isIncomingTransaction ? DamusColors.success : Color.gray
let txColor = (isIncomingTransaction && !hide_balance && !redactedForPrivacy) ? DamusColors.success : Color.gray
let txOp = isIncomingTransaction ? "+" : "-"
let created_at = Date.init(timeIntervalSince1970: TimeInterval(transaction.created_at))
let formatter = RelativeDateTimeFormatter()
let relativeDate = formatter.localizedString(for: created_at, relativeTo: Date.now)
let event = decode_nostr_event_json(transaction.description ?? "") ?? transaction.metadata?.nostr
let pubkey = self.pubkeyToDisplay(for: event, isIncomingTransaction: isIncomingTransaction) ?? ANON_PUBKEY
let pubkey = self.pubkeyToDisplay(for: event, isIncomingTransaction: isIncomingTransaction)
VStack(alignment: .leading) {
HStack(alignment: .center) {
ZStack {
ProfilePicView(pubkey: pubkey, size: 45, highlight: .custom(.damusAdaptableBlack, 0.1), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
ProfilePicView(pubkey: pubkey ?? ANON_PUBKEY, size: 45, highlight: .custom(.damusAdaptableBlack, 0.1), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation, privacy_sensitive: true)
.onTapGesture {
damus_state.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
if let pubkey {
damus_state.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
}
}
Image(txType)
.resizable()
.frame(width: 18, height: 18)
.foregroundColor(.white)
.padding(2)
.background(txColor)
.clipShape(Circle())
.overlay(Circle().stroke(Color.damusAdaptableWhite, lineWidth: 1.0))
.padding(.top, 25)
.padding(.leading, 35)
if !hide_balance && !redactedForPrivacy {
Image(txType)
.resizable()
.frame(width: 18, height: 18)
.foregroundColor(.white)
.padding(2)
.background(txColor)
.clipShape(Circle())
.overlay(Circle().stroke(Color.damusAdaptableWhite, lineWidth: 1.0))
.padding(.top, 25)
.padding(.leading, 35)
}
}
VStack(alignment: .leading, spacing: 10) {
@@ -58,10 +66,17 @@ struct TransactionView: View {
Spacer()
Text(verbatim: "\(txOp) \(format_msats(transaction.amount))")
.font(.headline)
.foregroundColor(txColor)
.bold()
if hide_balance {
Text(verbatim: "*****")
.font(.headline)
.foregroundColor(txColor)
.bold()
} else {
Text(verbatim: "\(txOp) \(format_msats(transaction.amount))")
.font(.headline)
.foregroundColor(txColor)
.bold()
}
}
.frame(maxWidth: .infinity, minHeight: 75, alignment: .center)
.padding(.horizontal, 10)
@@ -84,17 +99,15 @@ struct TransactionView: View {
}
}
func userDisplayName(pubkey: Pubkey) -> String {
let profile_txn = damus_state.profiles.lookup(id: pubkey, txn_name: "txview-profile")
let profile = profile_txn?.unsafeUnownedValue
if let display_name = profile?.display_name {
return display_name
} else if let name = profile?.name {
return "@" + name
} else {
func userDisplayName(pubkey: Pubkey?) -> String {
guard let pubkey else {
return NSLocalizedString("Unknown", comment: "A name label for an unknown user")
}
let profile_txn = damus_state.profiles.lookup(id: pubkey, txn_name: "txview-profile")
let profile = profile_txn?.unsafeUnownedValue
return Profile.displayName(profile: profile, pubkey: pubkey).displayName
}
}
@@ -107,27 +120,32 @@ struct TransactionsView: View {
transactions?.sorted(by: { $0.created_at > $1.created_at })
}
@Binding var hide_balance: Bool
var body: some View {
VStack(alignment: .leading, spacing: 10) {
Text("Latest transactions", comment: "Heading for latest wallet transactions list")
.foregroundStyle(DamusColors.neutral6)
if let sortedTransactions {
if sortedTransactions.isEmpty {
emptyTransactions
} else {
ForEach(sortedTransactions, id: \.self) { transaction in
TransactionView(damus_state: damus_state, transaction: transaction)
Group {
if let sortedTransactions {
if sortedTransactions.isEmpty {
emptyTransactions
} else {
ForEach(sortedTransactions, id: \.self) { transaction in
TransactionView(damus_state: damus_state, transaction: transaction, hide_balance: $hide_balance)
}
}
}
else {
// Make sure we do not show "No transactions yet" to the user when still loading (or when failed to load)
// This is important because if we show that when things are not loaded properly, we risk scaring the user into thinking that they have lost funds.
emptyTransactions
.redacted(reason: .placeholder)
.shimmer(true)
}
}
else {
// Make sure we do not show "No transactions yet" to the user when still loading (or when failed to load)
// This is important because if we show that when things are not loaded properly, we risk scaring the user into thinking that they have lost funds.
emptyTransactions
.redacted(reason: .placeholder)
.shimmer(true)
}
.privacySensitive()
}
}
@@ -154,8 +172,10 @@ struct TransactionsView_Previews: PreviewProvider {
static let transaction3: WalletConnect.Transaction = WalletConnect.Transaction(type: "outgoing", invoice: "", description: "", description_hash: "", preimage: "", payment_hash: "123456789042", amount: 303000, fees_paid: 0, created_at: 1737590101, expires_at: 0, settled_at: 0, metadata: nil)
static let transaction4: WalletConnect.Transaction = WalletConnect.Transaction(type: "incoming", invoice: "", description: "", description_hash: "", preimage: "", payment_hash: "1234567890662", amount: 720000, fees_paid: 0, created_at: 1737090300, expires_at: 0, settled_at: 0, metadata: nil)
static var test_transactions: [WalletConnect.Transaction] = [transaction1, transaction2, transaction3, transaction4]
@State private static var hide_balance: Bool = false
static var previews: some View {
TransactionsView(damus_state: tds, transactions: test_transactions)
TransactionsView(damus_state: tds, transactions: test_transactions, hide_balance: $hide_balance)
}
}
+25 -15
View File
@@ -12,9 +12,11 @@ let WALLET_WARNING_THRESHOLD: UInt64 = 100000
struct WalletView: View {
let damus_state: DamusState
@State var show_settings: Bool = false
@State var show_send_sheet: Bool = false
@ObservedObject var model: WalletModel
@ObservedObject var settings: UserSettingsStore
@State private var showBalance: Bool = false
init(damus_state: DamusState, model: WalletModel? = nil) {
self.damus_state = damus_state
self._model = ObservedObject(wrappedValue: model ?? damus_state.wallet)
@@ -47,6 +49,7 @@ struct WalletView: View {
.bold()
.foregroundStyle(.damusWarningTertiary)
}
.privacySensitive()
.padding()
.overlay(
RoundedRectangle(cornerRadius: 20)
@@ -56,9 +59,22 @@ struct WalletView: View {
VStack(spacing: 5) {
BalanceView(balance: model.balance)
BalanceView(balance: model.balance, hide_balance: $settings.hide_wallet_balance)
TransactionsView(damus_state: damus_state, transactions: model.transactions)
Button(action: {
show_send_sheet = true
}) {
HStack {
Image(systemName: "paperplane.fill")
Text("Send", comment: "Button label to send bitcoin payment from wallet")
.font(.headline)
}
.padding(.horizontal, 10)
}
.buttonStyle(GradientButtonStyle())
.padding(.bottom, 20)
TransactionsView(damus_state: damus_state, transactions: model.transactions, hide_balance: $settings.hide_wallet_balance)
}
}
.navigationTitle(NSLocalizedString("Wallet", comment: "Navigation title for Wallet view"))
@@ -102,23 +118,17 @@ struct WalletView: View {
.presentationDragIndicator(.visible)
.presentationDetents([.large])
}
.sheet(isPresented: $show_send_sheet) {
SendPaymentView(damus_state: damus_state, model: model, nwc: nwc)
.presentationDragIndicator(.visible)
.presentationDetents([.large])
}
}
}
@MainActor
func updateWalletInformation() async {
guard let url = damus_state.settings.nostr_wallet_connect,
let nwc = WalletConnectURL(str: url) else {
return
}
let flusher: OnFlush? = nil
let delay = 0.0 // We don't need a delay when fetching a transaction list or balance
WalletConnect.request_transaction_list(url: nwc, pool: damus_state.nostrNetwork.pool, post: damus_state.nostrNetwork.postbox, delay: delay, on_flush: flusher)
WalletConnect.request_balance_information(url: nwc, pool: damus_state.nostrNetwork.pool, post: damus_state.nostrNetwork.postbox, delay: delay, on_flush: flusher)
return
await WalletConnect.update_wallet_information(damus_state: damus_state)
}
}
Binary file not shown.
+32 -16
View File
@@ -50,6 +50,22 @@
<string>Folge ich</string>
</dict>
</dict>
<key>hellthread_notifications_disabled</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@HELLTHREAD_PROFILES@</string>
<key>HELLTHREAD_PROFILES</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Benachrichtigungen ausblenden, die mehr als %d Profil markieren</string>
<key>other</key>
<string>Benachrichtigungen ausblenden, die mehr als %d Profile markieren.</string>
</dict>
</dict>
<key>imports_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
@@ -82,6 +98,22 @@
<string>%2$@ und %1$d weitere teilten</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>Zitate</string>
<key>other</key>
<string>Zitat</string>
</dict>
</dict>
<key>reacted_tagged_in_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
@@ -242,22 +274,6 @@
<string>geteilte Beiträge</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>Zitate</string>
<key>other</key>
<string>Zitat</string>
</dict>
</dict>
<key>sats</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
+32
View File
@@ -2,6 +2,22 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>follow_pack_user_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@FOLLOW_PACK_USERS@</string>
<key>FOLLOW_PACK_USERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>user</string>
<key>other</key>
<string>users</string>
</dict>
</dict>
<key>followed_by_three_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
@@ -82,6 +98,22 @@
<string>Imports</string>
</dict>
</dict>
<key>notes_from_three_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Notes from %2$@, %3$@, %4$@ &amp; %1$d other in your trusted network</string>
<key>other</key>
<string>Notes from %2$@, %3$@, %4$@ &amp; %1$d others in your trusted network</string>
</dict>
</dict>
<key>people_reposted_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
File diff suppressed because it is too large Load Diff
@@ -28,7 +28,7 @@
"comment" : "Amount of money required to publish to the Nostr relay. In English, this would look something like '10 sats / event', meaning it costs 10 sats to publish one event."
},
"%@ %@" : {
"comment" : "Sentence composed of 2 variables to describe how many imports were performed from loading a NostrScript. In source English, the first variable is the number of imports, and the second variable is 'Import' or 'Imports'.\nSentence composed of 2 variables to describe how many people are following a user. In source English, the first variable is the number of followers, and the second variable is 'Follower' or 'Followers'.\nSentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.\nSentence composed of 2 variables to describe how many quoted reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.\nSentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.\nSentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.\nSentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.\nSentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.",
"comment" : "Sentence composed of 2 variables to describe how many imports were performed from loading a NostrScript. In source English, the first variable is the number of imports, and the second variable is 'Import' or 'Imports'.\nSentence composed of 2 variables to describe how many people are following a user. In source English, the first variable is the number of followers, and the second variable is 'Follower' or 'Followers'.\nSentence composed of 2 variables to describe how many people are in the follow pack. In source English, the first variable is the number of users, and the second variable is 'user' or 'users'.\nSentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.\nSentence composed of 2 variables to describe how many quoted reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.\nSentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.\nSentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.\nSentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.\nSentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.",
"localizations" : {
"en-US" : {
"stringUnit" : {
@@ -160,7 +160,7 @@
"comment" : "Heading for some advice text to help the user with an error"
},
"All" : {
"comment" : "Human-readable short description of the 'friends filter' when it is set to 'all'\nLabel for filter for all notifications."
"comment" : "Human-readable short description of the 'trusted network filter' when it is disabled, and therefore is showing all content.\nLabel for filter for all notifications."
},
"All recent notes" : {
"comment" : "A label indicating that the notes being displayed below it are all recent notes"
@@ -177,6 +177,9 @@
"An additional percentage of each zap will be sent to support Damus development" : {
"comment" : "Text indicating that they can contribute zaps to support Damus development."
},
"An internal error occurred in your wallet." : {
"comment" : "Error description for an internal error"
},
"An unexpected error happened while trying to perform this action. Please contact support." : {
"comment" : "Error message for a failed reset/repair operation"
},
@@ -186,6 +189,9 @@
"An unknown error occurred while adding a relay." : {
"comment" : "Title of an unknown relay error message."
},
"An unspecified error occurred in your wallet." : {
"comment" : "Error description for an unspecified error"
},
"Animations" : {
"comment" : "Toggle to enable or disable image animation"
},
@@ -303,6 +309,9 @@
"Check the address and/or the relay list." : {
"comment" : "Human readable tip for error"
},
"Check your account permissions or contact support." : {
"comment" : "Tip for restricted operation"
},
"Check your internet connection and try again. If the error persists, contact support." : {
"comment" : "Error tip when user tries to create the one-click Coinos wallet setup but fails for a generic reason."
},
@@ -330,7 +339,7 @@
"Close" : {
"comment" : "Button label giving the user the option to close the sheet due to not being logged in.\nButton label giving the user the option to close the sheet from which they shared content\nButton label giving the user the option to close the sheet from which they were trying share.\nButton label giving the user the option to close the sheet from which they were trying to share.\nButton label giving the user the option to close the view when no content is available to share"
},
"Coinos is a service operated by a third-party. We have no access to your Coinos wallet." : {
"Coinos is a service operated by a third-party. The Damus team has no access to your wallet." : {
"comment" : "Small caption with a disclaimer that Damus does not own or have access to Coinos wallets, Coinos is a third-party service."
},
"Coming soon" : {
@@ -387,6 +396,9 @@
"Copied" : {
"comment" : "Label indicating that a user's key was copied."
},
"Copied!" : {
"comment" : "Label indicating that the error technical information was successfully copied to the clipboard, which shows up as soon as the user clicks the copy button."
},
"Copy" : {
"comment" : "Button to copy a relay server address.\nButton to copy the value found.\nContext menu option for copying the version of damus."
},
@@ -417,6 +429,9 @@
"Copy Report ID" : {
"comment" : "Button to copy report ID."
},
"Copy technical information" : {
"comment" : "Button label to allow user to copy technical information from an error screen (usually to provide our support team for further troubleshooting)"
},
"Copy text" : {
"comment" : "Context menu option for copying the text from an note."
},
@@ -453,6 +468,9 @@
"Create new wallet" : {
"comment" : "Button text for creating a new wallet."
},
"Created by %@" : {
"comment" : "Lets the user know who created this follow pack."
},
"Current balance" : {
"comment" : "Label for displaying current wallet balance"
},
@@ -501,6 +519,9 @@
"Developer Mode enables features and options that may help developers diagnose issues and improve this app. Most users will not need Developer Mode." : {
"comment" : "Section header for Developer Settings view"
},
"Disable high balance warning" : {
"comment" : "Setting to disable high balance warnings on the user's wallet"
},
"Discard changes?" : {
"comment" : "Alert user that changes have been made."
},
@@ -511,7 +532,7 @@
"comment" : "Text for button to disconnect from Nostr Wallet Connect lightning wallet."
},
"Dismiss" : {
"comment" : "Button to dismiss alert\nButton to dismiss error"
"comment" : "Button label to dismiss the safety reminder that the user's wallet has a high balance\nButton to dismiss alert\nButton to dismiss error"
},
"DMs" : {
"comment" : "Navigation title for DMs view, where DM is the English abbreviation for Direct Message.\nNavigation title for view of DMs, where DM is an English abbreviation for Direct Message.\nPicker option for DM selector for seeing only DMs that have been responded to. DM is the English abbreviation for Direct Message.\nSetting to enable DM Local Notification\nToolbar label for DMs view, where DM is the English abbreviation for Direct Message."
@@ -540,6 +561,9 @@
"Edit profile picture" : {
"comment" : "Accessibility label for a button that edits a profile picture"
},
"Empty error message" : {
"comment" : "A human readable placeholder to indicate that the error message is empty"
},
"Enable experimental Purple API support" : {
"comment" : "Developer mode setting to enable experimental Purple API support."
},
@@ -639,6 +663,9 @@
"Follow me on Nostr" : {
"comment" : "Text on QR code view to prompt viewer looking at screen to follow the user."
},
"Follow Packs" : {
"comment" : "A label indicating that the items below it are follow packs"
},
"Followed by %@" : {
"comment" : "Text to indicate that the user is followed by one of our follows."
},
@@ -694,9 +721,6 @@
"Free" : {
"comment" : "Dropdown option for selecting Free plan for DeepL translation service."
},
"Friends of friends" : {
"comment" : "Human-readable short description of the 'friends filter' when it is set to 'friends-of-friends'"
},
"General" : {
"comment" : "Section header for general damus notifications user configuration"
},
@@ -742,6 +766,9 @@
"Hide all 🤙's" : {
"comment" : "Section footer describing OnlyZaps mode"
},
"Hide balance" : {
"comment" : "Setting to hide wallet balance."
},
"Hide notes with #nsfw tags" : {
"comment" : "Setting to hide notes with the #nsfw (not safe for work) tags"
},
@@ -985,6 +1012,9 @@
"No content available to share" : {
"comment" : "Title indicating that there was no available content to share"
},
"No cover image" : {
"comment" : "Text letting user know there is no cover image."
},
"No image is currently setup" : {
"comment" : "Accessibility value on image control"
},
@@ -1069,6 +1099,31 @@
"Notes & Replies" : {
"comment" : "Label for filter for seeing notes and replies (instead of only notes)."
},
"Notes from %@" : {
"comment" : "Text to indicate that notes from one pubkey in our trusted network are shown below."
},
"Notes from %@ & %@" : {
"comment" : "Text to indicate that notes from two pubkeys in our trusted network are shown below.",
"localizations" : {
"en-US" : {
"stringUnit" : {
"state" : "new",
"value" : "Notes from %1$@ & %2$@"
}
}
}
},
"Notes from %@, %@ & %@" : {
"comment" : "Text to indicate that notes from three pubkeys in our trusted network are shown below.",
"localizations" : {
"en-US" : {
"stringUnit" : {
"state" : "new",
"value" : "Notes from %1$@, %2$@ & %3$@"
}
}
}
},
"Notes with the #nsfw tag usually contains adult content or other \"Not safe for work\" content" : {
"comment" : "Section footer clarifying what #nsfw (not safe for work) tags mean"
},
@@ -1099,6 +1154,9 @@
"Nudity" : {
"comment" : "Description of report type for nudity."
},
"NWC wallet" : {
"comment" : "Title for section in zap settings that controls general NWC wallet settings."
},
"Ok" : {
"comment" : "Button to dismiss the alert."
},
@@ -1160,7 +1218,7 @@
"comment" : "Label to display that authentication to a server is pending."
},
"People" : {
"comment" : "Label for filter for seeing only people follows."
"comment" : "Label for filter for seeing only people follows.\nLabel for filter for seeing the people in this follow pack."
},
"People will be able to send you cash from your profile. No money goes to Damus." : {
"comment" : "The description for one of the \"Why add Zaps?\" boxes"
@@ -1171,6 +1229,9 @@
"Plan" : {
"comment" : "Prompt selection of DeepL subscription plan to perform machine translations on notes"
},
"Please check for updates or contact your wallet provider." : {
"comment" : "Tip for not implemented error"
},
"Please check the address and try again" : {
"comment" : "Tip for an error where the relay address being added is invalid"
},
@@ -1183,9 +1244,18 @@
"Please contact support." : {
"comment" : "Tip for an unknown relay error message."
},
"Please contact the developer of your wallet provider for help." : {
"comment" : "Human readable error description for an unknown error raised by a wallet provider."
},
"Please contact the person who provided the link, and ask for another link." : {
"comment" : "User-visible tip on what to do if a link contains a deprecated \"nrelay\" reference."
},
"Please copy the technical info and send it to our support team." : {
"comment" : "Tip on how to resolve issue when wallet returns an invalid response"
},
"Please deposit more funds and try again." : {
"comment" : "Tip for insufficient balance errors"
},
"Please double-check the checkout web page, or go to the Side Menu → \"Purple\" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue." : {
"comment" : "User-facing tips on what to do if a Purple welcome link doesn't work"
},
@@ -1198,18 +1268,27 @@
"Please try again later or contact support if the issue persists." : {
"comment" : "Human readable tip for error"
},
"Please try again or contact your wallet provider for further assistance." : {
"comment" : "Tip for unspecified error"
},
"Please try again, check the URL for typos, or contact support for further help." : {
"comment" : "User visible error tips"
},
"Please try opening this content on another Nostr app that supports this type of content." : {
"comment" : "User-visible advice on what to do if they see the error indicating a note has an unknown kind or is unsupported for viewing."
},
"Please verify your credentials or permissions." : {
"comment" : "Tip for unauthorized access"
},
"Point your camera to a QR code…" : {
"comment" : "Text on QR code camera view instructing user to point to QR code"
},
"Post" : {
"comment" : "Button to post a note."
},
"Posts" : {
"comment" : "Label for filter for seeing the posts from the people in this follow pack."
},
"Private" : {
"comment" : "Button text to indicate that the zap type is a private zap.\nHeading indicating that this application keeps personally identifiable information private. A sentence describing what is done to keep data private comes after this heading.\nPicker option to indicate that a zap should be sent privately and not identify the user to the public."
},
@@ -1336,6 +1415,9 @@
"Repair relay list" : {
"comment" : "Button to repair relay list."
},
"Replies outside your trusted network" : {
"comment" : "Section title in thread for replies from outside of the current user's trusted network, which is their follows and follows of follows."
},
"Reply" : {
"comment" : "Accessibility label for reply button"
},
@@ -1395,6 +1477,9 @@
"Reset contact list" : {
"comment" : "Button to reset contact list."
},
"Reset tips on launch" : {
"comment" : "Developer mode setting to reset tips upon app first launch. Tips are visual contextual hints that highlight new, interesting, or unused features users have not discovered yet."
},
"Retry" : {
"comment" : "Button to retry completing account creation after an error occurred."
},
@@ -1560,6 +1645,9 @@
"Show profile action sheets" : {
"comment" : "Setting to show profile action sheets when clicking on a user's profile picture"
},
"Show replies from your trusted network first" : {
"comment" : "Setting to show replies in threads from the current user's trusted network first."
},
"Show wallet selector" : {
"comment" : "Toggle to show or hide selection of wallet."
},
@@ -1701,6 +1789,9 @@
"This device's in-app purchase is registered to a different Nostr account. Unable to manage this Purple account. If you believe this was a mistake, please contact us via support@damus.io." : {
"comment" : "Notice label that user cannot manage their In-App purchases"
},
"This feature is not implemented by your wallet." : {
"comment" : "Error description for not implemented feature"
},
"This is a public key, you will not be able to make notes or interact in any way. This is used for viewing accounts from their perspective." : {
"comment" : "Warning that the inputted account key is a public key and the result of what happens because of it."
},
@@ -1713,6 +1804,9 @@
"This note contains too many items and cannot be rendered" : {
"comment" : "Error message indicating that a note is too big and cannot be rendered"
},
"This operation is restricted by your wallet." : {
"comment" : "Error description for restricted operation"
},
"This relay is already in your list." : {
"comment" : "Human readable tip for error"
},
@@ -1728,6 +1822,12 @@
"Toggle key visibility" : {
"comment" : "Accessibility label for toggling the visibility of the private key input field"
},
"Toggle visibility of content from outside your trusted network" : {
"comment" : "Title of tip that informs users what trusted network means and that they can toggle the visibility of content from outside their trusted network."
},
"Toggle visibility of replies from outside your trusted network" : {
"comment" : "Title of tip that informs users what trusted network means and that they can toggle the visibility of threaded replies from outside their trusted network."
},
"Top hits" : {
"comment" : "A label indicating that the notes being displayed below it are all top note search results"
},
@@ -1758,9 +1858,15 @@
"Truncate timeline text" : {
"comment" : "Setting to truncate text in timeline"
},
"Trusted Network" : {
"comment" : "Human-readable short description of the 'trusted network filter' when it is enabled, and therefore showing content from only the trusted network."
},
"Try checking the link again, your internet connection, or contact the person who provided you the link for help." : {
"comment" : "Tips on what to do if a note cannot be found."
},
"Try restarting your wallet or contacting support if the problem persists." : {
"comment" : "Tip for internal error"
},
"Type %@ to delete" : {
"comment" : "Text field prompt asking user to type DELETE in all caps to confirm that they want to proceed with deleting their account."
},
@@ -1798,7 +1904,7 @@
"comment" : "Label represnting a button that the user can tap to unmute a given hashtag so they start seeing it in their feed again."
},
"Untitled" : {
"comment" : "Title of longform event if it is untitled."
"comment" : "Title of follow list event if it is untitled.\nTitle of longform event if it is untitled."
},
"Update" : {
"comment" : "Update button text for updating image url."
@@ -1854,9 +1960,24 @@
"Visit the Damus website on a web browser to manage billing" : {
"comment" : "Instruction on how to manage billing externally"
},
"Wait a few moments, and then try again." : {
"comment" : "Tip for rate limit error"
},
"Wait for the quota to reset, or configure your wallet provider to allow a higher limit." : {
"comment" : "Tip for quota exceeded"
},
"Wallet" : {
"comment" : "Navigation title for Wallet view\nNavigation title for attaching Nostr Wallet Connect lightning wallet.\nSidebar menu label for Wallet view.\nTitle for section in zap settings that controls the Lightning wallet selection."
},
"Wallet provider returned a response that we could not decrypt." : {
"comment" : "Error description shown to the user when a response from the wallet provider contains data the app could not decrypt."
},
"Wallet provider returned a response that we do not understand." : {
"comment" : "Error description shown to the user when a response from the wallet provider contains data the app does not understand"
},
"Wallet provider returned an invalid response." : {
"comment" : "Error description shown to the user when a response from the wallet provider is invalid"
},
"WARNING:\n\nThis will attempt to repair your relay list based on other information we have. You may lose any relays you have added manually. Only proceed if you have lost your relay list beyond recoverability or if you are ok with losing any manually added relays." : {
"comment" : "Alert for repairing the user's relay list."
},
@@ -1926,6 +2047,9 @@
"you" : {
"comment" : "You, in this context, is the person who controls their own social network. You is used in the context of a larger sentence that welcomes the reader to the social network that they control themself."
},
"You are not authorized to perform this action with your wallet." : {
"comment" : "Error description for unauthorized access"
},
"You cannot share content because you are not logged in. Please close this view, log in to your account, and try again." : {
"comment" : "Label explaining that sharing cannot proceed because the user is not logged in."
},
@@ -1953,6 +2077,9 @@
"You unlocked" : {
"comment" : "Part 1 of 2 in message 'You unlocked automatic translations' the user gets when they sign up for Damus Purple"
},
"Your connected wallet raised an unknown error. Message: %s" : {
"comment" : "Human readable error description for unknown error"
},
"Your content is being broadcasted to the network. Please wait." : {
"comment" : "Label explaining that their content sharing action is in progress"
},
@@ -1980,6 +2107,18 @@
"Your report will be sent to the relays you are connected to" : {
"comment" : "Footer text to inform user what will happen when the report is submitted."
},
"Your transaction quota has been exceeded." : {
"comment" : "Error description for quota exceeded"
},
"Your trusted network is comprised of profiles you follow and profiles that they follow." : {
"comment" : "Description of the tip that informs users what trusted network means."
},
"Your wallet does not have sufficient balance for this transaction." : {
"comment" : "Error description for insufficient balance"
},
"Your wallet is temporarily being rate limited." : {
"comment" : "Error description for rate limit error"
},
"Zap" : {
"comment" : "Accessibility label for zap button\nButton label that allows the user to zap (i.e. send a Bitcoin tip via the lightning network) the user shown on-screen\nText underneath the number of sats indicating that it's the amount used for zaps.\nTitle of notification when a non-private zap is received."
},
@@ -2,6 +2,22 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>follow_pack_user_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@FOLLOW_PACK_USERS@</string>
<key>FOLLOW_PACK_USERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>user</string>
<key>other</key>
<string>users</string>
</dict>
</dict>
<key>followed_by_three_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
@@ -82,6 +98,22 @@
<string>Imports</string>
</dict>
</dict>
<key>notes_from_three_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Notes from %2$@, %3$@, %4$@ &amp; %1$d other in your trusted network</string>
<key>other</key>
<string>Notes from %2$@, %3$@, %4$@ &amp; %1$d others in your trusted network</string>
</dict>
</dict>
<key>people_reposted_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
+2 -2
View File
@@ -3,10 +3,10 @@
"project" : "damus.xcodeproj",
"targetLocale" : "en-US",
"toolInfo" : {
"toolBuildNumber" : "16E140",
"toolBuildNumber" : "16F6",
"toolID" : "com.apple.dt.xcode",
"toolName" : "Xcode",
"toolVersion" : "16.3"
"toolVersion" : "16.4"
},
"version" : "1.0"
}
Binary file not shown.
+32
View File
@@ -2,6 +2,22 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>follow_pack_user_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@FOLLOW_PACK_USERS@</string>
<key>FOLLOW_PACK_USERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>gebruiker</string>
<key>other</key>
<string>gebruikers</string>
</dict>
</dict>
<key>followed_by_three_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
@@ -82,6 +98,22 @@
<string>Importeringen</string>
</dict>
</dict>
<key>notes_from_three_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Notities van %2$@, %3$@ en %4$@; %1$d ander in je netwerk</string>
<key>other</key>
<string>Notities van %2$@, %3$@ en %4$@; %1$d anderen in je netwerk</string>
</dict>
</dict>
<key>people_reposted_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
Binary file not shown.
+14
View File
@@ -2,6 +2,20 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>follow_pack_user_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@FOLLOW_PACK_USERS@</string>
<key>FOLLOW_PACK_USERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>ผู้ใช้</string>
</dict>
</dict>
<key>followed_by_three_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
+72
View File
@@ -0,0 +1,72 @@
//
// Benchmarking.swift
// damusTests
//
// Created by William Casarin on 3/6/25.
//
import Testing
import XCTest
@testable import damus
class BenchmarkingTests: XCTestCase {
// Old regex-based implementations for comparison
func trim_suffix_regex(_ str: String) -> String {
return str.replacingOccurrences(of: "\\s+$", with: "", options: .regularExpression)
}
func trim_prefix_regex(_ str: String) -> String {
return str.replacingOccurrences(of: "^\\s+", with: "", options: .regularExpression)
}
// Test strings with different characteristics
lazy var testStrings: [String] = [
" Hello World ", // Simple whitespace
" \n\t Hello World \n\t ", // Mixed whitespace
String(repeating: " ", count: 1000) + "Hello", // Large prefix
"Hello" + String(repeating: " ", count: 1000), // Large suffix
String(repeating: " ", count: 500) + "Hello" + String(repeating: " ", count: 500) // Both
]
func testTrimSuffixRegexPerformance() throws {
measure {
for str in testStrings {
_ = trim_suffix_regex(str)
}
}
}
func testTrimSuffixNewPerformance() throws {
measure {
for str in testStrings {
_ = trim_suffix(str)
}
}
}
func testTrimPrefixRegexPerformance() throws {
measure {
for str in testStrings {
_ = trim_prefix_regex(str)
}
}
}
func testTrimPrefixNewPerformance() throws {
measure {
for str in testStrings {
_ = trim_prefix(str)
}
}
}
func testTrimFunctionCorrectness() throws {
// Verify that both implementations produce the same results
for str in testStrings {
XCTAssertEqual(trim_suffix(str), trim_suffix_regex(str), "New trim_suffix implementation produces different results")
XCTAssertEqual(trim_prefix(str), trim_prefix_regex(str), "New trim_prefix implementation produces different results")
}
}
}
-12
View File
@@ -9,18 +9,6 @@ import XCTest
@testable import damus
extension Block {
var asInvoice: Invoice? {
switch self {
case .invoice(let invoice):
return invoice
default:
return nil
}
}
}
final class InvoiceTests: XCTestCase {
override func setUpWithError() throws {
+1
View File
@@ -15,6 +15,7 @@ final class LocalizationUtilTests: XCTestCase {
// Test cases of the localization string key, and the expected en-US strings for a count of 0, 1, and 2.
let keys = [
["follow_pack_user_count", "users", "user", "users"],
["followers_count", "Followers", "Follower", "Followers"],
["following_count", "Following", "Following", "Following"],
["hellthread_notifications_disabled", "Hide notifications that tag more than 0 profiles", "Hide notifications that tag more than 1 profile", "Hide notifications that tag more than 2 profiles"],
+2 -1
View File
@@ -49,7 +49,8 @@ func generate_test_damus_state(
video: .init(),
ndb: ndb,
quote_reposts: .init(our_pubkey: our_pubkey),
emoji_provider: DefaultEmojiProvider(showAllVariations: false)
emoji_provider: DefaultEmojiProvider(showAllVariations: false),
favicon_cache: .init()
)
return damus
@@ -0,0 +1,39 @@
//
// NIP05DomainTimelineHeaderViewTests.swift
// damusTests
//
// Created by Terry Yiu on 5/23/25.
//
import XCTest
@testable import damus
final class NIP05DomainTimelineHeaderViewTests: XCTestCase {
let enUsLocale = Locale(identifier: "en-US")
func testFriendsOfFriendsString() throws {
let pk1 = test_pubkey
let pk2 = test_pubkey_2
let pk3 = Pubkey(hex: "b42e44b555013239a0d5dcdb09ebde0857cd8a5a57efbba5a2b6ac78833cb9f0")!
let pk4 = Pubkey(hex: "cc590e46363d0fa66bb27081368d01f169b8ffc7c614629d4e9eef6c88b38670")!
let pk5 = Pubkey(hex: "f2aa579bb998627e04a8f553842a09446360c9d708c6141dd119c479f6ab9d29")!
let ndb = Ndb(path: Ndb.db_path)!
let damus_name = "17ldvg64:nq5mhr77"
XCTAssertEqual(friendsOfFriendsString([pk1], ndb: ndb, locale: enUsLocale), "Notes from \(damus_name)")
XCTAssertEqual(friendsOfFriendsString([pk1, pk2], ndb: ndb, locale: enUsLocale), "Notes from \(damus_name) & 1rppft3m:4qxhsgnj")
XCTAssertEqual(friendsOfFriendsString([pk1, pk2, pk3], ndb: ndb, locale: enUsLocale), "Notes from \(damus_name), 1rppft3m:4qxhsgnj & 1kshyfd2:cq04aze0")
XCTAssertEqual(friendsOfFriendsString([pk1, pk2, pk3, pk4,], ndb: ndb, locale: enUsLocale), "Notes from \(damus_name), 1rppft3m:4qxhsgnj, 1kshyfd2:cq04aze0 & 1 other in your trusted network")
XCTAssertEqual(friendsOfFriendsString([pk1, pk2, pk3, pk4, pk5], ndb: ndb, locale: enUsLocale), "Notes from \(damus_name), 1rppft3m:4qxhsgnj, 1kshyfd2:cq04aze0 & 2 others in your trusted network")
let pubkeys = [pk1, pk2, pk3, pk4, pk5, pk1, pk2, pk3, pk4, pk5]
Bundle.main.localizations.map { Locale(identifier: $0) }.forEach {
for count in 1...10 {
XCTAssertNoThrow(friendsOfFriendsString(pubkeys.prefix(count).map { $0 }, ndb: ndb, locale: $0))
}
}
}
}
+39
View File
@@ -291,6 +291,45 @@ class NoteContentViewTests: XCTestCase {
XCTAssertEqual(noteArtifactsSeparated.images[0].absoluteString, "https://damus.io/nothidden.png")
}
func testRenderBlocksWithPreviewableBlocksAtEndAreHiddenWhenHashtagsAreEmbedded() throws {
let noteId = test_note.id.bech32
let invoiceString = "lnbc100n1p357sl0sp5t9n56wdztun39lgdqlr30xqwksg3k69q4q2rkr52aplujw0esn0qpp5mrqgljk62z20q4nvgr6lzcyn6fhylzccwdvu4k77apg3zmrkujjqdpzw35xjueqd9ejqcfqv3jhxcmjd9c8g6t0dcxqyjw5qcqpjrzjqt56h4gvp5yx36u2uzqa6qwcsk3e2duunfxppzj9vhypc3wfe2wswz607uqq3xqqqsqqqqqqqqqqqlqqyg9qyysgqagx5h20aeulj3gdwx3kxs8u9f4mcakdkwuakasamm9562ffyr9en8yg20lg0ygnr9zpwp68524kmda0t5xp2wytex35pu8hapyjajxqpsql29r"
let content = " Check this out. \nhttps://hidden.tld/\nhttps://damus.io/hidden1.png\n\(invoiceString)\nhttps://damus.io/hidden2.png\nnostr:\(noteId)#hashtag1 #hashtag2 "
let note = try XCTUnwrap(NostrEvent(content: content, keypair: test_keypair))
let parsed: Blocks = parse_note_content(content: .init(note: note, keypair: test_keypair))
let testState = test_damus_state
let noteArtifactsSeparated: NoteArtifactsSeparated = render_blocks(blocks: parsed, profiles: testState.profiles, can_hide_last_previewable_refs: true)
let attributedText: AttributedString = noteArtifactsSeparated.content.attributed
let runs: AttributedString.Runs = attributedText.runs
let runArray: [AttributedString.Runs.Run] = Array(runs)
XCTAssertEqual(runArray.count, 4)
XCTAssertTrue(runArray[0].description.contains("Check this out."))
XCTAssertFalse(runArray[0].description.contains("https://hidden.tld/"))
XCTAssertFalse(runArray[0].description.contains("https://damus.io/hidden1.png"))
XCTAssertFalse(runArray[0].description.contains("lnbc100n:qpsql29r"))
XCTAssertFalse(runArray[0].description.contains("https://damus.io/hidden2.png"))
XCTAssertFalse(runArray[0].description.contains("note1qqq:qqn2l0z3"))
XCTAssertTrue(runArray[1].description.contains("#hashtag1"))
XCTAssertTrue(runArray[3].description.contains("#hashtag2"))
XCTAssertEqual(noteArtifactsSeparated.images.count, 2)
XCTAssertEqual(noteArtifactsSeparated.images[0].absoluteString, "https://damus.io/hidden1.png")
XCTAssertEqual(noteArtifactsSeparated.images[1].absoluteString, "https://damus.io/hidden2.png")
XCTAssertEqual(noteArtifactsSeparated.media.count, 2)
XCTAssertEqual(noteArtifactsSeparated.media[0].url.absoluteString, "https://damus.io/hidden1.png")
XCTAssertEqual(noteArtifactsSeparated.media[1].url.absoluteString, "https://damus.io/hidden2.png")
XCTAssertEqual(noteArtifactsSeparated.links.count, 1)
XCTAssertEqual(noteArtifactsSeparated.links[0].absoluteString, "https://hidden.tld/")
XCTAssertEqual(noteArtifactsSeparated.invoices.count, 1)
XCTAssertEqual(noteArtifactsSeparated.invoices[0].string, invoiceString)
}
/// Based on https://github.com/damus-io/damus/issues/1468
/// Tests whether a note content view correctly parses an image block when url in JSON content contains optional escaped slashes
func testParseImageBlockInContentWithEscapedSlashes() throws {
+1 -1
View File
@@ -368,7 +368,7 @@ class NdbNote: Codable, Equatable, Hashable {
// Extension to make NdbNote compatible with NostrEvent's original API
extension NdbNote {
var is_textlike: Bool {
return kind == 1 || kind == 42 || kind == 30023 || kind == 9802
return kind == 1 || kind == 42 || kind == 30023 || kind == 9802 || kind == 39089
}
var is_quote_repost: NoteId? {