Compare commits

..

458 Commits

Author SHA1 Message Date
tyiu 242ef4e0cc Fix bug with reaction notifications referencing the wrong event
Changelog-Fixed: Fix bug with reaction notifications referencing the wrong event
2023-04-29 15:20:04 -04:00
William Casarin 68ed3d7796 Fix nip10 thread incompatibility for clients that add more than one reply tag
Changelog-Fixed: Fix thread incompatibility for clients that add more than one reply tag
2023-04-29 07:49:10 -07:00
Swift 5c885b0fd4 Fix sats plurality
Closes: #1034
2023-04-29 06:01:10 -07:00
tyiu 8589fe9aee Translations (#1020)
* Apply translations in sv_SE

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.

* Export strings for translation

* Apply translations in nl

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.

* Apply translations in cs

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.

* Apply translations in ja

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in el_GR

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.

* Apply translations in hu_HU

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'hu_HU' language.

* Apply translations in zh_CN

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.

* Apply translations in zh_CN

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.

* Apply translations in zh_CN

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.

* Apply translations in zh_CN

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.

* Apply translations in zh_CN

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.

* Apply translations in zh_CN

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.

* Apply translations in zh_HK

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_HK' language.

* Apply translations in zh_TW

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.

* Apply translations in zh_TW

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.

* Apply translations in zh_TW

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.

* Apply translations in zh_TW

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.

* Apply translations in zh_TW

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.

* Apply translations in zh_TW

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.

* Apply translations in sv_SE

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2023-04-29 06:00:56 -07:00
William Casarin 2aee84c65f Add q tag to quote renotes
Changelog-Added: Add q tag to quoted renotes
2023-04-28 17:54:44 -07:00
William Casarin 76c6ac0f0b Add referencedid helpers 2023-04-28 17:54:44 -07:00
William Casarin be08083b88 Load zaps instantly on events
Refactor our event cache a bit and add zap caching

Changelog-Changed: Load zaps instantly on events
2023-04-28 17:25:31 -07:00
William Casarin c2325a5e39 always process events 2023-04-28 13:03:26 -07:00
William Casarin 3eb544e40d build 13 2023-04-28 13:00:44 -07:00
William Casarin 9d209f485c Preserve order of bookmarks when saving
Changelog-Fixed: Preserve order of bookmarks when saving
2023-04-27 13:04:46 -07:00
Swift 1394122542 Add confirmation alert when clearing all bookmarks
Changelog-Added: Add confirmation alert when clearing all bookmarks
Closes: #1016
2023-04-27 10:31:55 -07:00
William Casarin e89c025d9d Remove blurhash opacity 2023-04-27 10:30:31 -07:00
William Casarin 0970c364b6 Tweak fade speed and opacity on blurhash placeholders 2023-04-26 15:41:30 -07:00
William Casarin d16192e845 Show blurhash placeholders from image metadata
Changelog-Added: Show blurhash placeholders from image metadata
2023-04-26 15:21:12 -07:00
William Casarin 3b50f82094 Add image metadata to image uploads
Adds blurhash and image dimensions. This is an alternative and backwards
compatible version of NIP94 for images in kind1 notes.

Changelog-Added: Add image metadata to image uploads
2023-04-26 10:53:13 -07:00
William Casarin 46b53e1326 Add BinaryParser
Didn't end up using this, but might be useful in the future
2023-04-26 10:53:13 -07:00
William Casarin 225a028f3e build 12 2023-04-25 15:21:16 -07:00
William Casarin d074d092a2 Fix crash when you have invalid relays in your relay list
Changelog-Fixed: Fix crash when you have invalid relays in your relay list
2023-04-25 15:06:09 -07:00
William Casarin 633fcd69a8 build 11 2023-04-25 14:31:13 -07:00
William Casarin 67869394cb Fix permanent OnlyZaps (likes broken lol) 2023-04-25 12:43:02 -07:00
William Casarin 22876b5c28 v1.4.3-10 changelog 2023-04-25 09:43:49 -07:00
William Casarin 6f7d6d1933 build 10 2023-04-25 08:57:45 -07:00
William Casarin fddd86b207 Revert "Remove unneeded periodic reconnect timer"
This reverts commit ed058afc3b.
2023-04-25 08:57:45 -07:00
alltheseas 95f1127b74 Readme.MD: add privacy section, and link to issues
Added:
1. Damus implications on user privacy (IP address)
2. Added direct link to Damus github issues for interested contributors
3. Added statement of relation between IP address and public key

Co-authored-by: Max Hillebrand <30683012+MaxHillebrand@users.noreply.github.com>
2023-04-25 08:57:45 -07:00
William Casarin 4c82176466 Merge remote-tracking branch 'github/translations' 2023-04-25 08:57:45 -07:00
transifex-integration[bot] e2d55ddae4 Apply translations in hu_HU
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'hu_HU' language.
2023-04-25 10:18:10 +00:00
transifex-integration[bot] 8fa80b7921 Apply translations in zh_TW
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.
2023-04-25 08:50:45 +00:00
transifex-integration[bot] 732b484faf Apply translations in zh_HK
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_HK' language.
2023-04-25 08:48:39 +00:00
transifex-integration[bot] cd9c705221 Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-25 08:43:59 +00:00
transifex-integration[bot] ba5a062829 Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.
2023-04-25 05:29:16 +00:00
transifex-integration[bot] 687d1c9a3e Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.
2023-04-25 05:26:56 +00:00
transifex-integration[bot] adef207018 Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-25 03:58:29 +00:00
transifex-integration[bot] a050a5b729 Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-25 03:49:06 +00:00
transifex-integration[bot] 6bced24430 Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-25 03:48:56 +00:00
transifex-integration[bot] 60cddf2a15 Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-25 03:48:29 +00:00
transifex-integration[bot] c9568fe7ac Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-25 03:46:59 +00:00
transifex-integration[bot] 5c0e4599ad Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-25 03:46:49 +00:00
transifex-integration[bot] e3519c51a5 Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-25 03:46:46 +00:00
transifex-integration[bot] ed1aa246c4 Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-25 03:46:26 +00:00
transifex-integration[bot] 532647d273 Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-25 03:46:03 +00:00
transifex-integration[bot] dab8f7ca61 Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-25 03:45:52 +00:00
transifex-integration[bot] 390eb342f7 Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-25 03:26:11 +00:00
Suhail Saqan f1a7c0eded Add paste button to login
Changelog-Added: Add paste button to login
Closes: #927
2023-04-24 18:30:25 -07:00
William Casarin ed058afc3b Remove unneeded periodic reconnect timer 2023-04-24 18:21:12 -07:00
Bryan Montz 0e94c48e26 Replace Starscream with URLSessionWebSocketTask
Changelog-Fixed: Fix slow reconnection issues
2023-04-24 18:11:07 -07:00
symbsrcool 6ac68b5a73 Add nokyctranslate translation option
Changelog-Added: Add nokyctranslate translation option
Closes: #946
2023-04-24 18:07:03 -07:00
Swift 2048e68d67 Fix issue where uploaded images were from someone else
Changelog-Fixed: Fix issue where uploaded images were from someone else
2023-04-24 17:53:28 -07:00
William Casarin 624d9662d7 Merge remote-tracking branch 'github/translations' 2023-04-24 16:20:19 -07:00
William Casarin 88db9de4ea Fix reposts on macos 2023-04-24 15:38:11 -07:00
William Casarin d667a9d8f7 Fix custom zap button hitboxes
Suggested-by: eric
2023-04-24 10:51:47 -07:00
transifex-integration[bot] 65f3c76eca Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-24 17:13:45 +00:00
transifex-integration[bot] aed1e543d3 Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-24 17:11:27 +00:00
transifex-integration[bot] 65576424fd Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-24 17:11:17 +00:00
transifex-integration[bot] f99ad8fffa Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-24 17:10:28 +00:00
transifex-integration[bot] 987d173529 Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-24 17:09:38 +00:00
transifex-integration[bot] eab7a91f01 Apply translations in el_GR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.
2023-04-24 13:35:13 +00:00
transifex-integration[bot] 2d045f4dfb Apply translations in nl
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-04-24 11:13:59 +00:00
transifex-integration[bot] 835e5a438f Apply translations in ar
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.
2023-04-24 08:13:36 +00:00
transifex-integration[bot] ca4e91564a Apply translations in ar
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.
2023-04-24 08:10:57 +00:00
tyiu 8733cbd42c Fix localization issues and export strings for translation 2023-04-24 01:25:07 +02:00
William Casarin f5cdd4a159 You can now change the default zap type
Changelog-Added: You can now change the default zap type
2023-04-23 10:31:51 -07:00
William Casarin c4f41220e5 refactor: extract ZapTypePicker into its own file 2023-04-23 09:54:38 -07:00
William Casarin 0f119d34e6 Change 500 custom zap amount to 420🌿
Suggested-by: Terry
Changelog-Changed: Change 500 custom zap to 420
2023-04-23 08:54:13 -07:00
William Casarin ea90fb0429 Make custom zap amounts into a grid 2023-04-23 08:40:33 -07:00
ericholguin 8227be1873 Updated custom zap view
Changelog-Changed: New looks to the custom zaps view
2023-04-23 08:40:22 -07:00
William Casarin cbd92539a6 Merge remote-tracking branch 'github/translations' 2023-04-23 04:36:16 -07:00
William Casarin 7940e6fd32 Fix tests 2023-04-23 04:36:16 -07:00
Swift 07f8ad75dc Adjust attachment images placement when posting
Changelog-Changed: Adjust attachment images placement when posting
Closes: #979
2023-04-23 04:36:13 -07:00
transifex-integration[bot] 018bb4c33b Apply translations in ar
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.
2023-04-23 06:22:51 +00:00
transifex-integration[bot] c81b403817 Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-23 01:57:25 +00:00
transifex-integration[bot] e54ce88a3b Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-23 01:57:22 +00:00
transifex-integration[bot] 84f4f1c71c Apply translations in es_ES
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_ES' language.
2023-04-22 20:10:01 +00:00
tyiu e934c2bb11 Fix crash with LibreTranslate server setting selection and remove delisted vern server
Broken after the settings refactor

Changelog-Fixed: Fix crash with LibreTranslate server setting selection and remove delisted vern server
Closes: #998
2023-04-22 12:33:11 -07:00
William Casarin fd8ad494e9 Add partial support for different repost variants
Changelog-Added: Add partial support for different repost variants
2023-04-22 12:11:45 -07:00
William Casarin f14ba7cce4 Fix potentially buggy media uploader setting 2023-04-22 12:11:45 -07:00
William Casarin 055b13c1cd Fix buggy zap amounts and wallet selector settings
Changelog-Fixed: Fix buggy zap amounts and wallet selector settings
2023-04-22 12:10:10 -07:00
William Casarin 357e8adf86 Refactor disable_animation setting
Pass it down from the top instead of using a function which goes around
our settings store
2023-04-22 12:08:24 -07:00
transifex-integration[bot] a82a78c7df Apply translations in vi
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'vi' language.
2023-04-22 18:45:04 +00:00
William Casarin 084c86eb0e Only show friends, not friend-of-friend in friend filter
Changelog-Changed: Only show friends, not friend-of-friend in friend filter
2023-04-22 11:15:45 -07:00
transifex-integration[bot] 10d9d23b7b Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-22 15:30:37 +00:00
transifex-integration[bot] 50ecff0ec6 Apply translations in zh_TW
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.
2023-04-22 13:37:58 +00:00
transifex-integration[bot] 5ae96ec80a Apply translations in zh_TW
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.
2023-04-22 13:37:48 +00:00
transifex-integration[bot] abfd48ca20 Apply translations in zh_HK
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_HK' language.
2023-04-22 13:37:32 +00:00
transifex-integration[bot] 67326e2003 Apply translations in zh_HK
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_HK' language.
2023-04-22 13:37:22 +00:00
transifex-integration[bot] 306c3fe75c Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-22 13:36:57 +00:00
transifex-integration[bot] 08c2056290 Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-22 13:36:11 +00:00
transifex-integration[bot] bc5ee7cd51 Apply translations in zh_TW
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.
2023-04-22 13:17:58 +00:00
transifex-integration[bot] b2d1ad2537 Apply translations in zh_HK
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_HK' language.
2023-04-22 13:16:17 +00:00
transifex-integration[bot] b9f37697d7 Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-22 13:06:28 +00:00
transifex-integration[bot] 58e88262b0 Apply translations in el_GR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.
2023-04-22 12:49:50 +00:00
transifex-integration[bot] 26e28dd3dd Apply translations in el_GR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'el_GR' language.
2023-04-22 12:46:13 +00:00
transifex-integration[bot] 6ac6ea3cd7 Apply translations in el_GR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'el_GR' language.
2023-04-22 12:45:53 +00:00
transifex-integration[bot] 2de75968fb Apply translations in el_GR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'el_GR' language.
2023-04-22 12:44:46 +00:00
transifex-integration[bot] 37b99983d3 Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pt_BR' language.
2023-04-22 10:42:33 +00:00
transifex-integration[bot] f62dc9348a Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pt_BR' language.
2023-04-22 10:42:11 +00:00
transifex-integration[bot] 6aab705399 Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pt_BR' language.
2023-04-22 10:41:41 +00:00
transifex-integration[bot] f7da481c68 Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pt_BR' language.
2023-04-22 10:36:14 +00:00
transifex-integration[bot] e5b629742a Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pt_BR' language.
2023-04-22 10:35:55 +00:00
transifex-integration[bot] a0e6aa060b Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pt_BR' language.
2023-04-22 10:35:52 +00:00
transifex-integration[bot] 6394f96ac0 Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pt_BR' language.
2023-04-22 10:35:38 +00:00
transifex-integration[bot] b6d6af12b8 Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pt_BR' language.
2023-04-22 10:35:29 +00:00
transifex-integration[bot] df84c4a64b Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pt_BR' language.
2023-04-22 10:33:40 +00:00
transifex-integration[bot] 71b333a18a Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pt_BR' language.
2023-04-22 10:33:30 +00:00
transifex-integration[bot] 29936f7b06 Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pt_BR' language.
2023-04-22 10:33:21 +00:00
transifex-integration[bot] 1cc1bfbbef Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pt_BR' language.
2023-04-22 10:33:08 +00:00
transifex-integration[bot] 2aa39e775e Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pt_BR' language.
2023-04-22 10:32:58 +00:00
transifex-integration[bot] 8a33243c98 Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pt_BR' language.
2023-04-22 10:32:04 +00:00
transifex-integration[bot] d198e69dc9 Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pt_BR' language.
2023-04-22 10:31:38 +00:00
transifex-integration[bot] c26b30f3c0 Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pt_BR' language.
2023-04-22 10:31:24 +00:00
transifex-integration[bot] c2479df213 Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pt_BR' language.
2023-04-22 10:30:36 +00:00
transifex-integration[bot] 30d045b1c5 Apply translations in nl
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-04-22 11:27:02 +02:00
transifex-integration[bot] d76f7564ef Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-22 11:27:02 +02:00
transifex-integration[bot] bab72b215d Apply translations in ar
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.
2023-04-22 11:27:02 +02:00
transifex-integration[bot] d8cd81deb8 Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.
2023-04-22 11:27:02 +02:00
transifex-integration[bot] 92dfdacf97 Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.
2023-04-22 11:27:02 +02:00
transifex-integration[bot] ead6e96613 Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.
2023-04-22 11:27:02 +02:00
transifex-integration[bot] 7312ee3884 Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-22 11:27:02 +02:00
transifex-integration[bot] 45099c59db Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-22 11:27:02 +02:00
tyiu 3440c828e3 Export strings for translation 2023-04-22 11:27:01 +02:00
William Casarin 00aa897f05 build 8 2023-04-21 17:48:34 -07:00
William Casarin 71a1a6f0a3 Add some default height to images 2023-04-21 17:45:58 -07:00
William Casarin 9d22f40a53 Remove some cruft 2023-04-21 17:15:23 -07:00
William Casarin 0bd40c0018 Add Friends filter to DMs
Changelog-Added: Add friends filter to DMs
2023-04-21 16:55:59 -07:00
William Casarin ec75769a0f Refactor notification state filter saving and loading 2023-04-21 16:39:12 -07:00
William Casarin 47e349558c rename FineNotificationFilter to FriendFilter 2023-04-21 16:25:18 -07:00
William Casarin aa559b2916 Refactor and Scope user settings to pubkey 2023-04-21 16:21:01 -07:00
William Casarin 9bf8349db6 Friends filter for notifications
Changelog-Added: Friends filter for notifications
2023-04-21 14:17:37 -07:00
William Casarin 4c44de9276 Merge remote-tracking branch 'github/translations' 2023-04-21 11:27:35 -07:00
William Casarin aa5f8d19f7 build 7 2023-04-21 11:26:49 -07:00
William Casarin 040e452132 Fix having to set onlyzaps mode every time on restart
Changelog-Fixed: Fix having to set onlyzaps mode every time on restart
2023-04-21 10:01:32 -07:00
William Casarin ff41bb1b35 Update OnlyZaps wording: others can still pointlessly send you likes 2023-04-21 09:54:29 -07:00
transifex-integration[bot] efe231c122 Apply translations in nl [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'nl' language.
2023-04-21 16:52:37 +00:00
transifex-integration[bot] 0305712f65 Apply translations in es_419 [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'es_419' language.
2023-04-21 16:46:29 +00:00
William Casarin ba844aec97 Enable like button on OnlyZaps profiles for people who don't have OnlyZaps mode on
It seems like people still want to like posts even if the receiver isn't
going to see it.

Changelog-Changed: Enable like button on OnlyZaps profiles for people who don't have OnlyZaps mode on
2023-04-21 09:43:18 -07:00
transifex-integration[bot] 62eac0032f Apply translations in ja [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-21 16:43:06 +00:00
transifex-integration[bot] b673e9b43b Apply translations in cs [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.
2023-04-21 16:43:03 +00:00
transifex-integration[bot] 1dd274e07f Apply translations in pt_PT [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'pt_PT' language.
2023-04-21 16:43:00 +00:00
transifex-integration[bot] e8aa52efea Apply translations in sv_SE [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'sv_SE' language.
2023-04-21 16:42:59 +00:00
transifex-integration[bot] c7f6db84dd Apply translations in pl_PL [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'pl_PL' language.
2023-04-21 16:42:57 +00:00
transifex-integration[bot] 4a28ddefab Apply translations in el_GR [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'el_GR' language.
2023-04-21 16:42:53 +00:00
transifex-integration[bot] 3f4e37f1f8 Apply translations in zh_HK [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'zh_HK' language.
2023-04-21 16:42:49 +00:00
transifex-integration[bot] 9dc304aa04 Apply translations in zh_CN [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'zh_CN' language.
2023-04-21 16:42:45 +00:00
transifex-integration[bot] d3dd4faa42 Apply translations in vi [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'vi' language.
2023-04-21 16:42:42 +00:00
transifex-integration[bot] b40ee38895 Apply translations in ru [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'ru' language.
2023-04-21 16:42:36 +00:00
transifex-integration[bot] fb9d1db6e6 Apply translations in ja [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'ja' language.
2023-04-21 16:42:26 +00:00
transifex-integration[bot] 591cdd478e Apply translations in fr [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'fr' language.
2023-04-21 16:42:23 +00:00
transifex-integration[bot] b97204400f Apply translations in cs [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'cs' language.
2023-04-21 16:42:19 +00:00
transifex-integration[bot] ebfe00362c Apply translations in zh_TW [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'zh_TW' language.
2023-04-21 16:37:37 +00:00
transifex-integration[bot] accafb4cb2 Apply translations in zh_CN [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'zh_CN' language.
2023-04-21 16:37:34 +00:00
transifex-integration[bot] 20b124aa59 Apply translations in pt_BR [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'pt_BR' language.
2023-04-21 16:37:30 +00:00
transifex-integration[bot] 057bb2add5 Apply translations in de [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'de' language.
2023-04-21 16:37:24 +00:00
transifex-integration[bot] c1b31a9938 Apply translations in sv_SE [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-21 16:37:16 +00:00
transifex-integration[bot] 1e532f9e63 Apply translations in el_GR [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.
2023-04-21 16:37:12 +00:00
transifex-integration[bot] 16a7d5dedf Apply translations in pl_PL [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-21 16:37:08 +00:00
transifex-integration[bot] 8898bffbed Apply translations in hu_HU [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'hu_HU' language.
2023-04-21 16:37:04 +00:00
transifex-integration[bot] 7583346b06 Apply translations in zh_TW [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.
2023-04-21 16:37:00 +00:00
transifex-integration[bot] 8fbc71a2c2 Apply translations in zh_CN [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-21 16:36:56 +00:00
transifex-integration[bot] e100f8c313 Apply translations in ru [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ru' language.
2023-04-21 16:36:53 +00:00
transifex-integration[bot] 676c6f2afb Apply translations in pt_BR [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pt_BR' language.
2023-04-21 16:36:49 +00:00
transifex-integration[bot] e7c66156d3 Apply translations in nl [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-04-21 16:36:45 +00:00
transifex-integration[bot] 99816695ae Apply translations in de [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-21 16:36:41 +00:00
transifex-integration[bot] 8e984ffa98 Apply translations in ar [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.
2023-04-21 16:36:37 +00:00
transifex-integration[bot] 61e4359164 Apply translations in zh_TW [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'zh_TW' language.
2023-04-21 16:36:33 +00:00
transifex-integration[bot] 71940aaca0 Apply translations in hu_HU [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'hu_HU' language.
2023-04-21 16:36:22 +00:00
transifex-integration[bot] 042237ace7 Apply translations in es_ES [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'es_ES' language.
2023-04-21 16:36:17 +00:00
transifex-integration[bot] b9c10d1eb1 Apply translations in pt_BR [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'pt_BR' language.
2023-04-21 16:35:59 +00:00
transifex-integration[bot] 638b98624e Apply translations in ko [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'ko' language.
2023-04-21 16:32:28 +00:00
transifex-integration[bot] c8224f841d Apply translations in de [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'de' language.
2023-04-21 16:32:25 +00:00
transifex-integration[bot] 18439bbdf9 Apply translations in ar [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'ar' language.
2023-04-21 16:32:21 +00:00
transifex-integration[bot] 1fd7b759b1 Apply translations in zh_HK [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'zh_HK' language.
2023-04-21 16:32:12 +00:00
transifex-integration[bot] 0642abe064 Apply translations in zh_HK [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_HK' language.
2023-04-21 16:32:06 +00:00
transifex-integration[bot] 080efda25e Apply translations in vi [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'vi' language.
2023-04-21 16:32:04 +00:00
William Casarin bbfe5380e0 Add onlyzap badges to usernames 2023-04-20 14:40:39 -07:00
William Casarin 8a20b7e4a7 Add #zaps and #onlyzaps to custom hashtags
Changelog-Added: Add #zaps and #onlyzaps to custom hashtags
2023-04-20 14:40:16 -07:00
William Casarin 191950a5aa Colorize friend icons
No reason why nip05 badges should have all the fun

Changelog-Added: Colorize friend icons
2023-04-20 14:30:27 -07:00
William Casarin ac82f1bc09 Add OnlyZaps Mode
Changelog-Added: Add OnlyZaps mode: disable reactions, only zaps!
2023-04-20 13:41:05 -07:00
William Casarin 209f3e8759 onlyzaps: Clean up reaction filter logic a bit 2023-04-20 11:23:58 -07:00
William Casarin 897621b5ed Don't fetch delete events 2023-04-20 08:23:56 -07:00
tyiu 66641fc9ae Add setting to hide reactions
Changelog-Added: Add setting to hide reactions
2023-04-20 08:20:50 -07:00
tyiu 681e0f0be9 Translations (#976)
* Apply translations in pt_PT

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'pt_PT' language.

* Apply translations in sv_SE

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.

* Apply translations in ja

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in ja

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in el_GR

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.

* Apply translations in el_GR

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'el_GR' language.

* Apply translations in el_GR

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.

* Apply translations in el_GR

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.

* Apply translations in el_GR

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.

* Apply translations in el_GR

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.

* Apply translations in el_GR

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.

* Apply translations in el_GR

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.

* Apply translations in el_GR

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.

* Apply translations in el_GR

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.

* Apply translations in hu_HU

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'hu_HU' language.

* Apply translations in es_419

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.

* Export strings for translation

* Apply translations in nl

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.

* Apply translations in sv_SE

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.

* Apply translations in ja

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.

* Apply translations in hu_HU

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'hu_HU' language.

* Apply translations in pl_PL

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.

* Apply translations in zh_CN

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.

* Apply translations in zh_CN

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.

* Apply translations in zh_CN

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.

* Apply translations in zh_CN

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.

* Apply translations in zh_CN

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.

* Apply translations in zh_CN

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.

* Apply translations in zh_CN

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.

* Apply translations in zh_CN

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.

* Apply translations in zh_CN

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.

* Apply translations in zh_CN

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.

* Apply translations in zh_CN

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.

* Apply translations in zh_CN

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.

* Apply translations in zh_HK

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_HK' language.

* Apply translations in zh_TW

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.

* Apply translations in zh_TW

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.

* Apply translations in zh_TW

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.

* Apply translations in zh_TW

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.

* Apply translations in zh_TW

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.

* Apply translations in zh_TW

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.

* Apply translations in zh_TW

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.

* Apply translations in zh_TW

100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2023-04-20 08:16:23 -07:00
William Casarin 2ff12823f2 Don't nest embedded notes 2023-04-18 12:43:26 -07:00
William Casarin d6d996e84b Don't create spiral hellthreads 2023-04-18 12:38:16 -07:00
William Casarin d1fce5054d Implement quote reposting 2023-04-18 12:16:11 -07:00
William Casarin 300cd87fc2 Remove EmbeddedEventView
All embedded events are now just regular event views
2023-04-18 10:42:56 -07:00
tyiu bff3c0dd52 Improve sats numeric entry for zaps
Changelog-Changed: Add number formatting for sats entry and use selected zaps amount from picker as placeholder
Changelog-Fixed: Do not allow non-numeric characters for sats amount and fix numeric entry for other number systems for all locales
2023-04-17 15:20:14 -07:00
William Casarin ddd027141a no banner uploads yet :( 2023-04-17 15:18:57 -07:00
William Casarin 9b3e25dd6d v1.4.3-2 changelog 2023-04-17 14:58:44 -07:00
William Casarin 1ae6a3d871 v1.4.3-2 2023-04-17 14:29:50 -07:00
William Casarin 4821ba61a5 reply_p_tags: Use uniq instead of Set to preserve order 2023-04-17 14:29:32 -07:00
Swift 15849e290e Fixed repost turning green too early and not reposting sometimes
Changelog-Fixed: Fixed repost turning green too early and not reposting sometimes
2023-04-17 12:55:52 -07:00
Joshua Jiang 6ed562ed24 Fix shuffling when choosing users to reply to
Changelog-Fixed: Fix shuffling when choosing users to reply to
Co-authored-by: William Casarin <jb55@jb55.com>
Closes: #937
2023-04-17 12:55:22 -07:00
tyiu 93580e5296 Do not translate own notes if logged in with private key
If you're not the private key user, it's possible you're just lurking on
somebody's account and you might not speak their language, so you might
need translations.

Changelog-Fixed: Do not translate own notes if logged in with private key
Closes: #943
2023-04-17 11:06:50 -07:00
Swift a5c33e4431 Fix reply image previews appearing on new posts
Closes: #954
2023-04-17 11:00:51 -07:00
tyiu ad7a79c2bb Fix typo on ParticipantsView.swift file name
Closes: #945
2023-04-17 10:27:47 -07:00
William Casarin eaec3ae011 Merge remote-tracking branch 'github/translations' 2023-04-17 10:26:35 -07:00
William Casarin 5a1b966191 Only check inner_events on boosts 2023-04-17 10:19:47 -07:00
Gísli Kristjánsson a320fae2bc Load missing profiles from boosts on home view
Changelog-Fixed: Load missing profiles from boosts on home view
Closes: #947
2023-04-17 10:19:29 -07:00
Gísli Kristjánsson 8e0136a13a Load profiles for profile model
Changelog-Fixed: Load missing profiles from boosts on profile view
2023-04-17 10:17:59 -07:00
Gísli Kristjánsson 8f767b03ae fix: find inner_event profiles from events 2023-04-17 10:10:59 -07:00
Swift 209b23674d Add ability to copy url from post media previews 2023-04-17 10:10:41 -07:00
transifex-integration[bot] 86917dbd69 Apply translations in zh_TW
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.
2023-04-17 08:56:51 +00:00
transifex-integration[bot] 59daf555cd Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-17 08:56:06 +00:00
transifex-integration[bot] 916c7885f8 Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-17 08:55:54 +00:00
transifex-integration[bot] 252763ea77 Apply translations in zh_HK
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_HK' language.
2023-04-17 08:55:31 +00:00
transifex-integration[bot] aa610c18b2 Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-17 08:34:09 +00:00
transifex-integration[bot] 1de96a9dc5 Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-17 05:07:03 +00:00
transifex-integration[bot] 746a4093de Apply translations in el_GR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.
2023-04-16 19:27:10 +00:00
transifex-integration[bot] 5cc9288759 Apply translations in el_GR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'el_GR' language.
2023-04-16 19:12:53 +00:00
transifex-integration[bot] 3bedc83764 Apply translations in el_GR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'el_GR' language.
2023-04-16 19:12:32 +00:00
transifex-integration[bot] eb17e07478 Apply translations in nl
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-04-16 17:42:10 +00:00
transifex-integration[bot] b96ae84068 Apply translations in ar
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.
2023-04-16 17:35:54 +00:00
transifex-integration[bot] af7d4d2c53 Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.
2023-04-16 17:21:08 +00:00
tyiu d93a0600f3 Fix localization issues 2023-04-16 19:07:57 +02:00
transifex-integration[bot] e9e5756c94 Apply translations in zh_TW
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.
2023-04-16 17:01:24 +02:00
transifex-integration[bot] 0b3cc2092f Apply translations in zh_HK
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_HK' language.
2023-04-16 17:01:24 +02:00
transifex-integration[bot] 285ab11324 Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-16 17:01:24 +02:00
transifex-integration[bot] 3610a76c55 Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-16 17:01:23 +02:00
transifex-integration[bot] 4644c57bf3 Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-16 17:01:23 +02:00
transifex-integration[bot] 33887982b0 Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-16 17:01:23 +02:00
transifex-integration[bot] c3d3db352e Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-16 17:01:23 +02:00
transifex-integration[bot] 8f08e5c4c8 Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-16 17:01:23 +02:00
transifex-integration[bot] e00e89c16b Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-16 17:01:23 +02:00
transifex-integration[bot] 9151ef02a0 Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-16 17:01:23 +02:00
transifex-integration[bot] 709a707942 Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-16 17:01:23 +02:00
transifex-integration[bot] 343b7a2bcc Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-16 17:01:23 +02:00
transifex-integration[bot] 6534ba3bde Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-16 17:01:22 +02:00
William Casarin 02fc065005 Always check signatures on profile events
These contain sensitive data (lightning addresses) and it would be
really bad if these were forged.

Changelog-Changed: Always check signatures of profile events
2023-04-15 16:01:00 -07:00
Bartholomew Joyce 668b0a94df cleanup nostr bech32 entity parsing
Closes: #911
2023-04-15 15:04:13 -07:00
William Casarin ebba9d3004 Fix broken notifications 2023-04-15 13:42:10 -07:00
William Casarin 8733a34933 v1.4.3-1 changelog 2023-04-15 12:57:42 -07:00
William Casarin 0de6cfe344 Merge remote-tracking branch 'github/translations' 2023-04-15 12:54:34 -07:00
William Casarin a4d40dbfa6 Revert "Changelog-Added: Banner Image Upload"
I didn't mean to commit this yet

This reverts commit 95041600dc.
2023-04-15 12:43:44 -07:00
Swift 05d332eac3 Add deep links for local notifications
Allow notifications to be clickable

Changelog-Added: Add deep links for local notifications
Co-authored-by: William Casarin <jb55@jb55.com>
Closes: #880
2023-04-15 12:41:49 -07:00
William Casarin b5a3697d78 Refactor direct messages model
We can track the pubkey in the DirectMessageModel instead of having a
janky tuple.
2023-04-15 12:41:00 -07:00
William Casarin 247270f3d3 Introduce LocalNotification
This will be used for local notification data
2023-04-15 12:40:00 -07:00
William Casarin 9327068264 ContentView: open thread helper 2023-04-15 12:38:05 -07:00
transifex-integration[bot] c277c14bcd Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-15 18:48:00 +00:00
transifex-integration[bot] e688a691fc Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-15 18:47:38 +00:00
transifex-integration[bot] b470af8f1d Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-15 18:47:29 +00:00
transifex-integration[bot] efd1168217 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-15 18:46:02 +00:00
transifex-integration[bot] f7a3f9ab76 Apply translations in hu_HU
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'hu_HU' language.
2023-04-15 17:55:34 +00:00
Joel Klabo 95041600dc Changelog-Added: Banner Image Upload 2023-04-15 10:28:17 -07:00
William Casarin 8a8d2ebbc3 PostView: change wording on upload confirmation 2023-04-15 10:26:36 -07:00
William Casarin fad0a6b783 PostView: remove ! unwrap for mediaToUpload 2023-04-15 10:22:05 -07:00
Swift f5d7465368 Ask permission before uploading media
Changelog-Changed: Ask permission before uploading media
Closes: #886
2023-04-15 10:19:53 -07:00
OlegAba 8a785559c6 Fix tap area when mentioning users
Changelog-Fixed: Fix tap area when mentioning users
Closes: #895
2023-04-15 10:16:40 -07:00
William Casarin d4c8c15cc3 Revert "Revert "Fix tap area when mentioning users""
This reverts commit 735376b00f.
2023-04-15 09:59:55 -07:00
Ryan Calder 41a462871c Enable Mac Catalyst
Closes: #926
2023-04-15 09:47:21 -07:00
William Casarin 76a669acc2 Show DM message in local notification
Changelog-Changed: Show DM message in local notification
2023-04-15 09:46:13 -07:00
William Casarin 39236dc094 remove verbose logs 2023-04-15 09:44:17 -07:00
transifex-integration[bot] 5860125802 Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.
2023-04-15 10:29:15 +00:00
transifex-integration[bot] ae96c3b707 Apply translations in el_GR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.
2023-04-15 09:50:43 +00:00
transifex-integration[bot] 136f6f37e8 Apply translations in el_GR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.
2023-04-15 09:49:12 +00:00
transifex-integration[bot] 21f84f722b Apply translations in el_GR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.
2023-04-15 09:49:04 +00:00
transifex-integration[bot] 47747379ee Apply translations in nl
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-04-15 09:48:02 +00:00
transifex-integration[bot] a1b95d40e6 Apply translations in el_GR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.
2023-04-15 09:43:47 +00:00
transifex-integration[bot] 0fc69d862a Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-15 09:36:45 +00:00
tyiu fb0330476d Export strings for translation 2023-04-15 01:32:07 +02:00
transifex-integration[bot] 4b978594fa Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-15 01:31:27 +02:00
William Casarin f0bbba7a33 Fix invalid DM author notifications
Changelog-Fixed: Fix invalid DM author notifications
2023-04-14 11:46:55 -07:00
William Casarin b5faae9d1c Make reposts use postbox 2023-04-14 11:46:55 -07:00
William Casarin a4d4954abd Fix relay signal indicator, properly show how many relays you are connected to
Changelog-Fixed: Fix relay signal indicator, properly show how many relays you are connected to
2023-04-14 11:46:55 -07:00
William Casarin 735376b00f Revert "Fix tap area when mentioning users"
This reverts commit a2cd51b6e7.
2023-04-14 11:36:45 -07:00
William Casarin 042e02d2e4 regression: unbreak dms 2023-04-14 10:33:50 -07:00
William Casarin 40468b1603 refactor: dms view init logic 2023-04-14 10:33:20 -07:00
William Casarin 8c19ec1532 small refactor to include_event 2023-04-14 09:54:54 -07:00
tyiu 1ac9620242 Add thread muting
Changelog-Added: Add thread muting
Closes: #893
2023-04-14 09:43:36 -07:00
Swift d5ecc9bce4 Preview media uploads when posting
Changelog-Added: Preview media uploads when posting
Closes: #894
2023-04-14 09:32:27 -07:00
William Casarin d82b69aac5 Merge remote-tracking branch 'github/translations' 2023-04-14 09:24:59 -07:00
William Casarin bad6ba3643 qrcode: don't default pubkey to "" 2023-04-14 09:23:14 -07:00
ericholguin 5c131e62d7 Add QR Code in profiles
Changelog-Added: Add QR Code in profiles
Closes: #918
2023-04-14 09:20:10 -07:00
William Casarin 29ab48287f start v1.4.3-1 build train 2023-04-14 09:20:10 -07:00
transifex-integration[bot] 5c854519db Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.
2023-04-14 14:15:46 +00:00
transifex-integration[bot] 2cc04e24a3 Apply translations in el_GR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.
2023-04-14 11:51:53 +00:00
transifex-integration[bot] d24bea366d Apply translations in ar
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.
2023-04-14 03:20:41 +00:00
transifex-integration[bot] 85ce8cb93c Apply translations in nl
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-04-13 18:44:01 +00:00
tyiu 32bb8c365d Export strings for translation 2023-04-13 18:37:58 +02:00
transifex-integration[bot] d9285ab3ca Apply translations in nl
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-04-13 18:33:59 +02:00
tyiu 3cba771655 Export strings for translation 2023-04-13 18:33:58 +02:00
tyiu f6f2517fda Merge French variant translations into general French translations 2023-04-13 18:33:58 +02:00
transifex-integration[bot] 047325e6b2 Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.
2023-04-13 18:33:58 +02:00
transifex-integration[bot] ba2108d659 Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'cs' language.
2023-04-13 18:33:58 +02:00
transifex-integration[bot] 863c7baa8b Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'cs' language.
2023-04-13 18:33:58 +02:00
transifex-integration[bot] 8a5e95e47a Apply translations in fr
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'fr' language.
2023-04-13 18:33:58 +02:00
transifex-integration[bot] de0997216d Apply translations in fr
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'fr' language.
2023-04-13 18:33:58 +02:00
transifex-integration[bot] cc64c82ec4 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] e2ca02399b Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] 5418f55cee Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] eb65d473cd Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] dd337c4805 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] 2f6ed72f6d Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] d71bb33408 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] 72cfb2b071 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] a67cb2df90 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:56 +02:00
William Casarin 23e9ce1455 v1.4.2 changelog 2023-04-13 09:27:57 -07:00
William Casarin 5f1132cbc8 v1.4.2-2 2023-04-12 20:11:08 -07:00
William Casarin 806c6257df A few more performance tweaks 2023-04-12 20:09:51 -07:00
William Casarin 18aafb086e Revert "Make tabs easier to click"
This reverts commit 2a2af056eb.
2023-04-12 19:49:52 -07:00
William Casarin fc534ea42d Avoid slow string byte counting functions 2023-04-12 19:26:28 -07:00
William Casarin 54c8958250 Fix a few more hitches 2023-04-12 18:22:16 -07:00
William Casarin e9f71ed07c Load images asyncronously from disk
Changelog-Fixed: Fix hitches caused by syncronous loading of cached images
2023-04-12 13:34:21 -07:00
William Casarin c719058487 Revert "Remove duplicate share sheet action"
This reverts commit 2b34e88a47.
2023-04-12 11:26:41 -07:00
Luis Cabrera ab853c406c Display follows in most recent to oldest
Changelog-Changed: Display follows in most recent to oldest
2023-04-11 15:30:32 -07:00
William Casarin 8a88824677 Merge remote-tracking branch 'github/translations' 2023-04-11 15:22:26 -07:00
William Casarin 0b3918710a Include #btc in custom #bitcoin hashtag
Changelog-Added: Include #btc in custom #bitcoin hashtag
2023-04-11 15:16:57 -07:00
William Casarin 1320ff6bec Fix tabs sometimes not switching
This is the dumbest code I've ever written

Changelog-Fixed: Fix tabs sometimes not switching
2023-04-11 15:05:03 -07:00
William Casarin 10cab37270 remove unneeded id thingies 2023-04-11 14:54:33 -07:00
William Casarin 179da97090 cleanup some dubious code 2023-04-11 14:21:48 -07:00
William Casarin 2b2d124495 Configurable notification dots
Changelog-Added: Make notification dots configurable
2023-04-11 13:58:04 -07:00
William Casarin 2a2af056eb Make tabs easier to click
Changelog-Changed: Make tabs easier to click
2023-04-11 13:58:04 -07:00
William Casarin f56edd5547 switch to v1.4.2 train 2023-04-11 13:00:03 -07:00
William Casarin b30d0c01db nip19: allow empty relays in nprofile and nevent 2023-04-11 12:45:45 -07:00
William Casarin e8d63768c1 Fix note mention test 2023-04-11 10:27:11 -07:00
William Casarin 1f648057d5 Add another bech32 test 2023-04-11 10:23:34 -07:00
Bartholomew Joyce ea14099b62 Improved parsing of bech32 entities 2023-04-11 10:23:34 -07:00
William Casarin f5942f5123 build 9 2023-04-11 10:23:34 -07:00
William Casarin cb8585e4f8 Fix naddr crash 2023-04-11 10:03:48 -07:00
transifex-integration[bot] 526689c742 Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:41:40 +00:00
transifex-integration[bot] 32a9856e2a Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:41:37 +00:00
transifex-integration[bot] 6082a2829f Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:41:16 +00:00
transifex-integration[bot] b3f6b451bf Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:41:14 +00:00
transifex-integration[bot] 91d51c5e76 Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:41:02 +00:00
transifex-integration[bot] 3262fe806a Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:40:53 +00:00
transifex-integration[bot] d04e9c9b5f Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:40:42 +00:00
transifex-integration[bot] 015eb5f9fe Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:40:36 +00:00
transifex-integration[bot] 4a525a7581 Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:40:27 +00:00
transifex-integration[bot] 72e14fc3a8 Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:33:08 +00:00
transifex-integration[bot] af275965ee Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:32:42 +00:00
transifex-integration[bot] 9f5913828a Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-11 12:32:30 +00:00
transifex-integration[bot] c7c21cdee7 Apply translations in hu_HU
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'hu_HU' language.
2023-04-11 09:01:19 +00:00
William Casarin 44a2c4ba7b v1.4.1-8 changelog 2023-04-10 16:13:43 -07:00
William Casarin a74aea9d12 v1.4.1-8 2023-04-10 16:12:26 -07:00
William Casarin 0866c70346 Fix tests 2023-04-10 16:09:43 -07:00
William Casarin 2b34e88a47 Remove duplicate share sheet action 2023-04-10 10:02:20 -07:00
William Casarin 2aa8d527b9 Don't leak mentions in DMs
Changelog-Fixed: Don't leak mentions in DMs
2023-04-10 09:31:26 -07:00
transifex-integration[bot] 88cbb55953 Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-10 15:26:20 +00:00
transifex-integration[bot] e738c6c1ca Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-10 15:26:08 +00:00
transifex-integration[bot] f4e0c8df5c Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ko' language.
2023-04-10 15:17:52 +00:00
transifex-integration[bot] 269269d056 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ko' language.
2023-04-10 15:14:51 +00:00
transifex-integration[bot] fd49539615 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ko' language.
2023-04-10 15:14:36 +00:00
transifex-integration[bot] dbb5c19002 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ko' language.
2023-04-10 15:14:28 +00:00
transifex-integration[bot] 6961113734 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ko' language.
2023-04-10 15:14:11 +00:00
transifex-integration[bot] 4642656ce2 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'ko' language.
2023-04-10 14:24:40 +00:00
transifex-integration[bot] 0c2132b122 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'ko' language.
2023-04-10 14:24:14 +00:00
transifex-integration[bot] a1311a940a Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'ko' language.
2023-04-10 14:22:27 +00:00
transifex-integration[bot] af18975240 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'ko' language.
2023-04-10 14:22:15 +00:00
William Casarin f500da03e8 nip27: handle nrelay a bit better 2023-04-09 22:45:38 -07:00
William Casarin 8a230861bf remove broken test 2023-04-09 22:45:21 -07:00
William Casarin 13354b0eb5 Refactor NIP19 implementation and add tests
Closes: #837
2023-04-09 22:03:51 -07:00
Bartholomew Joyce c6f4643b5a Add support for nostr: bech32 urls in posts and DMs (NIP19)
Changelog-Added: Add support for nostr: bech32 urls in posts and DMs (NIP19)
2023-04-09 22:03:27 -07:00
transifex-integration[bot] ee34b1c0a3 Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-10 01:12:21 +00:00
OlegAba a2cd51b6e7 Fix tap area when mentioning users
Changelog-Fixed: Fix tap area when mentioning users
Closes: #895
2023-04-09 16:17:57 -07:00
transifex-integration[bot] 831a409fe6 Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'pt_BR' language.
2023-04-09 14:48:54 +00:00
transifex-integration[bot] 11e22628bb Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'pt_BR' language.
2023-04-09 14:47:54 +00:00
transifex-integration[bot] 6598b5b4bb Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-08 20:05:16 -04:00
transifex-integration[bot] ca293ef29b Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-08 20:05:16 -04:00
transifex-integration[bot] 8051324d3e Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-08 20:05:15 -04:00
transifex-integration[bot] a11cf66088 Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-08 20:05:15 -04:00
transifex-integration[bot] a10142d3b9 Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-08 20:05:15 -04:00
transifex-integration[bot] 83fcd8600a Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-08 20:05:15 -04:00
transifex-integration[bot] 6283157ef4 Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-08 20:05:15 -04:00
transifex-integration[bot] 8b2f45da41 Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-08 20:05:15 -04:00
transifex-integration[bot] ed467a2f79 Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-08 20:05:15 -04:00
transifex-integration[bot] feace9b70d Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-08 20:05:15 -04:00
transifex-integration[bot] 62bccd6b60 Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-04-08 20:05:15 -04:00
transifex-integration[bot] 6f4d12cab4 Apply translations in zh_TW
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.
2023-04-08 20:05:14 -04:00
transifex-integration[bot] 436fe830c7 Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-08 20:05:14 -04:00
transifex-integration[bot] 69b6d54b0e Apply translations in zh_HK
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_HK' language.
2023-04-08 20:05:14 -04:00
transifex-integration[bot] 13522028ba Apply translations in zh_HK
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_HK' language.
2023-04-08 20:05:14 -04:00
transifex-integration[bot] 1459581ec9 Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-08 20:05:14 -04:00
transifex-integration[bot] 31aedd2a6e Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-08 20:05:14 -04:00
transifex-integration[bot] bb1dff13ce Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-08 20:05:14 -04:00
transifex-integration[bot] 3b8c884b30 Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-08 20:05:14 -04:00
transifex-integration[bot] 30299fafed Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-08 20:05:13 -04:00
William Casarin 46c208c9a5 v1.4.1-7 changelog 2023-04-07 12:15:34 -07:00
William Casarin 8e78bf9e1a v1.4.1-7 2023-04-07 12:14:57 -07:00
William Casarin 82fff4591c Add #zap and #zapathon custom hashtags
Changelog-Added: Add #zap and #zapathon custom hashtags
2023-04-07 12:05:54 -07:00
OlegAba b8226d674d Fix post view padding
Changelog-Fixed: Fix padding in post view
Closes: #879
2023-04-07 11:33:52 -07:00
tyiu 3f3892ba1d Add validation to prevent whitespaces be inputted on NIP-05 input field
Changelog-Changed: Add validation to prevent whitespaces be inputted on NIP-05 input field
Closes: #793
2023-04-07 11:22:38 -07:00
Bryan Montz 1b6224e665 Show most recently bookmarked notes at the top
Changelog-Fixed: Show most recently bookmarked notes at the top
Closes: #854
2023-04-07 11:21:42 -07:00
William Casarin 782f8d6a69 Change reply color from blue to purple. Blue is banned from Damus.
Changelog-Changed: Change reply color from blue to purple. Blue is banned from Damus.
2023-04-07 10:49:33 -07:00
Joel Klabo 9ad6af0e4f Fix Edit Profile Background Colors 2023-04-07 10:37:59 -07:00
William Casarin e16ea0f4dc Merge remote-tracking branch 'github/translations' 2023-04-07 10:15:53 -07:00
William Casarin 5b1808d8e7 Add custom #plebchain icon
Changelog-Added: Add custom #plebchain icon
2023-04-07 10:10:13 -07:00
transifex-integration[bot] d50b2deb26 Apply translations in nl
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-04-07 07:09:24 +00:00
transifex-integration[bot] 9de4730e17 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-07 06:14:30 +00:00
transifex-integration[bot] 382265dd39 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-07 06:13:13 +00:00
tyiu 6c7f8cdbe5 Fix localization issues and export strings for translation 2023-04-06 21:05:53 -04:00
William Casarin 36a92b3795 v1.4.1-6 changelog 2023-04-06 17:41:34 -07:00
William Casarin 6d4d218c28 v1.4.1-6 2023-04-06 17:39:41 -07:00
transifex-integration[bot] 8679da1275 Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-06 20:38:48 -04:00
transifex-integration[bot] 59dcbc130c Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-06 20:38:48 -04:00
transifex-integration[bot] badd3210e5 Apply translations in nl
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-04-06 20:38:48 -04:00
transifex-integration[bot] dd6cc3cf4f Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-06 20:38:48 -04:00
transifex-integration[bot] 36a0ca9c80 Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-06 20:38:48 -04:00
transifex-integration[bot] a7d3b19665 Apply translations in vi
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'vi' language.
2023-04-06 20:38:48 -04:00
transifex-integration[bot] 226e567987 Apply translations in vi
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'vi' language.
2023-04-06 20:38:48 -04:00
transifex-integration[bot] 898ffc0186 Apply translations in es_ES
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_ES' language.
2023-04-06 20:38:48 -04:00
transifex-integration[bot] 4d0f7b576b Apply translations in es_ES
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_ES' language.
2023-04-06 20:38:48 -04:00
transifex-integration[bot] 230d049384 Apply translations in es_ES
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_ES' language.
2023-04-06 20:38:47 -04:00
transifex-integration[bot] 774a4173b0 Apply translations in es_ES
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_ES' language.
2023-04-06 20:38:47 -04:00
transifex-integration[bot] f8b5d91720 Apply translations in es_ES
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_ES' language.
2023-04-06 20:38:47 -04:00
transifex-integration[bot] 0832c82ee8 Apply translations in es_ES
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_ES' language.
2023-04-06 20:38:46 -04:00
William Casarin 4786c6f0cb Don't show Translating... if we're not actually translating
Changelog-Fixed: Don't show Translating... if we're not actually translating
2023-04-06 17:33:01 -07:00
William Casarin 8d0aea22fd Custom hashtag offsets
This allows you to tweak the positioning a bit
2023-04-06 17:28:43 -07:00
William Casarin 3d27e49e70 Add custom #nostr and #coffeechain hashtags
Changelog-Added: Custom hashtags for #nostr and #coffeechain
2023-04-06 17:09:50 -07:00
William Casarin e9be227009 Add bitcoin icon to bitcoin hashtags
Changelog-Added: Add bitcoin icon to bitcoin hashtags
2023-04-06 16:04:16 -07:00
William Casarin 2e640db012 Make bitcoin hashtag orange 2023-04-06 12:10:39 -07:00
William Casarin eed16449fe oops, we're on build 5 2023-04-06 12:10:31 -07:00
William Casarin 3661d64450 regression: remove action bar from DM view 2023-04-06 12:10:17 -07:00
William Casarin 24c82293b3 Disable translations in DMs by default
There is an option to enable it now.

Changelog-Changed: Disable translations in DMs by default
2023-04-06 12:07:01 -07:00
William Casarin 7fb1bc48c4 rename NostrBuild to nostr.build
Closes: #883
2023-04-06 11:05:51 -07:00
William Casarin 2ae4a156da Make sure we only update translation cache on main thread 2023-04-06 10:39:01 -07:00
William Casarin f700dd799f v1.4.1-4 changelog 2023-04-06 10:31:52 -07:00
William Casarin 00548adc1f v1.4.1-4 2023-04-06 10:31:20 -07:00
William Casarin 7c08d4af45 perf: cache note artifacts 2023-04-06 10:27:15 -07:00
William Casarin c7bf1da797 perf: cache lnurl to avoid bech32 encoding on the main thread 2023-04-06 10:26:51 -07:00
William Casarin c5341ba337 Cache translations, fix translation popping
Completely refactor Translate View. Simplify bool logic into a state enum.

Changelog-Fixed: Fix translation text popping
Changelog-Added: Cache translations
2023-04-06 10:16:53 -07:00
William Casarin 95fb7bccf8 Merge remote-tracking branch 'github/translations' 2023-04-06 08:35:24 -07:00
William Casarin 6499738994 Fix auto-translations
Changelog-Fixed: Fix broken auto-translations
2023-04-06 08:27:27 -07:00
William Casarin 532eaa35bf pointless refactors 2023-04-06 08:27:27 -07:00
tyiu 9c1bb30f5f Localization fix and export translations 2023-04-06 10:47:51 -04:00
transifex-integration[bot] 4683b417b9 Apply translations in pl_PL
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pl_PL' language.
2023-04-06 10:42:42 -04:00
transifex-integration[bot] 2176b58215 Apply translations in zh_TW
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_TW' language.
2023-04-06 10:42:42 -04:00
transifex-integration[bot] 3c7627bb9a Apply translations in zh_HK
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_HK' language.
2023-04-06 10:42:41 -04:00
transifex-integration[bot] 463ca5ce7b Apply translations in fr_FR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'fr_FR' language.
2023-04-06 10:42:41 -04:00
transifex-integration[bot] b6a0626890 Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-06 10:42:41 -04:00
transifex-integration[bot] dbfad189cb Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-06 10:42:41 -04:00
transifex-integration[bot] 8159213ef0 Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-04-06 10:42:41 -04:00
transifex-integration[bot] 684841c03a Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.
2023-04-06 10:42:41 -04:00
transifex-integration[bot] e429bd7df9 Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.
2023-04-06 10:42:41 -04:00
transifex-integration[bot] 45ebcf565a Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-06 10:42:41 -04:00
transifex-integration[bot] c031b08c9a Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-06 10:42:41 -04:00
transifex-integration[bot] d98503732e Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'de' language.
2023-04-06 10:42:40 -04:00
transifex-integration[bot] bfe0a39370 Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'de' language.
2023-04-06 10:42:40 -04:00
transifex-integration[bot] 36de599326 Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'de' language.
2023-04-06 10:42:40 -04:00
transifex-integration[bot] 3cbdfb7ddf Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'de' language.
2023-04-06 10:42:40 -04:00
transifex-integration[bot] ddd309634f Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'de' language.
2023-04-06 10:42:40 -04:00
transifex-integration[bot] 6bbf56f80c Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'de' language.
2023-04-06 10:42:40 -04:00
transifex-integration[bot] de1332d6ea Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'de' language.
2023-04-06 10:42:40 -04:00
transifex-integration[bot] 648f25ea7a Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-06 10:42:40 -04:00
transifex-integration[bot] b46dc04ae8 Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-06 10:42:40 -04:00
transifex-integration[bot] 8cabfaf90e Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-06 10:42:39 -04:00
transifex-integration[bot] 0b02956bb7 Apply translations in nl
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-04-06 10:42:39 -04:00
transifex-integration[bot] 4abea02d6e Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'pt_BR' language.
2023-04-06 10:42:39 -04:00
transifex-integration[bot] 5d20c49f7d Apply translations in pt_BR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'pt_BR' language.
2023-04-06 10:42:39 -04:00
transifex-integration[bot] c11e0fa919 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-06 10:42:39 -04:00
transifex-integration[bot] 54bfeb04da Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-06 10:42:39 -04:00
transifex-integration[bot] 214eec9cb6 Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-06 10:42:39 -04:00
transifex-integration[bot] b48de9525e Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-06 10:42:39 -04:00
transifex-integration[bot] 690f3ec740 Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-06 10:42:39 -04:00
tyiu 6df4ffc1f0 Export translations 2023-04-06 10:42:38 -04:00
transifex-integration[bot] 83cead1fbb Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-06 10:42:38 -04:00
transifex-integration[bot] 478600cf0e Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-06 10:42:38 -04:00
transifex-integration[bot] 75002802fa Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-06 10:42:38 -04:00
transifex-integration[bot] faf072d432 Apply translations in de
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'de' language.
2023-04-06 10:42:38 -04:00
transifex-integration[bot] e888da26a7 Apply translations in nl
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-04-06 10:42:38 -04:00
William Casarin b532dc48e1 Also trim whitespace before single note refs 2023-04-05 13:30:05 -07:00
209 changed files with 6081 additions and 2626 deletions
+147 -1
View File
@@ -1,3 +1,150 @@
## [1.4.3-10] - 2023-04-25
### Added
- Add paste button to login (Suhail Saqan)
- Add nokyctranslate translation option (symbsrcool)
- You can now change the default zap type (William Casarin)
- Add partial support for different repost variants (William Casarin)
### Changed
- Change 500 custom zap to 420 (William Casarin)
- New looks to the custom zaps view (ericholguin)
- Adjust attachment images placement when posting (Swift)
- Only show friends, not friend-of-friend in friend filter (William Casarin)
### Fixed
- Fix reposts on macos and ipad (William Casarin)
- Fix slow reconnection issues (Bryan Montz)
- Fix issue where uploaded images were from someone else (Swift)
- Fix crash with LibreTranslate server setting selection and remove delisted vern server (Terry Yiu)
- Fix buggy zap amounts and wallet selector settings (William Casarin)
[1.4.3-10]: https://github.com/damus-io/damus/releases/tag/v1.4.3-10
## [1.4.3-2] - 2023-04-17
### Added
- Add deep links for local notifications (Swift)
- Add thread muting (Terry Yiu)
- Preview media uploads when posting (Swift)
- Add QR Code in profiles (ericholguin)
### Changed
- Always check signatures of profile events (William Casarin)
- Ask permission before uploading media (Swift)
- Show DM message in local notification (William Casarin)
### Fixed
- Fixed repost turning green too early and not reposting sometimes (Swift)
- Fix shuffling when choosing users to reply to (Joshua Jiang)
- Do not translate own notes if logged in with private key (Terry Yiu)
- Load missing profiles from boosts on home view (Gísli Kristjánsson)
- Load missing profiles from boosts on profile view (Gísli Kristjánsson)
- Fix tap area when mentioning users (OlegAba)
- Fix invalid DM author notifications (William Casarin)
- Fix relay signal indicator, properly show how many relays you are connected to (William Casarin)
[1.4.3-2]: https://github.com/damus-io/damus/releases/tag/v1.4.3-2
## [1.4.2-2] - 2023-04-12
### Added
- Include #btc in custom #bitcoin hashtag (William Casarin)
- Make notification dots configurable (William Casarin)
### Changed
- Display follows in most recent to oldest (Luis Cabrera)
### Fixed
- Fix hitches caused by syncronous loading of cached images (William Casarin)
- Fix tabs sometimes not switching (William Casarin)
[1.4.2-2]: https://github.com/damus-io/damus/releases/tag/v1.4.2-2
## [1.4.1-8] - 2023-04-10
### Added
- Add support for nostr: bech32 urls in posts and DMs (NIP19) (Bartholomew Joyce)
### Fixed
- Don't leak mentions in DMs (William Casarin)
- Fix tap area when mentioning users (OlegAba)
[1.4.1-8]: https://github.com/damus-io/damus/releases/tag/v1.4.1-8
## [1.4.1-7] - 2023-04-07
### Added
- Add #zap and #zapathon custom hashtags (William Casarin)
- Add custom #plebchain icon (William Casarin)
### Changed
- Add validation to prevent whitespaces be inputted on NIP-05 input field (Terry Yiu)
- Change reply color from blue to purple. Blue is banned from Damus. (William Casarin)
### Fixed
- Fix padding in post view (OlegAba)
- Show most recently bookmarked notes at the top (Bryan Montz)
[1.4.1-7]: https://github.com/damus-io/damus/releases/tag/v1.4.1-7
## [1.4.1-6] - 2023-04-06
### Added
- Custom hashtags for #bitcoin, #nostr and #coffeechain (William Casarin)
### Changed
- Disable translations in DMs by default (William Casarin)
### Fixed
- Don't show Translating... if we're not actually translating (William Casarin)
[1.4.1-6]: https://github.com/damus-io/damus/releases/tag/v1.4.1-6
## [1.4.1-4] - 2023-04-06
### Added
- Cache translations (William Casarin)
### Fixed
- Fix translation text popping (William Casarin)
- Fix broken auto-translations (William Casarin)
- Fix extraneous padding on some image posts (William Casarin)
- Fix crash in relay list view (William Casarin)
[1.4.1-4]: https://github.com/damus-io/damus/releases/tag/v1.4.1-4
## [1.4.1-3] - 2023-04-05
### Added
@@ -921,4 +1068,3 @@
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2
-1
View File
@@ -1,4 +1,3 @@
dependencies: [
.Package(url: "https://github.com/daltoniam/Starscream.git", majorVersion: 4),
.Package(url: "https://github.com/jb55/secp256k1.swift.git", branch: "main")
]
+6 -1
View File
@@ -92,7 +92,7 @@ damus implements the following [Nostr Implementation Possibilities][nips]
## Contributing
Contributors welcome!
Contributors welcome! Start by examining known issues: https://github.com/damus-io/damus/issues.
### Code
@@ -100,6 +100,11 @@ Contributors welcome!
[git-send-email]: http://git-send-email.io
### Privacy
Your internet protocol (IP) address is exposed to the relays you connect to, and third party media hosters (e.g. nostr.build, imgur.com, giphy.com, youtube.com etc.) that render on Damus. If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN) from trackers online.
The relay also learns which public keys you are requesting, meaning your public key will be tied to your IP address.
### Translations
Translators welcome! Join the [Transifex][transifex] project.
+10 -3
View File
@@ -91,13 +91,12 @@ int bech32_encode(char *output, const char *hrp, const uint8_t *data, size_t dat
return 1;
}
bech32_encoding bech32_decode(char* hrp, uint8_t *data, size_t *data_len, const char *input, size_t max_input_len) {
bech32_encoding bech32_decode_len(char* hrp, uint8_t *data, size_t *data_len, const char *input, size_t input_len) {
uint32_t chk = 1;
size_t i;
size_t input_len = strlen(input);
size_t hrp_len;
int have_lower = 0, have_upper = 0;
if (input_len < 8 || input_len > max_input_len) {
if (input_len < 8) {
return BECH32_ENCODING_NONE;
}
*data_len = 0;
@@ -154,6 +153,14 @@ bech32_encoding bech32_decode(char* hrp, uint8_t *data, size_t *data_len, const
}
}
bech32_encoding bech32_decode(char* hrp, uint8_t *data, size_t *data_len, const char *input, size_t max_input_len) {
size_t len = strlen(input);
if (len > max_input_len) {
return BECH32_ENCODING_NONE;
}
return bech32_decode_len(hrp, data, data_len, input, len);
}
int bech32_convert_bits(uint8_t* out, size_t* outlen, int outbits, const uint8_t* in, size_t inlen, int inbits, int pad) {
uint32_t val = 0;
int bits = 0;
+8
View File
@@ -118,6 +118,14 @@ bech32_encoding bech32_decode(
size_t max_input_len
);
bech32_encoding bech32_decode_len(
char *hrp,
uint8_t *data,
size_t *data_len,
const char *input,
size_t input_len
);
/* Helper from bech32: translates inbits-bit bytes to outbits-bit bytes.
* @outlen is incremented as bytes are added.
* @pad is true if we're to pad, otherwise truncate last byte if necessary
+56
View File
@@ -0,0 +1,56 @@
//
// block.h
// damus
//
// Created by William Casarin on 2023-04-09.
//
#ifndef block_h
#define block_h
#include "nostr_bech32.h"
#include "str_block.h"
#define MAX_BLOCKS 1024
enum block_type {
BLOCK_HASHTAG = 1,
BLOCK_TEXT = 2,
BLOCK_MENTION_INDEX = 3,
BLOCK_MENTION_BECH32 = 4,
BLOCK_URL = 5,
BLOCK_INVOICE = 6,
};
typedef struct invoice_block {
struct str_block invstr;
union {
struct bolt11 *bolt11;
};
} invoice_block_t;
typedef struct mention_bech32_block {
struct str_block str;
struct nostr_bech32 bech32;
} mention_bech32_block_t;
typedef struct block {
enum block_type type;
union {
struct str_block str;
struct invoice_block invoice;
struct mention_bech32_block mention_bech32;
int mention_index;
} block;
} block_t;
typedef struct blocks {
int num_blocks;
struct block *blocks;
} blocks_t;
void blocks_init(struct blocks *blocks);
void blocks_free(struct blocks *blocks);
#endif /* block_h */
+171
View File
@@ -0,0 +1,171 @@
//
// cursor.h
// damus
//
// Created by William Casarin on 2023-04-09.
//
#ifndef cursor_h
#define cursor_h
#include <ctype.h>
#include <string.h>
typedef unsigned char u8;
struct cursor {
const u8 *p;
const u8 *start;
const u8 *end;
};
static inline int is_whitespace(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
}
static inline int is_boundary(char c) {
return !isalnum(c);
}
static inline int is_invalid_url_ending(char c) {
return c == '!' || c == '?' || c == ')' || c == '.' || c == ',' || c == ';';
}
static inline int is_alphanumeric(char c) {
return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
}
static inline void make_cursor(struct cursor *c, const u8 *content, size_t len)
{
c->start = content;
c->end = content + len;
c->p = content;
}
static inline int consume_until_boundary(struct cursor *cur) {
char c;
while (cur->p < cur->end) {
c = *cur->p;
if (is_boundary(c))
return 1;
cur->p++;
}
return 1;
}
static inline int consume_until_whitespace(struct cursor *cur, int or_end) {
char c;
int consumedAtLeastOne = 0;
while (cur->p < cur->end) {
c = *cur->p;
if (is_whitespace(c))
return consumedAtLeastOne;
cur->p++;
consumedAtLeastOne = 1;
}
return or_end;
}
static inline int consume_until_non_alphanumeric(struct cursor *cur, int or_end) {
char c;
int consumedAtLeastOne = 0;
while (cur->p < cur->end) {
c = *cur->p;
if (!is_alphanumeric(c))
return consumedAtLeastOne;
cur->p++;
consumedAtLeastOne = 1;
}
return or_end;
}
static inline int parse_char(struct cursor *cur, char c) {
if (cur->p >= cur->end)
return 0;
if (*cur->p == c) {
cur->p++;
return 1;
}
return 0;
}
static inline int peek_char(struct cursor *cur, int ind) {
if ((cur->p + ind < cur->start) || (cur->p + ind >= cur->end))
return -1;
return *(cur->p + ind);
}
static int parse_digit(struct cursor *cur, int *digit) {
int c;
if ((c = peek_char(cur, 0)) == -1)
return 0;
c -= '0';
if (c >= 0 && c <= 9) {
*digit = c;
cur->p++;
return 1;
}
return 0;
}
static inline int pull_byte(struct cursor *cur, u8 *byte) {
if (cur->p >= cur->end)
return 0;
*byte = *cur->p;
cur->p++;
return 1;
}
static inline int pull_bytes(struct cursor *cur, int count, const u8 **bytes) {
if (cur->p + count > cur->end)
return 0;
*bytes = cur->p;
cur->p += count;
return 1;
}
static inline int parse_str(struct cursor *cur, const char *str) {
int i;
char c, cs;
unsigned long len;
len = strlen(str);
if (cur->p + len >= cur->end)
return 0;
for (i = 0; i < len; i++) {
c = tolower(cur->p[i]);
cs = tolower(str[i]);
if (c != cs)
return 0;
}
cur->p += len;
return 1;
}
#endif /* cursor_h */
+43 -123
View File
@@ -6,127 +6,13 @@
//
#include "damus.h"
#include "cursor.h"
#include "bolt11.h"
#include "bech32.h"
#include <stdlib.h>
#include <string.h>
typedef unsigned char u8;
struct cursor {
const u8 *p;
const u8 *start;
const u8 *end;
};
static inline int is_whitespace(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
}
static inline int is_boundary(char c) {
return !isalnum(c);
}
static inline int is_invalid_url_ending(char c) {
return c == '!' || c == '?' || c == ')' || c == '.' || c == ',' || c == ';';
}
static void make_cursor(struct cursor *c, const u8 *content, size_t len)
{
c->start = content;
c->end = content + len;
c->p = content;
}
static int consume_until_boundary(struct cursor *cur) {
char c;
while (cur->p < cur->end) {
c = *cur->p;
if (is_boundary(c))
return 1;
cur->p++;
}
return 1;
}
static int consume_until_whitespace(struct cursor *cur, int or_end) {
char c;
bool consumedAtLeastOne = false;
while (cur->p < cur->end) {
c = *cur->p;
if (is_whitespace(c))
return consumedAtLeastOne;
cur->p++;
consumedAtLeastOne = true;
}
return or_end;
}
static int parse_char(struct cursor *cur, char c) {
if (cur->p >= cur->end)
return 0;
if (*cur->p == c) {
cur->p++;
return 1;
}
return 0;
}
static inline int peek_char(struct cursor *cur, int ind) {
if ((cur->p + ind < cur->start) || (cur->p + ind >= cur->end))
return -1;
return *(cur->p + ind);
}
static int parse_digit(struct cursor *cur, int *digit) {
int c;
if ((c = peek_char(cur, 0)) == -1)
return 0;
c -= '0';
if (c >= 0 && c <= 9) {
*digit = c;
cur->p++;
return 1;
}
return 0;
}
static int parse_str(struct cursor *cur, const char *str) {
int i;
char c, cs;
unsigned long len;
len = strlen(str);
if (cur->p + len >= cur->end)
return 0;
for (i = 0; i < len; i++) {
c = tolower(cur->p[i]);
cs = tolower(str[i]);
if (c != cs)
return 0;
}
cur->p += len;
return 1;
}
static int parse_mention(struct cursor *cur, struct block *block) {
static int parse_mention_index(struct cursor *cur, struct block *block) {
int d1, d2, d3, ind;
const u8 *start = cur->p;
@@ -151,8 +37,8 @@ static int parse_mention(struct cursor *cur, struct block *block) {
return 0;
}
block->type = BLOCK_MENTION;
block->block.mention = ind;
block->type = BLOCK_MENTION_INDEX;
block->block.mention_index = ind;
return 1;
}
@@ -274,6 +160,27 @@ static int parse_invoice(struct cursor *cur, struct block *block) {
return 1;
}
static int parse_mention_bech32(struct cursor *cur, struct block *block) {
const u8 *start = cur->p;
if (!parse_str(cur, "nostr:"))
return 0;
block->block.str.start = (const char *)cur->p;
if (!parse_nostr_bech32(cur, &block->block.mention_bech32.bech32)) {
cur->p = start;
return 0;
}
block->block.str.end = (const char *)cur->p;
block->type = BLOCK_MENTION_BECH32;
return 1;
}
static int add_text_then_block(struct cursor *cur, struct blocks *blocks, struct block block, const u8 **start, const u8 *pre_mention)
{
if (!add_text_block(blocks, *start, pre_mention))
@@ -303,7 +210,7 @@ int damus_parse_content(struct blocks *blocks, const char *content) {
pre_mention = cur.p;
if (cp == -1 || is_whitespace(cp)) {
if (c == '#' && (parse_mention(&cur, &block) || parse_hashtag(&cur, &block))) {
if (c == '#' && (parse_mention_index(&cur, &block) || parse_hashtag(&cur, &block))) {
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
return 0;
continue;
@@ -315,6 +222,10 @@ int damus_parse_content(struct blocks *blocks, const char *content) {
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
return 0;
continue;
} else if (c == 'n' && parse_mention_bech32(&cur, &block)) {
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
return 0;
continue;
}
}
@@ -335,8 +246,17 @@ void blocks_init(struct blocks *blocks) {
}
void blocks_free(struct blocks *blocks) {
if (blocks->blocks) {
free(blocks->blocks);
blocks->num_blocks = 0;
if (!blocks->blocks) {
return;
}
for (int i = 0; i < blocks->num_blocks; ++i) {
if (blocks->blocks[i].type == BLOCK_MENTION_BECH32) {
free(blocks->blocks[i].block.mention_bech32.bech32.buffer);
blocks->blocks[i].block.mention_bech32.bech32.buffer = NULL;
}
}
free(blocks->blocks);
blocks->num_blocks = 0;
}
+3 -38
View File
@@ -9,45 +9,10 @@
#define damus_h
#include <stdio.h>
#include "nostr_bech32.h"
#include "block.h"
typedef unsigned char u8;
#define MAX_BLOCKS 1024
enum block_type {
BLOCK_HASHTAG = 1,
BLOCK_TEXT = 2,
BLOCK_MENTION = 3,
BLOCK_URL = 4,
BLOCK_INVOICE = 5,
};
typedef struct str_block {
const char *start;
const char *end;
} str_block_t;
typedef struct invoice_block {
struct str_block invstr;
union {
struct bolt11 *bolt11;
};
} invoice_block_t;
typedef struct block {
enum block_type type;
union {
struct str_block str;
struct invoice_block invoice;
int mention;
} block;
} block_t;
typedef struct blocks {
int num_blocks;
struct block *blocks;
} blocks_t;
void blocks_init(struct blocks *blocks);
void blocks_free(struct blocks *blocks);
int damus_parse_content(struct blocks *blocks, const char *content);
#endif /* damus_h */
+295
View File
@@ -0,0 +1,295 @@
//
// nostr_bech32.c
// damus
//
// Created by William Casarin on 2023-04-09.
//
#include "nostr_bech32.h"
#include <stdlib.h>
#include "cursor.h"
#include "bech32.h"
#define MAX_TLVS 16
#define TLV_SPECIAL 0
#define TLV_RELAY 1
#define TLV_AUTHOR 2
#define TLV_KIND 3
#define TLV_KNOWN_TLVS 4
struct nostr_tlv {
u8 type;
u8 len;
const u8 *value;
};
struct nostr_tlvs {
struct nostr_tlv tlvs[MAX_TLVS];
int num_tlvs;
};
static int parse_nostr_tlv(struct cursor *cur, struct nostr_tlv *tlv) {
// get the tlv tag
if (!pull_byte(cur, &tlv->type))
return 0;
// unknown, fail!
if (tlv->type >= TLV_KNOWN_TLVS)
return 0;
// get the length
if (!pull_byte(cur, &tlv->len))
return 0;
// is the reported length greater then our buffer? if so fail
if (cur->p + tlv->len > cur->end)
return 0;
tlv->value = cur->p;
cur->p += tlv->len;
return 1;
}
static int parse_nostr_tlvs(struct cursor *cur, struct nostr_tlvs *tlvs) {
int i;
tlvs->num_tlvs = 0;
for (i = 0; i < MAX_TLVS; i++) {
if (parse_nostr_tlv(cur, &tlvs->tlvs[i])) {
tlvs->num_tlvs++;
} else {
break;
}
}
if (tlvs->num_tlvs == 0)
return 0;
return 1;
}
static int find_tlv(struct nostr_tlvs *tlvs, u8 type, struct nostr_tlv **tlv) {
*tlv = NULL;
for (int i = 0; i < tlvs->num_tlvs; i++) {
if (tlvs->tlvs[i].type == type) {
*tlv = &tlvs->tlvs[i];
return 1;
}
}
return 0;
}
static int parse_nostr_bech32_type(const char *prefix, enum nostr_bech32_type *type) {
// Parse type
if (strcmp(prefix, "note") == 0) {
*type = NOSTR_BECH32_NOTE;
return 1;
} else if (strcmp(prefix, "npub") == 0) {
*type = NOSTR_BECH32_NPUB;
return 1;
} else if (strcmp(prefix, "nprofile") == 0) {
*type = NOSTR_BECH32_NPROFILE;
return 1;
} else if (strcmp(prefix, "nevent") == 0) {
*type = NOSTR_BECH32_NEVENT;
return 1;
} else if (strcmp(prefix, "nrelay") == 0) {
*type = NOSTR_BECH32_NRELAY;
return 1;
} else if (strcmp(prefix, "naddr") == 0) {
*type = NOSTR_BECH32_NADDR;
return 1;
}
return 0;
}
static int parse_nostr_bech32_note(struct cursor *cur, struct bech32_note *note) {
return pull_bytes(cur, 32, &note->event_id);
}
static int parse_nostr_bech32_npub(struct cursor *cur, struct bech32_npub *npub) {
return pull_bytes(cur, 32, &npub->pubkey);
}
static int tlvs_to_relays(struct nostr_tlvs *tlvs, struct relays *relays) {
struct nostr_tlv *tlv;
struct str_block *str;
relays->num_relays = 0;
for (int i = 0; i < tlvs->num_tlvs; i++) {
tlv = &tlvs->tlvs[i];
if (tlv->type != TLV_RELAY)
continue;
if (relays->num_relays + 1 > MAX_RELAYS)
break;
str = &relays->relays[relays->num_relays++];
str->start = (const char*)tlv->value;
str->end = (const char*)(tlv->value + tlv->len);
}
return 1;
}
static int parse_nostr_bech32_nevent(struct cursor *cur, struct bech32_nevent *nevent) {
struct nostr_tlvs tlvs;
struct nostr_tlv *tlv;
if (!parse_nostr_tlvs(cur, &tlvs))
return 0;
if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
return 0;
if (tlv->len != 32)
return 0;
nevent->event_id = tlv->value;
if (find_tlv(&tlvs, TLV_AUTHOR, &tlv)) {
nevent->pubkey = tlv->value;
} else {
nevent->pubkey = NULL;
}
return tlvs_to_relays(&tlvs, &nevent->relays);
}
static int parse_nostr_bech32_naddr(struct cursor *cur, struct bech32_naddr *naddr) {
struct nostr_tlvs tlvs;
struct nostr_tlv *tlv;
if (!parse_nostr_tlvs(cur, &tlvs))
return 0;
if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
return 0;
naddr->identifier.start = (const char*)tlv->value;
naddr->identifier.end = (const char*)tlv->value + tlv->len;
if (!find_tlv(&tlvs, TLV_AUTHOR, &tlv))
return 0;
naddr->pubkey = tlv->value;
return tlvs_to_relays(&tlvs, &naddr->relays);
}
static int parse_nostr_bech32_nprofile(struct cursor *cur, struct bech32_nprofile *nprofile) {
struct nostr_tlvs tlvs;
struct nostr_tlv *tlv;
if (!parse_nostr_tlvs(cur, &tlvs))
return 0;
if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
return 0;
if (tlv->len != 32)
return 0;
nprofile->pubkey = tlv->value;
return tlvs_to_relays(&tlvs, &nprofile->relays);
}
static int parse_nostr_bech32_nrelay(struct cursor *cur, struct bech32_nrelay *nrelay) {
struct nostr_tlvs tlvs;
struct nostr_tlv *tlv;
if (!parse_nostr_tlvs(cur, &tlvs))
return 0;
if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
return 0;
nrelay->relay.start = (const char*)tlv->value;
nrelay->relay.end = (const char*)tlv->value + tlv->len;
return 1;
}
int parse_nostr_bech32(struct cursor *cur, struct nostr_bech32 *obj) {
const u8 *start, *end;
start = cur->p;
if (!consume_until_non_alphanumeric(cur, 1)) {
cur->p = start;
return 0;
}
end = cur->p;
size_t data_len;
size_t input_len = end - start;
if (input_len < 10 || input_len > 10000) {
return 0;
}
obj->buffer = malloc(input_len * 2);
if (!obj->buffer)
return 0;
u8 data[input_len];
char prefix[input_len];
if (bech32_decode_len(prefix, data, &data_len, (const char*)start, input_len) == BECH32_ENCODING_NONE) {
cur->p = start;
return 0;
}
obj->buflen = 0;
if (!bech32_convert_bits(obj->buffer, &obj->buflen, 8, data, data_len, 5, 0)) {
goto fail;
}
if (!parse_nostr_bech32_type(prefix, &obj->type)) {
goto fail;
}
struct cursor bcur;
make_cursor(&bcur, obj->buffer, obj->buflen);
switch (obj->type) {
case NOSTR_BECH32_NOTE:
if (!parse_nostr_bech32_note(&bcur, &obj->data.note))
goto fail;
break;
case NOSTR_BECH32_NPUB:
if (!parse_nostr_bech32_npub(&bcur, &obj->data.npub))
goto fail;
break;
case NOSTR_BECH32_NEVENT:
if (!parse_nostr_bech32_nevent(&bcur, &obj->data.nevent))
goto fail;
break;
case NOSTR_BECH32_NADDR:
if (!parse_nostr_bech32_naddr(&bcur, &obj->data.naddr))
goto fail;
break;
case NOSTR_BECH32_NPROFILE:
if (!parse_nostr_bech32_nprofile(&bcur, &obj->data.nprofile))
goto fail;
break;
case NOSTR_BECH32_NRELAY:
if (!parse_nostr_bech32_nrelay(&bcur, &obj->data.nrelay))
goto fail;
break;
}
return 1;
fail:
free(obj->buffer);
cur->p = start;
return 0;
}
+78
View File
@@ -0,0 +1,78 @@
//
// nostr_bech32.h
// damus
//
// Created by William Casarin on 2023-04-09.
//
#ifndef nostr_bech32_h
#define nostr_bech32_h
#include <stdio.h>
#include "str_block.h"
#include "cursor.h"
typedef unsigned char u8;
#define MAX_RELAYS 10
struct relays {
struct str_block relays[MAX_RELAYS];
int num_relays;
};
enum nostr_bech32_type {
NOSTR_BECH32_NOTE = 1,
NOSTR_BECH32_NPUB = 2,
NOSTR_BECH32_NPROFILE = 3,
NOSTR_BECH32_NEVENT = 4,
NOSTR_BECH32_NRELAY = 5,
NOSTR_BECH32_NADDR = 6,
};
struct bech32_note {
const u8 *event_id;
};
struct bech32_npub {
const u8 *pubkey;
};
struct bech32_nevent {
struct relays relays;
const u8 *event_id;
const u8 *pubkey; // optional
};
struct bech32_nprofile {
struct relays relays;
const u8 *pubkey;
};
struct bech32_naddr {
struct relays relays;
struct str_block identifier;
const u8 *pubkey;
};
struct bech32_nrelay {
struct str_block relay;
};
typedef struct nostr_bech32 {
enum nostr_bech32_type type;
u8 *buffer; // holds strings and tlv stuff
size_t buflen;
union {
struct bech32_note note;
struct bech32_npub npub;
struct bech32_nevent nevent;
struct bech32_nprofile nprofile;
struct bech32_naddr naddr;
struct bech32_nrelay nrelay;
} data;
} nostr_bech32_t;
int parse_nostr_bech32(struct cursor *cur, struct nostr_bech32 *obj);
#endif /* nostr_bech32_h */
+16
View File
@@ -0,0 +1,16 @@
//
// str_block.h
// damus
//
// Created by William Casarin on 2023-04-09.
//
#ifndef str_block_h
#define str_block_h
typedef struct str_block {
const char *start;
const char *end;
} str_block_t;
#endif /* str_block_h */
+150 -56
View File
@@ -17,7 +17,7 @@
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; };
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */; };
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; };
3A48E23B29D518F000BA313D /* Translations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E23A29D518F000BA313D /* Translations.swift */; };
3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; };
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
@@ -38,6 +38,12 @@
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F8E280F640A000448DE /* ThreadModel.swift */; };
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F90280F6528000448DE /* ChatView.swift */; };
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; };
4C198DF829F89323004C165C /* BinaryParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C198DF729F89323004C165C /* BinaryParser.swift */; };
4C198DEF29F88C6B004C165C /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C198DEB29F88C6B004C165C /* BlurHashEncode.swift */; };
4C198DF029F88C6B004C165C /* Readme.md in Resources */ = {isa = PBXBuildFile; fileRef = 4C198DEC29F88C6B004C165C /* Readme.md */; };
4C198DF129F88C6B004C165C /* License.txt in Resources */ = {isa = PBXBuildFile; fileRef = 4C198DED29F88C6B004C165C /* License.txt */; };
4C198DF229F88C6B004C165C /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C198DEE29F88C6B004C165C /* BlurHashDecode.swift */; };
4C198DF529F88D2E004C165C /* ImageMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C198DF429F88D2E004C165C /* ImageMetadata.swift */; };
4C1A9A1A29DCA17E00516EAC /* ReplyCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */; };
4C1A9A1D29DDCF9B00516EAC /* NotificationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A1C29DDCF9B00516EAC /* NotificationSettingsView.swift */; };
4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A1E29DDD24B00516EAC /* AppearanceSettingsView.swift */; };
@@ -121,7 +127,6 @@
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5F9117283D88E40052CD1C /* FollowingModel.swift */; };
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C63334F283D40E500B1C9C3 /* HomeModel.swift */; };
4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C633351283D419F00B1C9C3 /* SignalModel.swift */; };
4C649844285A952100EAE2B3 /* LocalUserConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C649843285A952100EAE2B3 /* LocalUserConfig.swift */; };
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */; };
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */; };
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 4C649880286E0EE300EAE2B3 /* secp256k1 */; };
@@ -137,8 +142,14 @@
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFBA2804A34C0006080F /* ProofOfWork.swift */; };
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FF7D42823313F009601DB /* Mentions.swift */; };
4C8682872814DE470026224F /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8682862814DE470026224F /* ProfileView.swift */; };
4C8D00C829DF791C0036AF10 /* CompatibleAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */; };
4C8D00CA29DF80350036AF10 /* TruncatedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00C929DF80350036AF10 /* TruncatedText.swift */; };
4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */; };
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 */; };
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */; };
4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD152839DB54008EE7EF /* NostrMetadata.swift */; };
4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD17283A9EE5008EE7EF /* LoginView.swift */; };
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD19283AA67F008EE7EF /* Bech32.swift */; };
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */; };
@@ -149,6 +160,8 @@
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */; };
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */; };
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; };
4CA3FA1029F593D000FDB3C3 /* ZapTypePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA3FA0F29F593D000FDB3C3 /* ZapTypePicker.swift */; };
4CA5588329F33F5B00DC6A45 /* StringCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA5588229F33F5B00DC6A45 /* StringCodable.swift */; };
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AC298851D000060CEA /* AccountDeletion.swift */; };
4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */; };
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */; };
@@ -177,7 +190,6 @@
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */; };
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEC297F0B9E00430951 /* Highlight.swift */; };
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEF297F11C700430951 /* SelectedEventView.swift */; };
4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF1297F129C00430951 /* EmbeddedEventView.swift */; };
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF3297F18B400430951 /* ReplyDescription.swift */; };
4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF5297F1A6A00430951 /* EventBody.swift */; };
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */; };
@@ -187,9 +199,14 @@
4CCEB7B029B5415A0078AA28 /* SearchingProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */; };
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; };
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128929E9D10C0006FA5A /* SignalView.swift */; };
4CDA128C29EB19C40006FA5A /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128B29EB19C40006FA5A /* LocalNotification.swift */; };
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */; };
4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */; };
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */; };
4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE1398F29F0661A00AC6A0B /* RepostAction.swift */; };
4CE1399229F0666100AC6A0B /* ShareActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE1399129F0666100AC6A0B /* ShareActionButton.swift */; };
4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE1399329F0669900AC6A0B /* BigButton.swift */; };
4CE4F0F229D4FCFA005914DB /* DebouncedOnChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */; };
4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F0F329D779B5005914DB /* PostBox.swift */; };
4CE4F0F829DB7399005914DB /* ThiccDivider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F0F729DB7399005914DB /* ThiccDivider.swift */; };
@@ -204,7 +221,6 @@
4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DEF727F7A08200C66700 /* damusTests.swift */; };
4CE6DF0227F7A08200C66700 /* damusUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF0127F7A08200C66700 /* damusUITests.swift */; };
4CE6DF0427F7A08200C66700 /* damusUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF0327F7A08200C66700 /* damusUITestsLaunchTests.swift */; };
4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 4CE6DF1127F7A2B300C66700 /* Starscream */; };
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */; };
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794729941DA700F758CC /* RelayFilters.swift */; };
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */; };
@@ -241,6 +257,7 @@
4CFF8F6B29CD0079008DB934 /* RepostedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */; };
4CFF8F6D29CD022E008DB934 /* WideEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6C29CD022E008DB934 /* WideEventView.swift */; };
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
50088DA129E8271A008A1FDF /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50088DA029E8271A008A1FDF /* WebSocket.swift */; };
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; };
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; };
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
@@ -271,7 +288,7 @@
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */; };
F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */; };
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */; };
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParicipantsView.swift */; };
F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParticipantsView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -324,10 +341,7 @@
3A41E559299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A48E23A29D518F000BA313D /* Translations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Translations.swift; sourceTree = "<group>"; };
3A4F3320297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A4F3321297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A4F3322297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-FR"; path = "fr-FR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutedThreadsManager.swift; sourceTree = "<group>"; };
3A5C4575296A879E0032D398 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-419"; path = "es-419.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A5CAE1E298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/Localizable.strings"; sourceTree = "<group>"; };
@@ -335,6 +349,9 @@
3A66D927299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A66D928299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
3A66D929299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A821C3E29E819D500B4BCA7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
3A821C3F29E819D500B4BCA7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A821C4029E819D500B4BCA7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A827A18299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A827A19299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
3A827A1A299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@@ -381,9 +398,6 @@
3AD14EB829C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "sv-SE"; path = "sv-SE.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3AD14EB929C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sv-SE"; path = "sv-SE.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AD14EBA29C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sv-SE"; path = "sv-SE.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AD14EBB29C40F47009D2D9C /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AD14EBC29C40F47009D2D9C /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-CA"; path = "fr-CA.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3AD14EBD29C40F47009D2D9C /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AD5662B29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3AD5662C29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fa; path = fa.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3AD5662D29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = "<group>"; };
@@ -408,6 +422,12 @@
4C0A3F8E280F640A000448DE /* ThreadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadModel.swift; sourceTree = "<group>"; };
4C0A3F90280F6528000448DE /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
4C0A3F92280F66F5000448DE /* ReplyMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyMap.swift; sourceTree = "<group>"; };
4C198DF729F89323004C165C /* BinaryParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryParser.swift; sourceTree = "<group>"; };
4C198DEB29F88C6B004C165C /* BlurHashEncode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlurHashEncode.swift; sourceTree = "<group>"; };
4C198DEC29F88C6B004C165C /* Readme.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = Readme.md; sourceTree = "<group>"; };
4C198DED29F88C6B004C165C /* License.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = License.txt; sourceTree = "<group>"; };
4C198DEE29F88C6B004C165C /* BlurHashDecode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
4C198DF429F88D2E004C165C /* ImageMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageMetadata.swift; sourceTree = "<group>"; };
4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyCounter.swift; sourceTree = "<group>"; };
4C1A9A1C29DDCF9B00516EAC /* NotificationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsView.swift; sourceTree = "<group>"; };
4C1A9A1E29DDD24B00516EAC /* AppearanceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceSettingsView.swift; sourceTree = "<group>"; };
@@ -521,7 +541,6 @@
4C5F9117283D88E40052CD1C /* FollowingModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowingModel.swift; sourceTree = "<group>"; };
4C63334F283D40E500B1C9C3 /* HomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeModel.swift; sourceTree = "<group>"; };
4C633351283D419F00B1C9C3 /* SignalModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalModel.swift; sourceTree = "<group>"; };
4C649843285A952100EAE2B3 /* LocalUserConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalUserConfig.swift; sourceTree = "<group>"; };
4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesView.swift; sourceTree = "<group>"; };
4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesModel.swift; sourceTree = "<group>"; };
4C75EFA327FA577B0006080F /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = "<group>"; };
@@ -537,8 +556,18 @@
4C75EFBA2804A34C0006080F /* ProofOfWork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProofOfWork.swift; sourceTree = "<group>"; };
4C7FF7D42823313F009601DB /* Mentions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mentions.swift; sourceTree = "<group>"; };
4C8682862814DE470026224F /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompatibleAttribute.swift; sourceTree = "<group>"; };
4C8D00C929DF80350036AF10 /* TruncatedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TruncatedText.swift; sourceTree = "<group>"; };
4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hashtags.swift; sourceTree = "<group>"; };
4C8D00CD29E38B950036AF10 /* nostr_bech32.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = nostr_bech32.h; sourceTree = "<group>"; };
4C8D00CE29E38B950036AF10 /* nostr_bech32.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = nostr_bech32.c; sourceTree = "<group>"; };
4C8D00D029E38E4C0036AF10 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = "<group>"; };
4C8D00D129E397AD0036AF10 /* block.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = block.h; sourceTree = "<group>"; };
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>"; };
4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusColors.swift; sourceTree = "<group>"; };
4C90BD152839DB54008EE7EF /* NostrMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrMetadata.swift; sourceTree = "<group>"; };
4C90BD17283A9EE5008EE7EF /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
4C90BD19283AA67F008EE7EF /* Bech32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32.swift; sourceTree = "<group>"; };
4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Tests.swift; sourceTree = "<group>"; };
@@ -549,6 +578,8 @@
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = "<group>"; };
4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeAnonPfpView.swift; sourceTree = "<group>"; };
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
4CA3FA0F29F593D000FDB3C3 /* ZapTypePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapTypePicker.swift; sourceTree = "<group>"; };
4CA5588229F33F5B00DC6A45 /* StringCodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringCodable.swift; sourceTree = "<group>"; };
4CAAD8AC298851D000060CEA /* AccountDeletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletion.swift; sourceTree = "<group>"; };
4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConfigView.swift; sourceTree = "<group>"; };
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyView.swift; sourceTree = "<group>"; };
@@ -577,7 +608,6 @@
4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuilderEventView.swift; sourceTree = "<group>"; };
4CC7AAEC297F0B9E00430951 /* Highlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Highlight.swift; sourceTree = "<group>"; };
4CC7AAEF297F11C700430951 /* SelectedEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedEventView.swift; sourceTree = "<group>"; };
4CC7AAF1297F129C00430951 /* EmbeddedEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedEventView.swift; sourceTree = "<group>"; };
4CC7AAF3297F18B400430951 /* ReplyDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyDescription.swift; sourceTree = "<group>"; };
4CC7AAF5297F1A6A00430951 /* EventBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBody.swift; sourceTree = "<group>"; };
4CC7AAF7297F1CEE00430951 /* EventProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfile.swift; sourceTree = "<group>"; };
@@ -587,9 +617,14 @@
4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingProfileView.swift; sourceTree = "<group>"; };
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploadModel.swift; sourceTree = "<group>"; };
4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; };
4CDA128929E9D10C0006FA5A /* SignalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalView.swift; sourceTree = "<group>"; };
4CDA128B29EB19C40006FA5A /* LocalNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotification.swift; sourceTree = "<group>"; };
4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventHolder.swift; sourceTree = "<group>"; };
4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreButton.swift; sourceTree = "<group>"; };
4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InnerTimelineView.swift; sourceTree = "<group>"; };
4CE1398F29F0661A00AC6A0B /* RepostAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostAction.swift; sourceTree = "<group>"; };
4CE1399129F0666100AC6A0B /* ShareActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareActionButton.swift; sourceTree = "<group>"; };
4CE1399329F0669900AC6A0B /* BigButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BigButton.swift; sourceTree = "<group>"; };
4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebouncedOnChange.swift; sourceTree = "<group>"; };
4CE4F0F329D779B5005914DB /* PostBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostBox.swift; sourceTree = "<group>"; };
4CE4F0F729DB7399005914DB /* ThiccDivider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThiccDivider.swift; sourceTree = "<group>"; };
@@ -644,6 +679,7 @@
4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostedEvent.swift; sourceTree = "<group>"; };
4CFF8F6C29CD022E008DB934 /* WideEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WideEventView.swift; sourceTree = "<group>"; };
4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; };
50088DA029E8271A008A1FDF /* WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocket.swift; sourceTree = "<group>"; };
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; };
5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = "<group>"; };
5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; };
@@ -673,7 +709,7 @@
F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIPURLBuilder.swift; sourceTree = "<group>"; };
F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfilePictureControl.swift; sourceTree = "<group>"; };
F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToDismiss.swift; sourceTree = "<group>"; };
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParicipantsView.swift; sourceTree = "<group>"; };
F7F0BA262978E54D009531F3 /* ParticipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantsView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -683,7 +719,6 @@
files = (
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
6C7DE41F2955169800E66263 /* Vault in Frameworks */,
4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */,
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -773,6 +808,11 @@
4C3EA67428FF7A5A00C48A62 /* take.c */,
4C3EA67628FF7A9800C48A62 /* talstr.c */,
4C3EA67828FF7ABF00C48A62 /* list.c */,
4C8D00CD29E38B950036AF10 /* nostr_bech32.h */,
4C8D00CE29E38B950036AF10 /* nostr_bech32.c */,
4C8D00D029E38E4C0036AF10 /* cursor.h */,
4C8D00D129E397AD0036AF10 /* block.h */,
4C8D00D229E3C19F0036AF10 /* str_block.h */,
);
path = "damus-c";
sourceTree = "<group>";
@@ -807,7 +847,6 @@
4C5F9117283D88E40052CD1C /* FollowingModel.swift */,
4C987B56283FD07F0042CE38 /* FollowersModel.swift */,
4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */,
4C649843285A952100EAE2B3 /* LocalUserConfig.swift */,
4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */,
4C216F372871EDE300040376 /* DirectMessageModel.swift */,
4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */,
@@ -823,11 +862,38 @@
3AA59D1C2999B0400061C48E /* DraftsModel.swift */,
4C54AA0629A540BA003E4487 /* NotificationsModel.swift */,
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */,
3A48E23A29D518F000BA313D /* Translations.swift */,
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */,
);
path = Models;
sourceTree = "<group>";
};
4C198DF629F89317004C165C /* Parser */ = {
isa = PBXGroup;
children = (
4C198DF729F89323004C165C /* BinaryParser.swift */,
);
path = Parser;
sourceTree = "<group>";
};
4C198DEA29F88C6B004C165C /* BlurHash */ = {
isa = PBXGroup;
children = (
4C198DEB29F88C6B004C165C /* BlurHashEncode.swift */,
4C198DEC29F88C6B004C165C /* Readme.md */,
4C198DED29F88C6B004C165C /* License.txt */,
4C198DEE29F88C6B004C165C /* BlurHashDecode.swift */,
);
path = BlurHash;
sourceTree = "<group>";
};
4C198DF329F88D23004C165C /* Images */ = {
isa = PBXGroup;
children = (
4C198DF429F88D2E004C165C /* ImageMetadata.swift */,
);
path = Images;
sourceTree = "<group>";
};
4C1A9A1B29DDCF8B00516EAC /* Settings */ = {
isa = PBXGroup;
children = (
@@ -863,6 +929,7 @@
4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup;
children = (
4C8D1A6D29F31E4100ACDF75 /* Buttons */,
4C1A9A1B29DDCF8B00516EAC /* Settings */,
4CFF8F6129CC9A80008DB934 /* Images */,
4CCEB7AC29B53D180078AA28 /* Search */,
@@ -888,7 +955,6 @@
4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */,
4C216F31286E388800040376 /* DMChatView.swift */,
4C216F33286F5ACD00040376 /* DMView.swift */,
E990020E2955F837003BBC5A /* EditMetadataView.swift */,
3169CAE4294E699400EE4006 /* Empty Views */,
4C75EFB82804A2740006080F /* EventView.swift */,
4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */,
@@ -906,7 +972,7 @@
4C3AC7A42836987600E1F516 /* MainTabView.swift */,
4C363A8B28236B92006E126D /* PubkeyView.swift */,
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */,
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */,
F7F0BA262978E54D009531F3 /* ParticipantsView.swift */,
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */,
4C3AC7A628369BA200E1F516 /* SearchHomeView.swift */,
4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */,
@@ -943,7 +1009,7 @@
4CACA9DB280C38C000D9BBE8 /* Profiles.swift */,
4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */,
4C363A8F28247A1D006E126D /* NostrLink.swift */,
4C90BD152839DB54008EE7EF /* NostrMetadata.swift */,
50088DA029E8271A008A1FDF /* WebSocket.swift */,
);
path = Nostr;
sourceTree = "<group>";
@@ -951,6 +1017,9 @@
4C7FF7D628233637009601DB /* Util */ = {
isa = PBXGroup;
children = (
4C198DF629F89317004C165C /* Parser */,
4C198DF329F88D23004C165C /* Images */,
4C198DEA29F88C6B004C165C /* BlurHash */,
4CE4F0F329D779B5005914DB /* PostBox.swift */,
7C0F392D29B57C8F0039859C /* Extensions */,
4CE879492995B58700F758CC /* Relays */,
@@ -986,10 +1055,22 @@
4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */,
4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */,
4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */,
4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */,
4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */,
4CDA128B29EB19C40006FA5A /* LocalNotification.swift */,
4CA5588229F33F5B00DC6A45 /* StringCodable.swift */,
);
path = Util;
sourceTree = "<group>";
};
4C8D1A6D29F31E4100ACDF75 /* Buttons */ = {
isa = PBXGroup;
children = (
4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */,
);
path = Buttons;
sourceTree = "<group>";
};
4CAAD8AE29888A9B00060CEA /* Relays */ = {
isa = PBXGroup;
children = (
@@ -1001,6 +1082,7 @@
4CE8794D2996B16A00F758CC /* RelayToggle.swift */,
4CE8794F2996B2BD00F758CC /* RelayStatus.swift */,
4CE879512996B68900F758CC /* RelayType.swift */,
4CDA128929E9D10C0006FA5A /* SignalView.swift */,
);
path = Relays;
sourceTree = "<group>";
@@ -1011,6 +1093,9 @@
4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */,
4CB88388296AF99A00DC99E7 /* EventDetailBar.swift */,
5CF72FC129B9142F00124A13 /* ShareAction.swift */,
4CE1398F29F0661A00AC6A0B /* RepostAction.swift */,
4CE1399129F0666100AC6A0B /* ShareActionButton.swift */,
4CE1399329F0669900AC6A0B /* BigButton.swift */,
);
path = ActionBar;
sourceTree = "<group>";
@@ -1029,12 +1114,14 @@
4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */,
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */,
F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */,
E990020E2955F837003BBC5A /* EditMetadataView.swift */,
4CEE2AF2280B25C500AB5EEF /* ProfilePicView.swift */,
4C8682862814DE470026224F /* ProfileView.swift */,
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */,
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */,
4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */,
4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */,
4C8D1A6B29F1DFC200ACDF75 /* FriendIcon.swift */,
);
path = Profile;
sourceTree = "<group>";
@@ -1043,7 +1130,6 @@
isa = PBXGroup;
children = (
4CC7AAEF297F11C700430951 /* SelectedEventView.swift */,
4CC7AAF1297F129C00430951 /* EmbeddedEventView.swift */,
4CC7AAF3297F18B400430951 /* ReplyDescription.swift */,
4CC7AAF5297F1A6A00430951 /* EventBody.swift */,
4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */,
@@ -1105,6 +1191,7 @@
4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */,
4CE4F0F729DB7399005914DB /* ThiccDivider.swift */,
4C1A9A2229DDDB8100516EAC /* IconLabel.swift */,
4C8D00C929DF80350036AF10 /* TruncatedText.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -1180,6 +1267,7 @@
3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */,
3A3040F229A91366008A0F29 /* ProfileViewTests.swift */,
3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */,
4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@@ -1216,6 +1304,7 @@
children = (
4CE879572996C45300F758CC /* ZapsView.swift */,
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */,
4CA3FA0F29F593D000FDB3C3 /* ZapTypePicker.swift */,
);
path = Zaps;
sourceTree = "<group>";
@@ -1299,7 +1388,6 @@
);
name = damus;
packageProductDependencies = (
4CE6DF1127F7A2B300C66700 /* Starscream */,
4C649880286E0EE300EAE2B3 /* secp256k1 */,
4C06670328FC7EC500038D2A /* Kingfisher */,
6C7DE41E2955169800E66263 /* Vault */,
@@ -1383,8 +1471,7 @@
"es-419",
"es-ES",
fa,
"fr-CA",
"fr-FR",
fr,
"hu-HU",
id,
"it-IT",
@@ -1406,7 +1493,6 @@
);
mainGroup = 4CE6DEDA27F7A08100C66700;
packageReferences = (
4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */,
4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */,
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */,
6C7DE41D2955169800E66263 /* XCRemoteSwiftPackageReference "Vault" */,
@@ -1431,6 +1517,8 @@
4CE6DEEE27F7A08200C66700 /* Preview Assets.xcassets in Resources */,
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */,
4CE6DEEB27F7A08200C66700 /* Assets.xcassets in Resources */,
4C198DF129F88C6B004C165C /* License.txt in Resources */,
4C198DF029F88C6B004C165C /* Readme.md in Resources */,
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1459,6 +1547,7 @@
4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */,
5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */,
4C363A8A28236B57006E126D /* MentionView.swift in Sources */,
4C8D1A6C29F1DFC200ACDF75 /* FriendIcon.swift in Sources */,
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */,
4C30AC7829A577AB00E2BD5A /* EventCache.swift in Sources */,
4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */,
@@ -1472,6 +1561,7 @@
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */,
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */,
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */,
4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */,
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */,
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
@@ -1489,8 +1579,11 @@
4CC6193A29DC777C006A86D1 /* RelayBootstrap.swift in Sources */,
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */,
4CA5588329F33F5B00DC6A45 /* StringCodable.swift in Sources */,
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
4C8D00C829DF791C0036AF10 /* CompatibleAttribute.swift in Sources */,
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */,
4C198DF229F88C6B004C165C /* BlurHashDecode.swift in Sources */,
F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */,
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */,
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
@@ -1502,7 +1595,7 @@
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */,
4C3D52B6298DB4E6001C5831 /* ZapEvent.swift in Sources */,
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */,
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */,
F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */,
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */,
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */,
@@ -1510,15 +1603,17 @@
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */,
4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */,
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
4C8D1A6F29F31E5000ACDF75 /* FriendsButton.swift in Sources */,
4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */,
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */,
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */,
3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */,
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */,
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */,
4C8D00CA29DF80350036AF10 /* TruncatedText.swift in Sources */,
4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */,
4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */,
4C649844285A952100EAE2B3 /* LocalUserConfig.swift in Sources */,
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
4C30AC7629A5770900E2BD5A /* NotificationItemView.swift in Sources */,
@@ -1530,6 +1625,7 @@
4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */,
4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */,
4CB8838B296F6E1E00DC99E7 /* NIP05Badge.swift in Sources */,
4CA3FA1029F593D000FDB3C3 /* ZapTypePicker.swift in Sources */,
4C3EA66828FF5F9900C48A62 /* hex.c in Sources */,
E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */,
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */,
@@ -1550,10 +1646,10 @@
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */,
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
4C198DF529F88D2E004C165C /* ImageMetadata.swift in Sources */,
4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */,
4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */,
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */,
4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */,
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */,
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
@@ -1568,7 +1664,6 @@
4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */,
4C3EA64928FF597700C48A62 /* bech32.c in Sources */,
4CE879522996B68900F758CC /* RelayType.swift in Sources */,
4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */,
4CE8795B2996C47A00F758CC /* ZapsModel.swift in Sources */,
4C3A1D3729637E0500558C0F /* PreviewCache.swift in Sources */,
4C3EA67528FF7A5A00C48A62 /* take.c in Sources */,
@@ -1595,16 +1690,21 @@
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */,
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
4C198DF829F89323004C165C /* BinaryParser.swift in Sources */,
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */,
4CE1399229F0666100AC6A0B /* ShareActionButton.swift in Sources */,
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */,
4C363A9C282838B9006E126D /* EventRef.swift in Sources */,
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */,
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */,
4C3EA66528FF5F6800C48A62 /* mem.c in Sources */,
4C198DEF29F88C6B004C165C /* BlurHashEncode.swift in Sources */,
4CF0ABE52981EE0C00D66079 /* EULAView.swift in Sources */,
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */,
4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */,
50088DA129E8271A008A1FDF /* WebSocket.swift in Sources */,
4C3EA64128FF553900C48A62 /* hash_u5.c in Sources */,
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */,
4C3EA64F28FF59F200C48A62 /* tal.c in Sources */,
@@ -1621,15 +1721,19 @@
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */,
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */,
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */,
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
6439E014296790CF0020672B /* ProfilePicImageView.swift in Sources */,
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */,
4CDA128C29EB19C40006FA5A /* LocalNotification.swift in Sources */,
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */,
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */,
4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */,
4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */,
4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */,
4CEE2AF9280B2EAC00AB5EEF /* PowView.swift in Sources */,
3165648B295B70D500C64604 /* LinkView.swift in Sources */,
4C8D00CF29E38B950036AF10 /* nostr_bech32.c in Sources */,
4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */,
4C3EA66028FF5E7700C48A62 /* node_id.c in Sources */,
4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */,
@@ -1666,7 +1770,6 @@
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
3A48E23B29D518F000BA313D /* Translations.swift in Sources */,
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
4C06670B28FDE64700038D2A /* damus.c in Sources */,
@@ -1702,6 +1805,7 @@
buildActionMask = 2147483647;
files = (
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */,
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */,
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */,
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */,
@@ -1751,7 +1855,6 @@
3A5C4575296A879E0032D398 /* es-419 */,
3A2B8B0A296A8982009CC16D /* en-US */,
3AEB8005297CCEA900713A25 /* tr-TR */,
3A4F3322297CCFEE004B5F72 /* fr-FR */,
3A185A06297F2C3800F4BDC0 /* lv-LV */,
3A929C22297F2CF80090925E /* it-IT */,
3AB5B86C2986D8A3006599D2 /* de */,
@@ -1773,10 +1876,10 @@
3AD5663229C0DA4B00BF77C5 /* ko */,
3AD14EB529C40F38009D2D9C /* hu-HU */,
3AD14EB829C40F3F009D2D9C /* sv-SE */,
3AD14EBC29C40F47009D2D9C /* fr-CA */,
3A325AC629C9E0B8002BE7ED /* vi */,
3A325AC929C9E0CF002BE7ED /* es-ES */,
3AC59CA929CDDB78007E04A6 /* pt-BR */,
3A821C4029E819D500B4BCA7 /* fr */,
);
name = Localizable.stringsdict;
sourceTree = "<group>";
@@ -1786,7 +1889,6 @@
children = (
3ACB685B297633BC00C46468 /* es-419 */,
3AEB8003297CCEA800713A25 /* tr-TR */,
3A4F3320297CCFEE004B5F72 /* fr-FR */,
3A185A04297F2C3800F4BDC0 /* lv-LV */,
3A929C20297F2CF80090925E /* it-IT */,
3AB5B86A2986D8A3006599D2 /* de */,
@@ -1808,10 +1910,10 @@
3AD5663329C0DA4B00BF77C5 /* ko */,
3AD14EB629C40F38009D2D9C /* hu-HU */,
3AD14EB929C40F3F009D2D9C /* sv-SE */,
3AD14EBB29C40F47009D2D9C /* fr-CA */,
3A325AC529C9E0B8002BE7ED /* vi */,
3A325AC829C9E0CF002BE7ED /* es-ES */,
3AC59CA829CDDB78007E04A6 /* pt-BR */,
3A821C3F29E819D500B4BCA7 /* fr */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
@@ -1821,7 +1923,6 @@
children = (
3ACB685E297633BC00C46468 /* es-419 */,
3AEB8004297CCEA800713A25 /* tr-TR */,
3A4F3321297CCFEE004B5F72 /* fr-FR */,
3A185A05297F2C3800F4BDC0 /* lv-LV */,
3A929C21297F2CF80090925E /* it-IT */,
3AB5B86B2986D8A3006599D2 /* de */,
@@ -1844,10 +1945,10 @@
3AD5663129C0DA4B00BF77C5 /* ko */,
3AD14EB729C40F38009D2D9C /* hu-HU */,
3AD14EBA29C40F3F009D2D9C /* sv-SE */,
3AD14EBD29C40F47009D2D9C /* fr-CA */,
3A325AC429C9E0B8002BE7ED /* vi */,
3A325AC729C9E0CF002BE7ED /* es-ES */,
3AC59CA729CDDB78007E04A6 /* pt-BR */,
3A821C3E29E819D500B4BCA7 /* fr */,
);
name = Localizable.strings;
sourceTree = "<group>";
@@ -1983,7 +2084,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
CURRENT_PROJECT_VERSION = 13;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -2008,9 +2109,12 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.4.1;
MARKETING_VERSION = 1.4.3;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "damus-c/damus-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -2027,7 +2131,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
CURRENT_PROJECT_VERSION = 13;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -2052,9 +2156,12 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.4.1;
MARKETING_VERSION = 1.4.3;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "damus-c/damus-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -2196,14 +2303,6 @@
revision = 40b4b38b3b1c83f7088c76189a742870e0ca06a9;
};
};
4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/daltoniam/Starscream";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 4.0.0;
};
};
6C7DE41D2955169800E66263 /* XCRemoteSwiftPackageReference "Vault" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/SparrowTek/Vault";
@@ -2225,11 +2324,6 @@
package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */;
productName = secp256k1;
};
4CE6DF1127F7A2B300C66700 /* Starscream */ = {
isa = XCSwiftPackageProductDependency;
package = 4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */;
productName = Starscream;
};
6C7DE41E2955169800E66263 /* Vault */ = {
isa = XCSwiftPackageProductDependency;
package = 6C7DE41D2955169800E66263 /* XCRemoteSwiftPackageReference "Vault" */;
@@ -17,15 +17,6 @@
"revision" : "40b4b38b3b1c83f7088c76189a742870e0ca06a9"
}
},
{
"identity" : "starscream",
"kind" : "remoteSourceControl",
"location" : "https://github.com/daltoniam/Starscream",
"state" : {
"revision" : "df8d82047f6654d8e4b655d1b1525c64e1059d21",
"version" : "4.0.4"
}
},
{
"identity" : "vault",
"kind" : "remoteSourceControl",
@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"red" : "0xBE",
"green" : "0x5F",
"blue" : "0x00"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0xED",
"green" : "0x26",
"red" : "0xBF"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,20 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0x05",
"green" : "0xDF",
"red" : "0xFA"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "bitcoin-hashtag.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "bitcoin-hashtag.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "bitcoin-hashtag.svg",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="12.843903"
height="17"
viewBox="0 0 12.843902 16.999999"
version="1.1"
id="svg2"
sodipodi:docname="bitcoin-hashtag.svg"
inkscape:version="1.3-dev (77bc73e, 2022-05-18)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs2" />
<sodipodi:namedview
id="namedview2"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="31.12"
inkscape:cx="4.2577121"
inkscape:cy="7.535347"
inkscape:window-width="1526"
inkscape:window-height="957"
inkscape:window-x="1637"
inkscape:window-y="10"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" />
<g
id="surface1"
transform="matrix(0.94507527,0,0,0.94507527,-4.5943665,-3.2875042)">
<path
style="fill:#f59119;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40637"
d="M 18.388175,10.742602 C 18.668352,8.874761 17.240002,7.8694225 15.295251,7.193703 L 15.927019,4.6611305 14.383304,4.2765743 13.768015,6.7432244 C 13.361486,6.644338 12.943967,6.545453 12.531944,6.4520613 L 13.152726,3.9689298 11.609011,3.584375 10.977241,6.1169476 C 10.642129,6.0400371 10.312509,5.9686201 9.988384,5.886215 L 9.993834,5.880715 7.8623408,5.3478364 7.4558114,6.9959316 c 0,0 1.1426793,0.2636953 1.1207046,0.2801765 0.6207823,0.1538223 0.7361485,0.5658464 0.714174,0.8954659 l -0.7196673,2.889659 c 0.043949,0.01099 0.098885,0.02747 0.1648089,0.04944 L 8.5710227,11.072223 7.5601911,15.11555 c -0.076912,0.192277 -0.26919,0.477948 -0.7031873,0.368073 0.010984,0.02198 -1.1261994,-0.280174 -1.1261994,-0.280174 l -0.7636164,1.76346 2.0106754,0.505417 c 0.3735682,0.0934 0.7361485,0.186784 1.0987301,0.280176 l -0.6427568,2.565534 1.5437155,0.384556 0.6317701,-2.538067 c 0.4230107,0.115364 0.8295407,0.219746 1.2305777,0.318632 l -0.63177,2.527079 1.543716,0.384557 0.637263,-2.560042 c 2.631459,0.499922 4.614667,0.296656 5.444208,-2.082094 0.670226,-1.917284 -0.03296,-3.021507 -1.417363,-3.741176 1.010832,-0.236227 1.768957,-0.895465 1.972221,-2.268877 z m -3.526922,4.944286 c -0.477949,1.917283 -3.702721,0.884477 -4.752008,0.620782 l 0.851514,-3.395075 c 1.043795,0.2582 4.394922,0.774604 3.900494,2.774293 z M 15.3392,10.715134 c -0.433999,1.74698 -3.120394,0.857008 -3.993884,0.642757 l 0.769111,-3.081939 c 0.87349,0.219746 3.675252,0.620784 3.224773,2.439182 z m 0,0"
id="path2" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "coffee.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "coffee.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "coffee.svg",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon -->
<svg
version="1.1"
x="0px"
y="0px"
viewBox="0 0 13.999999 18"
enable-background="new 0 0 1000 1000"
xml:space="preserve"
id="svg4"
sodipodi:docname="coffee.svg"
width="14"
height="18"
inkscape:version="1.3-dev (77bc73e, 2022-05-18)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs4" /><sodipodi:namedview
id="namedview4"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="10.680141"
inkscape:cx="-17.181421"
inkscape:cy="4.07298"
inkscape:window-width="1368"
inkscape:window-height="947"
inkscape:window-x="1764"
inkscape:window-y="58"
inkscape:window-maximized="0"
inkscape:current-layer="svg4" />
<metadata
id="metadata1"> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
<g
id="g4"
transform="matrix(0.01779387,0,0,0.01779387,-1.8340539,0.04465199)"><g
transform="matrix(0.1,0,0,-0.1,0,511)"
id="g3"><path
d="m 4302.6,4870.6 c 149.5,-177.8 240.5,-319.3 347.6,-545.6 119.2,-254.6 169.7,-448.6 183.9,-699.2 22.2,-462.7 -137.4,-778 -539.5,-1060.9 -474.9,-335.4 -685,-739.6 -687,-1315.5 0,-260.7 38.4,-501.1 115.2,-739.6 50.5,-149.5 56.6,-159.6 60.6,-97 18.2,234.4 56.6,476.9 101,626.4 121.2,422.3 305.1,622.4 885.1,959.8 424.3,248.5 575.9,487 575.9,905.3 -2,501.1 -359.7,1295.3 -798.2,1768.1 -82.8,88.9 -198,202.1 -256.6,250.6 l -105.1,86.9 z"
id="path1"
style="fill:#be5f00;fill-opacity:1" /><path
d="m 5981.8,3577.3 c 272.8,-369.8 309.2,-846.7 90.9,-1192.2 -147.5,-232.4 -373.8,-406.2 -822.4,-638.5 -592,-303.1 -854.7,-683 -854.7,-1232.6 0,-276.8 14.2,-343.5 72.7,-343.5 38.4,0 48.5,16.2 68.7,111.1 34.4,167.7 135.4,349.6 262.7,476.9 147.5,145.5 349.6,244.5 838.6,412.2 503.2,171.8 725.4,280.9 846.7,416.3 210.1,232.4 276.8,535.5 202.1,903.2 -76.8,373.8 -216.2,618.3 -537.5,943.7 -155.7,155.6 -214.3,206.1 -167.8,143.4 z"
id="path2"
style="fill:#be5f00;fill-opacity:1" /><path
d="M 2748.7,592.8 C 2158.7,507.9 1732.3,352.3 1542.4,156.3 1415.1,25 1409,-11.4 1427.2,-445.8 c 34.3,-832.5 181.9,-1729.7 462.7,-2829 153.6,-600.1 309.2,-1113.4 351.6,-1159.9 56.6,-62.6 272.8,-157.6 476.9,-210.1 668.8,-173.8 2172.2,-196 2960.3,-42.4 357.7,68.7 604.2,163.7 731.5,278.9 68.7,60.6 84.9,92.9 107.1,198 64.7,335.4 56.6,319.3 131.3,319.3 107.1,0 438.5,92.9 602.2,167.7 220.3,103.1 363.7,202.1 567.8,398.1 470.8,452.6 759.8,1149.8 761.8,1840.8 0,398.1 -72.7,614.3 -274.8,818.4 -220.3,222.3 -466.8,309.2 -937.6,325.4 l -309.2,12.1 14.2,218.2 14.2,218.2 -56.6,52.5 C 6866.9,314 6274.9,499.9 5696.9,582.7 L 5614,594.8 5533.2,457.4 5450.4,322 5565.6,305.8 c 588,-84.9 868.9,-159.6 978,-256.6 56.6,-50.5 60.6,-62.6 44.5,-113.2 -80.8,-226.3 -1000.2,-371.8 -2329.8,-371.8 -1220.5,0 -2097.5,123.3 -2295.5,321.3 -52.5,54.5 -56.6,66.7 -36.4,111.1 48.5,107.1 341.5,206.1 822.4,276.8 143.5,20.2 301.1,38.4 349.6,38.4 46.5,0 84.9,4 84.9,8.1 0,8.1 -175.8,295 -185.9,305.1 -4.2,2.1 -115.3,-12 -248.7,-32.2 z m 4797.1,-1614.5 c 153.6,-26.3 272.8,-82.8 317.2,-153.6 56.6,-84.9 60.6,-400.1 8.1,-652.7 -111.1,-545.6 -404.1,-996.2 -788.1,-1220.5 -139.4,-80.8 -333.4,-151.5 -355.6,-129.3 -8.1,8.1 18.2,216.2 58.6,462.7 78.8,482.9 159.6,1073 202.1,1450.8 l 24.2,232.4 70.7,12.1 c 115.3,20.3 325.4,20.3 462.8,-1.9 z"
id="path3"
style="fill:#be5f00;fill-opacity:1" /></g></g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "nostr-hashtag.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "nostr-hashtag.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "nostr-hashtag.svg",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
version="1.0"
width="18pt"
height="18.199053pt"
viewBox="0 0 18 18.199053"
preserveAspectRatio="xMidYMid"
id="svg1"
sodipodi:docname="nostr-hashtag.svg"
inkscape:version="1.3-dev (77bc73e, 2022-05-18)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="pt"
showgrid="false"
inkscape:zoom="5.4017383"
inkscape:cx="50.354161"
inkscape:cy="-13.514168"
inkscape:window-width="1497"
inkscape:window-height="866"
inkscape:window-x="1747"
inkscape:window-y="96"
inkscape:window-maximized="0"
inkscape:current-layer="svg1" />
<metadata
id="metadata1">
Created by potrace 1.15, written by Peter Selinger 2001-2017
</metadata>
<g
transform="matrix(0.00138509,0,0,-0.00138509,0.3,17.927982)"
fill="#000000"
stroke="none"
id="g1">
<path
d="m 11315,12756 c -49,-23 -135,-71 -190,-106 -128,-81 -170,-100 -222,-100 -37,0 -43,-3 -43,-22 0,-11 12,-36 26,-55 l 26,-34 -20,-37 c -128,-248 -171,-359 -212,-547 -56,-262 -43,-645 36,-1045 48,-244 104,-451 234,-865 158,-503 280,-942 320,-1145 27,-140 60,-448 60,-559 0,-164 -39,-366 -92,-478 -57,-123 -176,-222 -286,-239 -49,-8 -50,-23 -5,-38 18,-6 34,-14 38,-18 13,-13 -25,-66 -97,-134 -121,-115 -267,-174 -432,-174 -200,0 -327,87 -732,507 -233,240 -541,529 -604,567 -31,18 -32,18 -27,0 3,-11 9,-36 13,-56 8,-49 -5,-49 -65,1 -75,62 -168,118 -250,150 -134,51 -214,63 -481,70 l -245,6 -65,41 c -100,63 -125,68 -295,66 -186,-3 -262,7 -484,64 -279,71 -341,67 -780,-61 -334,-97 -442,-118 -761,-150 -200,-19 -365,-19 -496,1 -58,9 -107,14 -111,11 -3,-4 6,-22 21,-41 l 27,-35 -23,-5 c -13,-3 -61,-8 -106,-11 -113,-9 -157,-26 -219,-87 -50,-49 -54,-51 -140,-64 -60,-10 -108,-25 -153,-48 -304,-157 -331,-166 -471,-166 -48,0 -120,7 -160,15 -40,9 -74,13 -78,10 -3,-3 0,-20 7,-37 6,-18 10,-34 7,-36 -2,-3 -24,0 -47,7 -24,6 -103,16 -176,23 -163,15 -350,1 -521,-40 l -114,-26 51,-23 c 53,-23 76,-50 66,-77 -3,-8 -51,-38 -105,-66 -251,-129 -515,-384 -727,-703 -110,-166 -208,-325 -217,-355 -6,-21 -5,-22 34,-16 36,6 41,4 41,-12 0,-11 -15,-45 -34,-76 -46,-75 -76,-161 -127,-359 -78,-307 -137,-444 -243,-569 -38,-44 -51,-67 -43,-72 7,-4 40,-8 75,-8 37,0 62,-4 62,-11 0,-6 -12,-26 -27,-45 -32,-43 -24,-53 48,-60 108,-11 103,-6 95,-82 -4,-37 -11,-128 -15,-202 -5,-74 -16,-164 -26,-200 -22,-85 -75,-190 -136,-269 -27,-35 -49,-67 -49,-72 0,-4 17,-11 38,-14 20,-3 49,-8 64,-10 64,-10 285,31 408,76 101,37 249,117 348,188 54,38 110,75 126,80 15,6 70,11 123,11 267,0 582,98 766,239 34,26 72,53 83,59 25,13 31,8 58,-46 21,-40 38,-32 57,26 30,93 97,200 141,223 25,14 38,3 98,-86 27,-38 73,-95 104,-125 31,-30 56,-63 56,-75 0,-11 -20,-60 -45,-110 -42,-84 -81,-202 -70,-213 3,-3 27,16 54,41 50,47 81,59 81,34 0,-8 -10,-49 -21,-93 -26,-97 -32,-275 -10,-313 l 14,-25 36,41 c 42,48 118,88 251,131 102,33 185,47 345,57 55,4 159,13 230,21 292,34 285,35 368,-56 66,-72 122,-115 170,-130 20,-6 37,-15 37,-19 0,-12 -22,-35 -75,-77 -27,-21 -151,-133 -275,-248 -424,-394 -462,-415 -864,-486 -65,-12 -145,-29 -177,-39 -191,-60 -348,-213 -554,-541 -103,-165 -162,-241 -292,-383 -176,-190 -332,-333 -778,-711 -497,-422 -750,-737 -895,-1113 -64,-165 -104,-203 -216,-203 -38,0 -94,7 -124,15 -74,20 -244,20 -301,0 -27,-10 -61,-33 -87,-62 -41,-45 -43,-46 -89,-39 -25,3 -67,18 -92,33 C 457,781 369,812 175,813 49,814 0,786 0,713 0,669 31,628 122,557 204,491 254,434 469,153 527,77 549,57 597,33 652,7 662,5 780,6 c 69,0 150,6 180,14 30,7 132,32 225,56 281,72 308,76 511,84 181,6 195,8 247,34 76,37 137,107 231,267 230,392 287,481 394,626 219,293 469,581 756,869 284,284 368,342 641,439 164,59 295,123 410,199 101,68 257,219 295,286 14,24 56,76 93,116 100,106 177,142 437,203 165,40 229,62 390,133 191,85 220,94 460,143 118,24 239,49 267,56 29,7 56,10 59,6 4,-3 15,-50 26,-104 14,-73 21,-159 25,-331 6,-265 -3,-385 -47,-605 -50,-247 -105,-393 -222,-592 -59,-98 -74,-167 -58,-256 23,-126 61,-177 174,-232 95,-47 169,-52 357,-28 125,16 244,22 505,26 667,12 1011,-35 1799,-241 277,-73 308,-86 376,-161 72,-79 113,-170 175,-388 19,-66 42,-130 50,-143 42,-63 155,-112 261,-112 109,0 124,57 54,206 -48,102 -53,149 -17,165 31,15 73,-5 102,-48 12,-18 45,-87 75,-153 57,-132 106,-197 173,-233 35,-18 56,-21 136,-20 73,2 110,-3 159,-19 89,-29 333,-32 403,-5 114,44 140,145 60,231 -23,24 -112,72 -272,143 -71,32 -199,124 -250,179 -71,78 -111,140 -224,343 -132,236 -172,297 -262,391 -58,61 -94,89 -148,115 -113,56 -85,53 -866,85 -223,9 -616,36 -656,45 -11,2 -86,12 -165,20 -179,20 -595,95 -712,130 -145,42 -319,139 -371,208 -32,42 -53,102 -67,187 -22,144 48,316 261,640 125,190 193,317 230,428 31,95 60,282 60,392 0,76 4,102 16,114 20,20 25,18 192,-102 73,-52 168,-114 210,-137 83,-47 132,-82 132,-95 0,-4 -14,-17 -32,-28 l -32,-21 29,-11 c 108,-41 305,-52 707,-39 186,6 345,9 353,5 8,-3 15,-15 15,-27 0,-20 3,-21 54,-15 110,13 266,81 426,187 89,58 99,63 170,69 161,14 229,78 523,495 88,124 143,185 169,185 2,0 2,-27 0,-60 -2,-33 0,-60 5,-60 23,0 140,145 212,263 117,192 165,317 271,712 106,394 111,410 135,410 15,0 22,-10 29,-40 13,-53 12,-52 42,-24 40,36 103,133 154,234 25,50 50,90 55,90 6,0 11,-17 13,-37 2,-21 7,-38 12,-38 19,0 93,101 139,190 29,56 87,206 140,363 50,147 114,318 142,380 132,288 199,526 246,867 25,190 25,756 0,975 -23,195 -53,379 -78,485 -46,194 -152,515 -233,706 -130,306 -218,564 -260,764 -48,228 -48,572 1,770 31,128 82,204 166,246 64,33 118,83 187,174 120,158 184,199 503,321 250,96 347,153 347,204 0,41 -42,57 -174,68 -112,9 -385,70 -439,99 -10,5 -27,29 -37,54 -23,53 -99,137 -159,176 -56,35 -72,36 -40,1 24,-26 23,-26 -13,3 -21,16 -42,41 -47,56 -9,26 -14,28 -60,28 -50,0 -101,16 -101,32 0,4 -42,8 -92,7 -88,0 -98,-3 -183,-43 z m 550,-126 c 10,-11 16,-20 13,-20 -3,0 -13,9 -23,20 -10,11 -16,20 -13,20 3,0 13,-9 23,-20 z"
id="path1"
style="fill:#cc43c5;fill-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.2 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "plebchain.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "plebchain.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "plebchain.svg",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
viewBox="0 0 21.315096 18"
width="21.315096"
height="18"
version="1.1"
id="svg21"
sodipodi:docname="plebchain.svg"
inkscape:version="1.3-dev (77bc73e, 2022-05-18)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs21" />
<sodipodi:namedview
id="namedview21"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="19.666667"
inkscape:cx="12.762712"
inkscape:cy="12.991525"
inkscape:window-width="1418"
inkscape:window-height="883"
inkscape:window-x="1745"
inkscape:window-y="10"
inkscape:window-maximized="0"
inkscape:current-layer="svg21" />
<path
d="M 18.625339,11.886754 C 17.668676,9.87076 15.553749,8.748431 12.298294,7.531368 12.258627,7.5164347 11.514296,7.2747022 10.649566,6.9942364 9.6532356,6.6713042 8.5682391,6.7520372 7.6316422,7.220569 L 5.7999816,8.136166 13.616623,17.9338 h 4.316652 l 0.703264,-1.662727 c 0.593598,-1.401862 0.641198,-3.009057 -0.0112,-4.384319 z"
id="path5"
style="fill:#bf26ed;fill-opacity:1;stroke-width:0.466665" />
<path
d="M 16.627546,5.1089093 C 16.710146,4.5484447 16.797412,3.95578 16.838479,3.6790475 17.083011,2.0219197 15.937348,0.48005806 14.28022,0.23552546 12.623092,-0.00900704 11.081231,1.1366559 10.836698,2.7937838 c -0.0406,0.2762657 -0.128333,0.8689305 -0.210932,1.4298619 -0.282799,0.058333 -0.513332,0.2851324 -0.558132,0.5870648 -0.0658,0.441465 0.084,1.0145298 0.370999,1.2072625 0.09707,1.4303286 1.118596,2.778524 2.547992,2.989457 1.429395,0.210933 2.796257,-0.785398 3.302589,-2.1265932 0.330399,-0.1021996 0.639331,-0.6071313 0.703731,-1.0490632 0.0434,-0.302399 -0.111533,-0.5856647 -0.365399,-0.7228643 z"
id="path10"
style="fill:#bf26ed;fill-opacity:1;stroke-width:0.466665" />
<path
d="m 21.193864,14.872477 c -0.0392,-0.501665 -0.310799,-0.956663 -0.684131,-1.294529 l -1.900727,-1.723394 c 0,0 0.172666,0.346732 0.332732,1.276796 L 15.53415,11.550288 c -0.878731,-0.405999 -1.835861,-0.616465 -2.804191,-0.616465 h -0.807331 c -0.87733,0 -1.681394,-0.553464 -1.9338602,-1.393928 C 9.8926348,9.220696 9.819835,8.832897 9.7904351,8.363432 V 8.362965 C 10.503966,7.7651672 10.944031,6.8285703 10.968765,5.8700401 11.247364,5.6651742 11.372897,5.0865094 11.28843,4.648311 11.230564,4.3482453 10.990698,4.1317127 10.706032,4.0850462 10.599632,3.5283147 10.487633,2.93985 10.435366,2.6654509 10.121301,1.0199896 8.5327726,-0.05940684 6.8873113,0.25419216 5.2418501,0.56779106 4.1624536,2.1567859 4.4760526,3.8022471 4.5283191,4.0766462 4.6407854,4.665111 4.7467184,5.2218423 4.4989192,5.3697751 4.355653,5.6595742 4.4125861,5.9596399 4.4956525,6.3983051 4.8255848,6.8901701 5.159717,6.9779032 5.2950499,7.2952355 5.4798493,7.5892345 5.7015152,7.8510336 L 5.7999816,8.136166 5.2171168,11.699621 3.2925898,12.11822 C 3.041524,12.177953 2.8109914,12.278752 2.6061254,12.411752 1.8342613,12.912484 1.5458622,13.865414 1.7731282,14.725478 1.8309946,14.943877 2.8833245,17.9338 2.8833245,17.9338 H 15.133284 l 0.408332,-1.691661 3.480855,0.605265 c 1.184397,0.211399 2.267993,-0.748064 2.171393,-1.974927 z"
id="path16"
style="fill:#f59119;fill-opacity:1;stroke-width:0.466665" />
<path
d="m 12.589026,15.06801 -2.251659,-0.776531 c 0,0 0.96693,-0.1106 0.95853,-0.625798 -4.67e-4,-0.03687 -0.01213,-0.240332 -0.307999,-0.210932 -0.0812,0.0079 -0.734064,0.07327 -1.1927962,0.118999 -0.3028657,0.03033 -0.6015314,0.09007 -0.8927304,0.178733 l -0.1591328,0.04853 -3.5256551,-2.101393 c 0,0 0,0 0.3495322,-0.763464 C 6.1779803,9.600561 5.8004482,8.136166 5.8004482,8.136166 L 1.324663,10.373825 C 0.63539857,10.718224 0.2,11.422889 0.2,12.193819 c 0,0.939397 0.64306455,1.756528 1.5558615,1.977727 l 6.4357789,1.371062 2.5834586,0.955731 c 0.167999,0.0532 0.329932,0.0602 0.517065,0.0033 0.187599,-0.05693 0.246399,-0.09847 0.337398,-0.165199 0.2464,-0.180133 0.706065,-0.536665 1.037864,-0.795664 0.170799,-0.133933 0.125533,-0.402266 -0.0784,-0.472732 z"
id="path21"
style="fill:#bf26ed;fill-opacity:1;stroke-width:0.466665" />
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "zapathon.svg",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "zapathon.svg",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "zapathon.svg",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="11.106925"
height="18"
viewBox="0 0 2.9387073 4.7624999"
version="1.1"
id="svg1"
inkscape:version="1.3-dev (77bc73e, 2022-05-18)"
sodipodi:docname="zapathon.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="16"
inkscape:cx="16.96875"
inkscape:cy="9.96875"
inkscape:window-width="1406"
inkscape:window-height="767"
inkscape:window-x="1741"
inkscape:window-y="214"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-10.993855,-8.058313)">
<g
transform="matrix(0.01604881,0,0,-0.01604881,10.573102,13.422443)"
id="g10"
inkscape:label="Bolt"
style="display:inline">
<path
id="path14"
style="fill:#c98f19;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1"
d="m 94.833712,155.13322 -8.129016,18.00903 -55.167069,-3.59001 18.868958,-24.19503 44.825648,2.77303 z m 83.691438,80.98476 -8.89814,-11.93085 -11.755,-16.519 -1.505,-2.114 -43.191,-60.691 L 63.081111,74.14633 79.705774,38.853232 C 121.34411,95.870625 162.2814,153.39008 203.27601,210.87013 Z m -22.57414,11.14115 12.32197,61.15287 -21.22997,22.33399 -17.432,-69.53486 -7.0256,-30.70796 27.52164,-9.9823 5.84396,26.73826"
sodipodi:nodetypes="cccccccccccccccccccccc" />
<path
id="path16"
style="fill:#fadf05;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1"
d="m 178.78514,236.43412 -5.679,-0.377 -8.065,-0.533 -5.9,-0.389 -33.394,-2.212 6.162,29.388 6.223,29.667 8.95029,38.92409 -54.426393,-75.90909 -61.5969,-86.55 44.1332,2.921 8.9137,0.589 -6.1723,-29.396 -0.9867,-4.71 -1.5106,-7.209 -11.7273,-55.917704 16.3074,22.9175 37.169603,52.234204 43.189,60.693 1.505,2.113 16.907,23.756 h -0.002"
sodipodi:nodetypes="cccccccccccccccccccccc" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+3
View File
@@ -12,11 +12,14 @@ class DamusColors {
static let adaptableGrey = Color("DamusAdaptableGrey")
static let white = Color("DamusWhite")
static let black = Color("DamusBlack")
static let brown = Color("DamusBrown")
static let yellow = Color("DamusYellow")
static let lightGrey = Color("DamusLightGrey")
static let mediumGrey = Color("DamusMediumGrey")
static let darkGrey = Color("DamusDarkGrey")
static let green = Color("DamusGreen")
static let purple = Color("DamusPurple")
static let deepPurple = Color("DamusDeepPurple")
static let blue = Color("DamusBlue")
}
+68 -31
View File
@@ -37,28 +37,53 @@ enum ImageShape {
case landscape
case portrait
case unknown
static func determine_image_shape(_ size: CGSize) -> ImageShape {
guard size.height > 0 else {
return .unknown
}
let imageRatio = size.width / size.height
switch imageRatio {
case 1.0: return .square
case ..<1.0: return .portrait
case 1.0...: return .landscape
default: return .unknown
}
}
}
// Try either calculated imagefill from the real image or from metadata hints in tags
func lookup_imgmeta_size_hint(events: EventCache, url: URL?) -> CGSize? {
guard let url,
let meta = events.lookup_img_metadata(url: url),
let img_size = meta.meta.dim?.size else {
return nil
}
return img_size
}
struct ImageCarousel: View {
var urls: [URL]
let evid: String
let previews: PreviewCache
let state: DamusState
@State private var open_sheet: Bool = false
@State private var current_url: URL? = nil
@State private var image_fill: ImageFill? = nil
@State private var fillHeight: CGFloat = 350
@State private var maxHeight: CGFloat = UIScreen.main.bounds.height * 0.85
init(previews: PreviewCache, evid: String, urls: [URL]) {
let fillHeight: CGFloat = 350
let maxHeight: CGFloat = UIScreen.main.bounds.height * 1.2
init(state: DamusState, evid: String, urls: [URL]) {
_open_sheet = State(initialValue: false)
_current_url = State(initialValue: nil)
_image_fill = State(initialValue: previews.lookup_image_meta(evid))
_image_fill = State(initialValue: state.previews.lookup_image_meta(evid))
self.urls = urls
self.evid = evid
self.previews = previews
self.state = state
}
var filling: Bool {
@@ -66,7 +91,29 @@ struct ImageCarousel: View {
}
var height: CGFloat {
image_fill?.height ?? 0
image_fill?.height ?? fillHeight
}
func Placeholder(url: URL, geo_size: CGSize) -> some View {
Group {
if let meta = state.events.lookup_img_metadata(url: url),
case .processed(let blurhash) = meta.state {
Image(uiImage: blurhash)
.resizable()
.frame(width: geo_size.width * UIScreen.main.scale, height: self.height * UIScreen.main.scale)
} else {
EmptyView()
}
}
.onAppear {
if self.image_fill == nil,
let meta = state.events.lookup_img_metadata(url: url),
let size = meta.meta.dim?.size
{
let fill = ImageFill.calculate_image_fill(geo_size: geo_size, img_size: size, maxHeight: maxHeight, fillHeight: fillHeight)
self.image_fill = fill
}
}
}
var body: some View {
@@ -79,15 +126,19 @@ struct ImageCarousel: View {
KFAnimatedImage(url)
.callbackQueue(.dispatch(.global(qos:.background)))
.backgroundDecode(true)
.imageContext(.note)
.imageContext(.note, disable_animation: state.settings.disable_animation)
.image_fade(duration: 0.25)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in
previews.cache_image_meta(evid: evid, image_fill: fill)
state.previews.cache_image_meta(evid: evid, image_fill: fill)
image_fill = fill
}
.background {
Placeholder(url: url, geo_size: geo.size)
}
.aspectRatio(contentMode: filling ? .fill : .fit)
.tabItem {
Text(url.absoluteString)
@@ -98,9 +149,9 @@ struct ImageCarousel: View {
}
}
.fullScreenCover(isPresented: $open_sheet) {
ImageView(urls: urls)
ImageView(urls: urls, disable_animation: state.settings.disable_animation)
}
.frame(height: height)
.frame(height: self.height)
.onTapGesture {
open_sheet = true
}
@@ -118,10 +169,7 @@ extension KFOptionSetter {
let modifier = AnyImageModifier { image -> KFCrossPlatformImage in
let img_size = image.size
let geo_size = size
let fill = ImageFill.calculate_image_fill(geo_size: geo_size,
img_size: img_size,
maxHeight: max,
fillHeight: fill)
let fill = ImageFill.calculate_image_fill(geo_size: geo_size, img_size: img_size, maxHeight: max, fillHeight: fill)
DispatchQueue.main.async { [block, fill] in
try? block(fill)
}
@@ -137,25 +185,14 @@ public struct ImageFill {
let filling: Bool?
let height: CGFloat
static func determine_image_shape(_ size: CGSize) -> ImageShape {
guard size.height > 0 else {
return .unknown
}
let imageRatio = size.width / size.height
switch imageRatio {
case 1.0: return .square
case ..<1.0: return .portrait
case 1.0...: return .landscape
default: return .unknown
}
}
static func calculate_image_fill(geo_size: CGSize, img_size: CGSize, maxHeight: CGFloat, fillHeight: CGFloat) -> ImageFill {
let shape = determine_image_shape(img_size)
let shape = ImageShape.determine_image_shape(img_size)
let xfactor = geo_size.width / img_size.width
let scaled = img_size.height * xfactor
//print("calc_img_fill \(img_size.width)x\(img_size.height) xfactor:\(xfactor) scaled:\(scaled)")
// calculate scaled image height
// set scale factor and constrain images to minimum 150
// and animations to scaled factor for dynamic size adjustment
@@ -172,7 +209,7 @@ public struct ImageFill {
struct ImageCarousel_Previews: PreviewProvider {
static var previews: some View {
ImageCarousel(previews: test_damus_state().previews, evid: "evid", urls: [URL(string: "https://jb55.com/red-me.jpg")!,URL(string: "https://jb55.com/red-me.jpg")!])
ImageCarousel(state: test_damus_state(), evid: "evid", urls: [URL(string: "https://jb55.com/red-me.jpg")!,URL(string: "https://jb55.com/red-me.jpg")!])
}
}
+6 -4
View File
@@ -14,6 +14,7 @@ struct InvoiceView: View {
let invoice: Invoice
@State var showing_select_wallet: Bool = false
@State var copied = false
let settings: UserSettingsStore
var CopyButton: some View {
Button {
@@ -36,10 +37,10 @@ struct InvoiceView: View {
var PayButton: some View {
Button {
if should_show_wallet_selector(our_pubkey) {
if settings.show_wallet_selector {
showing_select_wallet = true
} else {
open_with_wallet(wallet: get_default_wallet(our_pubkey).model, invoice: invoice.string)
open_with_wallet(wallet: settings.default_wallet.model, invoice: invoice.string)
}
} label: {
RoundedRectangle(cornerRadius: 20, style: .circular)
@@ -80,7 +81,7 @@ struct InvoiceView: View {
.padding(30)
}
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: our_pubkey, invoice: invoice.string)
SelectWalletView(default_wallet: settings.default_wallet, showingSelectWallet: $showing_select_wallet, our_pubkey: our_pubkey, invoice: invoice.string)
}
}
}
@@ -111,7 +112,8 @@ let test_invoice = Invoice(description: .description("this is a description"), a
struct InvoiceView_Previews: PreviewProvider {
static var previews: some View {
InvoiceView(our_pubkey: "", invoice: test_invoice)
InvoiceView(our_pubkey: "", invoice: test_invoice, settings: test_damus_state().settings)
.frame(width: 300, height: 200)
}
}
+3 -2
View File
@@ -10,6 +10,7 @@ import SwiftUI
struct InvoicesView: View {
let our_pubkey: String
var invoices: [Invoice]
let settings: UserSettingsStore
@State var open_sheet: Bool = false
@State var current_invoice: Invoice? = nil
@@ -17,7 +18,7 @@ struct InvoicesView: View {
var body: some View {
TabView {
ForEach(invoices, id: \.string) { invoice in
InvoiceView(our_pubkey: our_pubkey, invoice: invoice)
InvoiceView(our_pubkey: our_pubkey, invoice: invoice, settings: settings)
.tabItem {
Text(invoice.string)
}
@@ -31,7 +32,7 @@ struct InvoicesView: View {
struct InvoicesView_Previews: PreviewProvider {
static var previews: some View {
InvoicesView(our_pubkey: "", invoices: [Invoice.init(description: .description("description"), amount: .specific(10000), string: "invstr", expiry: 100000, payment_hash: Data(), created_at: 1000000)])
InvoicesView(our_pubkey: "", invoices: [Invoice.init(description: .description("description"), amount: .specific(10000), string: "invstr", expiry: 100000, payment_hash: Data(), created_at: 1000000)], settings: test_damus_state().settings)
.frame(width: 300)
}
}
+2 -2
View File
@@ -35,10 +35,10 @@ struct NIP05Badge: View {
.mask(Image(systemName: "checkmark.seal.fill")
.resizable()
).frame(width: 14, height: 14)
} else {
} else if show_domain {
Image(systemName: "checkmark.seal.fill")
.font(.footnote)
.foregroundColor(.gray)
.nip05_colorized(gradient: nip05_color)
}
}
}
+133 -83
View File
@@ -8,119 +8,169 @@
import SwiftUI
import NaturalLanguage
struct Translated: Equatable {
let artifacts: NoteArtifacts
let language: String
}
enum TranslateStatus: Equatable {
case havent_tried
case trying
case translating
case translated(Translated)
case not_needed
}
struct TranslateView: View {
let damus_state: DamusState
let event: NostrEvent
let size: EventViewKind
let currentLanguage: String
@State var translated: TranslateStatus
@State var checkingTranslationStatus: Bool = false
@State var translatable: Bool = true
@State var noteLanguage: String?
@State var show_translated_note: Bool
@State var translated_artifacts: NoteArtifacts?
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind) {
self.damus_state = damus_state
self.event = event
self.size = size
self._noteLanguage = State(initialValue: damus_state.translations.detectLanguage(event, state: damus_state))
if let translationWithLanguage = damus_state.translations.cachedTranslation(event) {
self._noteLanguage = State(initialValue: translationWithLanguage.language)
let translatedBlocks = event.get_blocks(content: translationWithLanguage.translation)
self._translated_artifacts = State.init(initialValue: render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey))
if #available(iOS 16, *) {
self.currentLanguage = Locale.current.language.languageCode?.identifier ?? "en"
} else {
self._translated_artifacts = State(initialValue: nil)
self.currentLanguage = Locale.current.languageCode ?? "en"
}
self._show_translated_note = State(initialValue: damus_state.settings.auto_translate)
if damus_state.pubkey == event.pubkey && damus_state.is_privkey_user {
// Do not translate self-authored notes if logged in with a private key
// as we can assume the user can understand their own notes.
// The detected language prediction could be incorrect and not in the list of preferred languages.
// Offering a translation in this case is definitely incorrect so let's avoid it altogether.
self._translated = State(initialValue: .not_needed)
} else if let cached = damus_state.events.lookup_translated_artifacts(evid: event.id) {
self._translated = State(initialValue: cached)
} else {
let initval: TranslateStatus = self.damus_state.settings.auto_translate ? .trying : .havent_tried
self._translated = State(initialValue: initval)
}
}
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
var TranslateButton: some View {
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
show_translated_note = true
processTranslation()
self.translated = .trying
}
.translate_button_style()
}
func processTranslation() {
guard noteLanguage != nil && !checkingTranslationStatus && translatable else {
func TranslatedView(lang: String?, artifacts: NoteArtifacts) -> some View {
return VStack(alignment: .leading) {
Text(String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang ?? "ja"))
.foregroundColor(.gray)
.font(.footnote)
.padding([.top, .bottom], 10)
if self.size == .selected {
SelectableText(attributedString: artifacts.content.attributed, size: self.size)
} else {
artifacts.content.text
.font(eventviewsize_to_font(self.size))
}
}
}
func failed_attempt() {
DispatchQueue.main.async {
self.translated = .not_needed
damus_state.events.store_translation_artifacts(evid: event.id, translated: .not_needed)
}
}
func attempt_translation() async {
guard case .trying = translated else {
return
}
guard damus_state.settings.can_translate(damus_state.pubkey) else {
return
}
let note_lang = event.note_language(damus_state.keypair.privkey) ?? currentLanguage
// Don't translate if its in our preferred languages
guard !preferredLanguages.contains(note_lang) else {
failed_attempt()
return
}
DispatchQueue.main.async {
self.translated = .translating
}
// If the note language is different from our preferred languages, send a translation request.
let translator = Translator(damus_state.settings)
let originalContent = event.get_content(damus_state.keypair.privkey)
let translated_note = try? await translator.translate(originalContent, from: note_lang, to: currentLanguage)
guard let translated_note else {
// if its the same, give up and don't retry
failed_attempt()
return
}
guard originalContent != translated_note else {
// if its the same, give up and don't retry
failed_attempt()
return
}
checkingTranslationStatus = true
show_translated_note = true
Task {
let translationWithLanguage = await damus_state.translations.translate(event, state: damus_state)
DispatchQueue.main.async {
guard translationWithLanguage != nil else {
noteLanguage = damus_state.translations.targetLanguage
checkingTranslationStatus = false
show_translated_note = false
translatable = false
return
}
noteLanguage = translationWithLanguage!.language
// Render translated note.
let translatedBlocks = event.get_blocks(content: translationWithLanguage!.translation)
translated_artifacts = render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
translatable = true
checkingTranslationStatus = false
}
}
}
func Translated(lang: String, artifacts: NoteArtifacts) -> some View {
return Group {
Button(String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang)) {
show_translated_note = false
}
.translate_button_style()
SelectableText(attributedString: artifacts.content, size: self.size)
}
}
func MainContent(note_lang: String) -> some View {
return Group {
if translatable {
let languageName = Locale.current.localizedString(forLanguageCode: note_lang)
if let languageName, let translated_artifacts, show_translated_note {
Translated(lang: languageName, artifacts: translated_artifacts)
} else if !damus_state.settings.auto_translate {
TranslateButton
} else {
EmptyView()
}
} else {
EmptyView()
}
// Render translated note
let translated_blocks = event.get_blocks(content: translated_note)
let artifacts = render_blocks(blocks: translated_blocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
// and cache it
DispatchQueue.main.async {
self.translated = .translated(Translated(artifacts: artifacts, language: note_lang))
damus_state.events.store_translation_artifacts(evid: event.id, translated: self.translated)
}
}
var body: some View {
Group {
if let note_lang = noteLanguage, note_lang != damus_state.translations.targetLanguage {
MainContent(note_lang: note_lang)
.task {
if show_translated_note {
processTranslation()
}
}
} else {
switch translated {
case .havent_tried:
if damus_state.settings.auto_translate {
Text("")
} else {
TranslateButton
}
case .trying:
Text("")
case .translating:
Text("Translating...", comment: "Text to display when waiting for the translation of a note to finish processing before showing it.")
.foregroundColor(.gray)
.font(.footnote)
.padding([.top, .bottom], 10)
case .translated(let translated):
let languageName = Locale.current.localizedString(forLanguageCode: translated.language)
TranslatedView(lang: languageName, artifacts: translated.artifacts)
case .not_needed:
Text("")
}
}
.onChange(of: translated) { val in
guard case .trying = translated else {
return
}
Task {
await attempt_translation()
}
}
.task {
await attempt_translation()
}
}
}
+53
View File
@@ -0,0 +1,53 @@
//
// TruncatedText.swift
// damus
//
// Created by William Casarin on 2023-04-06.
//
import SwiftUI
struct TruncatedText: View {
let text: CompatibleText
let maxChars: Int = 280
var body: some View {
let truncatedAttributedString: AttributedString? = getTruncatedString()
if let truncatedAttributedString {
Text(truncatedAttributedString)
.fixedSize(horizontal: false, vertical: true)
} else {
text.text
.fixedSize(horizontal: false, vertical: true)
}
if truncatedAttributedString != nil {
Spacer()
Button(NSLocalizedString("Show more", comment: "Button to show entire note.")) { }
.allowsHitTesting(false)
}
}
func getTruncatedString() -> AttributedString? {
let nsAttributedString = NSAttributedString(text.attributed)
if nsAttributedString.length < maxChars { return nil }
let range = NSRange(location: 0, length: maxChars)
let truncatedAttributedString = nsAttributedString.attributedSubstring(from: range)
return AttributedString(truncatedAttributedString) + "..."
}
}
struct TruncatedText_Previews: PreviewProvider {
static var previews: some View {
VStack(spacing: 100) {
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven"))
.frame(width: 200, height: 200)
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour"))
.frame(width: 200, height: 200)
}
}
}
+37 -13
View File
@@ -7,27 +7,51 @@
import SwiftUI
struct UserViewRow: View {
let damus_state: DamusState
let pubkey: String
@State var navigating: Bool = false
var body: some View {
let dest = ProfileView(damus_state: damus_state, pubkey: pubkey)
UserView(damus_state: damus_state, pubkey: pubkey)
.contentShape(Rectangle())
.background(
NavigationLink(destination: dest, isActive: $navigating) {
EmptyView()
}
)
.onTapGesture {
navigating = true
}
}
}
struct UserView: View {
let damus_state: DamusState
let pubkey: String
var body: some View {
NavigationLink(destination: ProfileView(damus_state: damus_state, pubkey: pubkey)) {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey)
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
if let about = profile?.about {
Text(about)
.lineLimit(3)
.font(.footnote)
}
}
VStack {
HStack {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
Spacer()
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey)
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
if let about = profile?.about {
Text(about)
.lineLimit(3)
.font(.footnote)
}
}
Spacer()
}
}
.buttonStyle(PlainButtonStyle())
}
}
+6 -5
View File
@@ -86,7 +86,7 @@ struct ZapButton: View {
return
}
send_zap(damus_state: damus_state, event: event, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: ZapType.pub)
send_zap(damus_state: damus_state, event: event, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type)
self.zapping = true
})
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
@@ -101,7 +101,7 @@ struct ZapButton: View {
CustomizeZapView(state: damus_state, event: event, lnurl: lnurl)
}
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: invoice)
SelectWalletView(default_wallet: damus_state.settings.default_wallet, showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: invoice)
}
.onReceive(handle_notify(.zapping)) { notif in
let zap_ev = notif.object as! ZappingEvent
@@ -118,11 +118,12 @@ struct ZapButton: View {
case .failed:
break
case .got_zap_invoice(let inv):
if should_show_wallet_selector(damus_state.pubkey) {
if damus_state.settings.show_wallet_selector {
self.invoice = inv
self.showing_select_wallet = true
} else {
open_with_wallet(wallet: get_default_wallet(damus_state.pubkey).model, invoice: inv)
let wallet = damus_state.settings.default_wallet.model
open_with_wallet(wallet: wallet, invoice: inv)
}
}
@@ -173,7 +174,7 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
damus_state.lnurls.endpoints[target.pubkey] = payreq
}
let zap_amount = amount_sats ?? get_default_zap_amount(pubkey: damus_state.pubkey) ?? 1000
let zap_amount = amount_sats ?? damus_state.settings.default_zap_amount
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount, zap_type: zap_type, comment: comment) else {
DispatchQueue.main.async {
+87 -82
View File
@@ -6,7 +6,6 @@
//
import SwiftUI
import Starscream
struct TimestampedProfile {
let profile: Profile
@@ -15,17 +14,15 @@ struct TimestampedProfile {
}
enum Sheets: Identifiable {
case post
case post(PostAction)
case report(ReportTarget)
case reply(NostrEvent)
case event(NostrEvent)
case filter
var id: String {
switch self {
case .report: return "report"
case .post: return "post"
case .reply(let ev): return "reply-" + ev.id
case .post(let action): return "post-" + (action.ev?.id ?? "")
case .event(let ev): return "event-" + ev.id
case .filter: return "filter"
}
@@ -66,7 +63,6 @@ struct ContentView: View {
@State var active_sheet: Sheets? = nil
@State var damus_state: DamusState? = nil
@State var selected_timeline: Timeline? = .home
@State var is_thread_open: Bool = false
@State var is_deleted_account: Bool = false
@State var is_profile_open: Bool = false
@State var event: NostrEvent? = nil
@@ -80,7 +76,6 @@ struct ContentView: View {
@State var confirm_mute: Bool = false
@State var user_muted_confirm: Bool = false
@State var confirm_overwrite_mutelist: Bool = false
@State var current_boost: NostrEvent? = nil
@State var filter_state : FilterState = .posts_and_replies
@State private var isSideBarOpened = false
@StateObject var home: HomeModel = HomeModel()
@@ -91,11 +86,19 @@ struct ContentView: View {
let sub_id = UUID().description
@Environment(\.colorScheme) var colorScheme
var mystery: some View {
Text("Are you lost?", comment: "Text asking the user if they are lost in the app.")
.id("what")
}
var PostingTimelineView: some View {
VStack {
ZStack {
TabView(selection: $filter_state) {
// This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why.
mystery
contentTimelineView(filter: FilterState.posts.filter)
.tag(FilterState.posts)
.id(FilterState.posts)
@@ -107,7 +110,7 @@ struct ContentView: View {
if privkey != nil {
PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) {
self.active_sheet = .post
self.active_sheet = .post(.posting)
}
}
}
@@ -176,8 +179,7 @@ struct ContentView: View {
NotificationsView(state: damus, notifications: home.notifications)
case .dms:
DirectMessagesView(damus_state: damus_state!)
.environmentObject(home.dms)
DirectMessagesView(damus_state: damus_state!, model: damus_state!.dms, settings: damus_state!.settings)
case .none:
EmptyView()
@@ -240,6 +242,11 @@ struct ContentView: View {
}
}
func open_event(ev: NostrEvent) {
self.active_event = ev
self.thread_open = true
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
if let damus = self.damus_state {
@@ -251,7 +258,7 @@ struct ContentView: View {
Button {
isSideBarOpened.toggle()
} label: {
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles)
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles, disable_animation: damus_state!.settings.disable_animation)
.opacity(isSideBarOpened ? 0 : 1)
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
}
@@ -260,13 +267,7 @@ struct ContentView: View {
ToolbarItem(placement: .navigationBarTrailing) {
HStack(alignment: .center) {
if home.signal.signal != home.signal.max_signal {
NavigationLink(destination: RelayConfigView(state: damus_state!)) {
Text("\(home.signal.signal)/\(home.signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
.font(.callout)
.foregroundColor(.gray)
}
}
SignalView(state: damus_state!, signal: home.signal)
// maybe expand this to other timelines in the future
if selected_timeline == .search {
@@ -291,7 +292,7 @@ struct ContentView: View {
}
.navigationViewStyle(.stack)
TabBar(new_events: $home.new_events, selected: $selected_timeline, isSidebarVisible: $isSideBarOpened, action: switch_timeline)
TabBar(new_events: $home.new_events, selected: $selected_timeline, settings: damus.settings, action: switch_timeline)
.padding([.bottom], 8)
.background(Color(uiColor: .systemBackground).ignoresSafeArea())
}
@@ -305,10 +306,8 @@ struct ContentView: View {
switch item {
case .report(let target):
MaybeReportView(target: target)
case .post:
PostView(replying_to: nil, damus_state: damus_state!)
case .reply(let event):
PostView(replying_to: event, damus_state: damus_state!)
case .post(let action):
PostView(action: action, damus_state: damus_state!)
case .event:
EventDetailView()
case .filter:
@@ -335,10 +334,9 @@ struct ContentView: View {
} else if ref.key == "e" {
find_event(state: damus_state!, evid: ref.ref_id, search_type: .event, find_from: nil) { ev in
if let ev {
active_event = ev
open_event(ev: ev)
}
}
thread_open = true
}
case .filter(let filt):
active_search = filt
@@ -348,19 +346,9 @@ struct ContentView: View {
}
}
.onReceive(handle_notify(.boost)) { notif in
current_boost = (notif.object as? NostrEvent)
}
.onReceive(handle_notify(.open_thread)) { obj in
//let ev = obj.object as! NostrEvent
//thread.set_active_event(ev)
//is_thread_open = true
}
.onReceive(handle_notify(.reply)) { notif in
let ev = notif.object as! NostrEvent
self.active_sheet = .reply(ev)
}
.onReceive(handle_notify(.like)) { like in
.onReceive(handle_notify(.compose)) { notif in
let action = notif.object as! PostAction
self.active_sheet = .post(action)
}
.onReceive(handle_notify(.deleted_account)) { notif in
self.is_deleted_account = true
@@ -466,7 +454,56 @@ struct ContentView: View {
self.damus_state?.pool.connect_to_disconnected()
}
.onReceive(handle_notify(.new_mutes)) { notif in
home.filter_muted()
home.filter_events()
}
.onReceive(handle_notify(.mute_thread)) { notif in
home.filter_events()
}
.onReceive(handle_notify(.unmute_thread)) { notif in
home.filter_events()
}
.onReceive(handle_notify(.local_notification)) { notif in
guard let local = notif.object as? LossyLocalNotification,
let damus_state else {
return
}
guard let target = damus_state.events.lookup(local.event_id) else {
return
}
switch local.type {
case .dm:
selected_timeline = .dms
damus_state.dms.open_dm_by_pk(target.pubkey)
case .like: fallthrough
case .zap: fallthrough
case .mention: fallthrough
case .repost:
open_event(ev: target)
}
}
.onReceive(handle_notify(.onlyzaps_mode)) { notif in
let hide = notif.object as! Bool
home.filter_events()
guard let damus_state else {
return
}
guard let profile = damus_state.profiles.lookup(id: damus_state.pubkey) else {
return
}
profile.reactions = !hide
guard let profile_ev = make_metadata_event(keypair: damus_state.keypair, metadata: profile) else {
return
}
damus_state.postbox.send(profile_ev)
}
.alert(NSLocalizedString("Deleted Account", comment: "Alert message to indicate this is a deleted account"), isPresented: $is_deleted_account) {
Button(NSLocalizedString("Logout", comment: "Button to close the alert that informs that the current account has been deleted.")) {
@@ -555,21 +592,11 @@ struct ContentView: View {
Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.")
}
})
.alert(NSLocalizedString("Repost", comment: "Title of alert for confirming to repost a post."), isPresented: $current_boost.mappedToBool()) {
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of reposting a post.")) {
current_boost = nil
}
Button(NSLocalizedString("Repost", comment: "Button to confirm reposting a post.")) {
if let current_boost {
self.damus_state?.pool.send(.event(current_boost))
}
}
} message: {
Text("Are you sure you want to repost this?", comment: "Alert message to ask if user wants to repost a post.")
}
}
func switch_timeline(_ timeline: Timeline) {
self.isSideBarOpened = false
self.popToRoot()
NotificationCenter.default.post(name: .switched_timeline, object: timeline)
@@ -602,15 +629,18 @@ struct ContentView: View {
let new_relay_filters = load_relay_filters(pubkey) == nil
for relay in bootstrap_relays {
if let url = URL(string: relay) {
if let url = RelayURL(relay) {
add_new_relay(relay_filters: relay_filters, metadatas: metadatas, pool: pool, url: url, info: .rw, new_relay_filters: new_relay_filters)
}
}
pool.register_handler(sub_id: sub_id, handler: home.handle_event)
// dumb stuff needed for property wrappers
UserSettingsStore.pubkey = pubkey
let settings = UserSettingsStore()
UserSettingsStore.shared = settings
self.damus_state = DamusState(pool: pool,
keypair: keypair,
likes: EventCounter(our_pubkey: pubkey),
@@ -631,7 +661,7 @@ struct ContentView: View {
postbox: PostBox(pool: pool),
bootstrap_relays: bootstrap_relays,
replies: ReplyCounter(our_pubkey: pubkey),
translations: Translations(settings)
muted_threads: MutedThreadsManager(keypair: keypair)
)
home.damus_state = self.damus_state!
@@ -655,31 +685,6 @@ func get_since_time(last_event: NostrEvent?) -> Int64? {
return nil
}
func ws_nostr_event(relay: String, ev: WebSocketEvent) -> NostrEvent? {
switch ev {
case .binary(let dat):
return NostrEvent(content: "binary data? \(dat.count) bytes", pubkey: relay)
case .cancelled:
return NostrEvent(content: "cancelled", pubkey: relay)
case .connected:
return NostrEvent(content: "connected", pubkey: relay)
case .disconnected:
return NostrEvent(content: "disconnected", pubkey: relay)
case .error(let err):
return NostrEvent(content: "error \(err.debugDescription)", pubkey: relay)
case .text(let txt):
return NostrEvent(content: "text \(txt)", pubkey: relay)
case .pong:
return NostrEvent(content: "pong", pubkey: relay)
case .ping:
return NostrEvent(content: "ping", pubkey: relay)
case .viabilityChanged(let b):
return NostrEvent(content: "viabilityChanged \(b)", pubkey: relay)
case .reconnectSuggested(let b):
return NostrEvent(content: "reconnectSuggested \(b)", pubkey: relay)
}
}
func is_notification(ev: NostrEvent, pubkey: String) -> Bool {
if ev.pubkey == pubkey {
return false
@@ -793,7 +798,7 @@ func find_event(state: DamusState, evid: String, search_type: SearchType, find_f
var filter = search_type == .event ? NostrFilter.filter_ids([ evid ]) : NostrFilter.filter_authors([ evid ])
if search_type == .profile {
filter.kinds = [0]
filter.kinds = [NostrKind.metadata.rawValue]
}
filter.limit = 1
+12
View File
@@ -23,6 +23,18 @@ class ActionBarModel: ObservableObject {
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
}
init() {
self.our_like = nil
self.our_boost = nil
self.our_reply = nil
self.our_zap = nil
self.likes = 0
self.boosts = 0
self.zaps = 0
self.zap_total = 0
self.replies = 0
}
init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, replies: Int, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zap?, our_reply: NostrEvent?) {
self.likes = likes
self.boosts = boosts
+2 -2
View File
@@ -19,7 +19,7 @@ func load_bookmarks(pubkey: String) -> [NostrEvent] {
}
func save_bookmarks(pubkey: String, current_value: [NostrEvent], value: [NostrEvent]) -> Bool {
let uniq_bookmarks = Array(Set(value))
let uniq_bookmarks = uniq(value)
if uniq_bookmarks != current_value {
let encoded = uniq_bookmarks.map(event_to_json)
@@ -61,7 +61,7 @@ class BookmarksManager: ObservableObject {
if isBookmarked(ev) {
bookmarks = bookmarks.filter { $0 != ev }
} else {
bookmarks.append(ev)
bookmarks.insert(ev, at: 0)
}
}
+1 -1
View File
@@ -242,7 +242,7 @@ func follow_with_existing_contacts(our_pubkey: String, our_contacts: NostrEvent,
func make_contact_relays(_ relays: [RelayDescriptor]) -> [String: RelayInfo] {
return relays.reduce(into: [:]) { acc, relay in
acc[relay.url.absoluteString] = relay.info
acc[relay.url.url.absoluteString] = relay.info
}
}
+13 -4
View File
@@ -29,7 +29,16 @@ struct DamusState {
let postbox: PostBox
let bootstrap_relays: [String]
let replies: ReplyCounter
let translations: Translations
let muted_threads: MutedThreadsManager
@discardableResult
func add_zap(zap: Zap) -> Bool {
// store generic zap mapping
self.zaps.add_zap(zap: zap)
// associate with events as well
return self.events.store_zap(zap: zap)
}
var pubkey: String {
return keypair.pubkey
}
@@ -38,8 +47,8 @@ struct DamusState {
keypair.privkey != nil
}
static var settings_pubkey: String? = nil
static var empty: DamusState {
let settings = UserSettingsStore()
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), translations: Translations(settings))
}
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil))) }
}
+13 -1
View File
@@ -7,7 +7,19 @@
import Foundation
enum DeepLPlan: String, CaseIterable, Identifiable {
enum DeepLPlan: String, CaseIterable, Identifiable, StringCodable {
init?(from string: String) {
guard let dl = DeepLPlan(rawValue: string) else {
return nil
}
self = dl
}
func to_string() -> String {
return self.rawValue
}
var id: String { self.rawValue }
struct Model: Identifiable, Hashable {
+6 -2
View File
@@ -16,6 +16,8 @@ class DirectMessageModel: ObservableObject {
@Published var draft: String
let pubkey: String
var is_request: Bool
var our_pubkey: String
@@ -29,17 +31,19 @@ class DirectMessageModel: ObservableObject {
return true
}
init(events: [NostrEvent], our_pubkey: String) {
init(events: [NostrEvent], our_pubkey: String, pubkey: String) {
self.events = events
self.is_request = false
self.our_pubkey = our_pubkey
self.draft = ""
self.pubkey = pubkey
}
init(our_pubkey: String) {
init(our_pubkey: String, pubkey: String) {
self.events = []
self.is_request = false
self.our_pubkey = our_pubkey
self.draft = ""
self.pubkey = pubkey
}
}
+32 -9
View File
@@ -8,20 +8,43 @@
import Foundation
class DirectMessagesModel: ObservableObject {
@Published var dms: [(String, DirectMessageModel)] = []
@Published var dms: [DirectMessageModel] = []
@Published var loading: Bool = false
@Published var open_dm: Bool = false
@Published private(set) var active_model: DirectMessageModel = DirectMessageModel(our_pubkey: "", pubkey: "")
let our_pubkey: String
init(our_pubkey: String) {
self.our_pubkey = our_pubkey
}
var message_requests: [(String, DirectMessageModel)] {
return dms.filter { dm in dm.1.is_request }
var message_requests: [DirectMessageModel] {
return dms.filter { dm in dm.is_request }
}
var friend_dms: [(String, DirectMessageModel)] {
return dms.filter { dm in !dm.1.is_request }
var friend_dms: [DirectMessageModel] {
return dms.filter { dm in !dm.is_request }
}
func set_active_dm_model(_ model: DirectMessageModel) {
self.active_model = model
}
func open_dm_by_pk(_ pubkey: String) {
self.set_active_dm(pubkey)
self.open_dm = true
}
func open_dm_by_model(_ model: DirectMessageModel) {
self.set_active_dm_model(model)
self.open_dm = true
}
func set_active_dm(_ pubkey: String) {
for model in self.dms where model.pubkey == pubkey {
self.set_active_dm_model(model)
break
}
}
func lookup_or_create(_ pubkey: String) -> DirectMessageModel {
@@ -29,15 +52,15 @@ class DirectMessagesModel: ObservableObject {
return dm
}
let new = DirectMessageModel(our_pubkey: our_pubkey)
dms.append((pubkey, new))
let new = DirectMessageModel(our_pubkey: our_pubkey, pubkey: pubkey)
dms.append(new)
return new
}
func lookup(_ pubkey: String) -> DirectMessageModel? {
for dm in dms {
if pubkey == dm.0 {
return dm.1
if pubkey == dm.pubkey {
return dm
}
}
+20 -4
View File
@@ -1,5 +1,5 @@
//
// DraftModel.swift
// DraftsModel.swift
// damus
//
// Created by Terry Yiu on 2/12/23.
@@ -7,7 +7,23 @@
import Foundation
class Drafts: ObservableObject {
@Published var post: NSMutableAttributedString = NSMutableAttributedString(string: "")
@Published var replies: [NostrEvent: NSMutableAttributedString] = [:]
class DraftArtifacts {
var content: NSMutableAttributedString
var media: [UploadedMedia]
init() {
self.content = NSMutableAttributedString(string: "")
self.media = []
}
init(content: NSMutableAttributedString, media: [UploadedMedia]) {
self.content = content
self.media = media
}
}
class Drafts: ObservableObject {
@Published var post: DraftArtifacts? = nil
@Published var replies: [NostrEvent: DraftArtifacts] = [:]
@Published var quotes: [NostrEvent: DraftArtifacts] = [:]
}
+5 -1
View File
@@ -74,8 +74,12 @@ func build_mention_indices(_ blocks: [Block], type: MentionType) -> Set<Int> {
switch block {
case .mention(let m):
if m.type == type {
acc.insert(m.index)
if let idx = m.index {
acc.insert(idx)
}
}
case .relay:
return
case .text:
return
case .hashtag:
+1 -1
View File
@@ -82,7 +82,7 @@ class FollowersModel: ObservableObject {
if ev.known_kind == .contacts {
handle_contact_event(ev)
} else if ev.known_kind == .metadata {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
}
case .notice(let msg):
+2 -2
View File
@@ -22,7 +22,7 @@ class FollowingModel {
}
func get_filter() -> NostrFilter {
var f = NostrFilter.filter_kinds([0])
var f = NostrFilter.filter_kinds([NostrKind.metadata.rawValue])
f.authors = self.contacts.reduce(into: Array<String>()) { acc, pk in
// don't fetch profiles we already have
if damus_state.profiles.lookup(id: pk) != nil {
@@ -62,7 +62,7 @@ class FollowingModel {
break
case .event(_, let ev):
if ev.kind == 0 {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
}
case .notice(let msg):
print("followingmodel notice: \(msg)")
+226 -136
View File
@@ -8,26 +8,19 @@
import Foundation
import UIKit
struct NewEventsBits {
let bits: Int
init() {
bits = 0
}
init (prev: NewEventsBits, setting: Timeline) {
self.bits = prev.bits | timeline_bit(setting)
}
init (prev: NewEventsBits, unsetting: Timeline) {
self.bits = prev.bits & ~timeline_bit(unsetting)
}
func is_set(_ timeline: Timeline) -> Bool {
let notification_bit = timeline_bit(timeline)
return (bits & notification_bit) == notification_bit
}
struct NewEventsBits: OptionSet {
let rawValue: Int
static let home = NewEventsBits(rawValue: 1 << 0)
static let zaps = NewEventsBits(rawValue: 1 << 1)
static let mentions = NewEventsBits(rawValue: 1 << 2)
static let reposts = NewEventsBits(rawValue: 1 << 3)
static let likes = NewEventsBits(rawValue: 1 << 4)
static let search = NewEventsBits(rawValue: 1 << 5)
static let dms = NewEventsBits(rawValue: 1 << 6)
static let all = NewEventsBits(rawValue: 0xFFFFFFFF)
static let notifications: NewEventsBits = [.zaps, .likes, .reposts, .mentions]
}
class HomeModel: ObservableObject {
@@ -48,34 +41,27 @@ class HomeModel: ObservableObject {
let dms_subid = UUID().description
let init_subid = UUID().description
let profiles_subid = UUID().description
var loading: Bool = false
var signal = SignalModel()
@Published var new_events: NewEventsBits = NewEventsBits()
@Published var notifications = NotificationsModel()
@Published var dms: DirectMessagesModel
@Published var events = EventHolder()
@Published var loading: Bool = false
@Published var signal: SignalModel = SignalModel()
init() {
self.damus_state = DamusState.empty
self.dms = DirectMessagesModel(our_pubkey: "")
}
init(damus_state: DamusState) {
self.damus_state = damus_state
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
filter_events()
self.setup_debouncer()
}
var pool: RelayPool {
return damus_state.pool
}
func setup_debouncer() {
// turn off debouncer after initial load
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.should_debounce_dms = false
}
var dms: DirectMessagesModel {
return damus_state.dms
}
func has_sub_id_event(sub_id: String, ev_id: String) -> Bool {
@@ -86,6 +72,13 @@ class HomeModel: ObservableObject {
return has_event[sub_id]!.contains(ev_id)
}
func setup_debouncer() {
// turn off debouncer after initial load
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.should_debounce_dms = false
}
}
func process_event(sub_id: String, relay_id: String, ev: NostrEvent) {
if has_sub_id_event(sub_id: sub_id, ev_id: ev.id) {
@@ -135,13 +128,13 @@ class HomeModel: ObservableObject {
return
}
damus_state.zaps.add_zap(zap: zap)
damus_state.add_zap(zap: zap)
guard zap.target.pubkey == our_keypair.pubkey else {
return
}
if !notifications.insert_zap(zap) {
if !notifications.insert_zap(zap, damus_state: damus_state) {
return
}
@@ -152,7 +145,7 @@ class HomeModel: ObservableObject {
}
if damus_state.settings.zap_notification {
// Create in-app local notification for zap received.
create_in_app_zap_notification(profiles: profiles, zap: zap)
create_in_app_zap_notification(profiles: profiles, zap: zap, evId: ev.referenced_ids.first?.id ?? "")
}
}
@@ -193,27 +186,31 @@ class HomeModel: ObservableObject {
}
func handle_channel_create(_ ev: NostrEvent) {
guard ev.is_valid else {
return
}
self.channels[ev.id] = ev
}
func handle_channel_meta(_ ev: NostrEvent) {
}
func filter_muted() {
events.filter { !damus_state.contacts.is_muted($0.pubkey) }
self.dms.dms = dms.dms.filter { !damus_state.contacts.is_muted($0.0) }
notifications.filter { !damus_state.contacts.is_muted($0.pubkey) }
func filter_events() {
events.filter { ev in
!damus_state.contacts.is_muted(ev.pubkey)
}
self.dms.dms = dms.dms.filter { ev in
!damus_state.contacts.is_muted(ev.pubkey)
}
notifications.filter { ev in
if damus_state.settings.onlyzaps_mode && ev.known_kind == NostrKind.like {
return false
}
return !damus_state.contacts.is_muted(ev.pubkey) && !damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey)
}
}
func handle_delete_event(_ ev: NostrEvent) {
guard ev.is_valid else {
return
}
self.deleted_events.insert(ev.id)
}
@@ -232,10 +229,10 @@ class HomeModel: ObservableObject {
func handle_boost_event(sub_id: String, _ ev: NostrEvent) {
var boost_ev_id = ev.last_refid()?.ref_id
if let inner_ev = ev.inner_event {
if let inner_ev = ev.get_inner_event(cache: damus_state.events) {
boost_ev_id = inner_ev.id
guard inner_ev.is_valid else {
guard validate_event(ev: inner_ev) == .ok else {
return
}
@@ -264,6 +261,10 @@ class HomeModel: ObservableObject {
return
}
if damus_state.settings.onlyzaps_mode {
return
}
switch damus_state.likes.add_event(ev, target: e.ref_id) {
case .already_counted:
break
@@ -289,34 +290,28 @@ class HomeModel: ObservableObject {
send_home_filters(relay_id: relay_id)
}
case .error(let merr):
let desc = merr.debugDescription
let desc = String(describing: merr)
if desc.contains("Software caused connection abort") {
pool.reconnect(to: [relay_id])
}
case .disconnected: fallthrough
case .cancelled:
case .disconnected:
pool.reconnect(to: [relay_id])
case .reconnectSuggested(let t):
if t {
pool.reconnect(to: [relay_id])
}
default:
break
}
update_signal_from_pool(signal: signal, pool: damus_state.pool)
print("ws_event \(ev)")
update_signal_from_pool(signal: self.signal, pool: damus_state.pool)
case .nostr_event(let ev):
switch ev {
case .event(let sub_id, let ev):
// globally handle likes
/*
let always_process = sub_id == notifications_subid || sub_id == contacts_subid || sub_id == home_subid || sub_id == dms_subid || sub_id == init_subid || ev.known_kind == .like || ev.known_kind == .boost || ev.known_kind == .zap || ev.known_kind == .contacts || ev.known_kind == .metadata
if !always_process {
// TODO: other views like threads might have their own sub ids, so ignore those events... or should we?
return
}
*/
self.process_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
case .notice(let msg):
@@ -326,11 +321,13 @@ class HomeModel: ObservableObject {
case .eose(let sub_id):
if sub_id == dms_subid {
var dms = dms.dms.flatMap { $0.1.events }
var dms = dms.dms.flatMap { $0.events }
dms.append(contentsOf: incoming_dms)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(dms), damus_state: damus_state)
} else if sub_id == notifications_subid {
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_keys(notifications.uniq_pubkeys()), damus_state: damus_state)
} else if sub_id == home_subid {
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus_state)
}
self.loading = false
@@ -360,13 +357,13 @@ class HomeModel: ObservableObject {
var friends = damus_state.contacts.get_friend_list()
friends.append(damus_state.pubkey)
var contacts_filter = NostrFilter.filter_kinds([0])
var contacts_filter = NostrFilter.filter_kinds([NostrKind.metadata.rawValue])
contacts_filter.authors = friends
var our_contacts_filter = NostrFilter.filter_kinds([3, 0])
var our_contacts_filter = NostrFilter.filter_kinds([NostrKind.contacts.rawValue, NostrKind.metadata.rawValue])
our_contacts_filter.authors = [damus_state.pubkey]
var our_blocklist_filter = NostrFilter.filter_kinds([30000])
var our_blocklist_filter = NostrFilter.filter_kinds([NostrKind.list.rawValue])
our_blocklist_filter.parameter = ["mute"]
our_blocklist_filter.authors = [damus_state.pubkey]
@@ -385,21 +382,27 @@ class HomeModel: ObservableObject {
our_dms_filter.authors = [ damus_state.pubkey ]
// TODO: separate likes?
var home_filter = NostrFilter.filter_kinds([
var home_filter_kinds = [
NostrKind.text.rawValue,
NostrKind.like.rawValue,
NostrKind.boost.rawValue,
])
NostrKind.boost.rawValue
]
if !damus_state.settings.onlyzaps_mode {
home_filter_kinds.append(NostrKind.like.rawValue)
}
var home_filter = NostrFilter.filter_kinds(home_filter_kinds)
// include our pubkey as well even if we're not technically a friend
home_filter.authors = friends
home_filter.limit = 500
var notifications_filter = NostrFilter.filter_kinds([
var notifications_filter_kinds = [
NostrKind.text.rawValue,
NostrKind.like.rawValue,
NostrKind.boost.rawValue,
NostrKind.zap.rawValue,
])
]
if !damus_state.settings.onlyzaps_mode {
notifications_filter_kinds.append(NostrKind.like.rawValue)
}
var notifications_filter = NostrFilter.filter_kinds(notifications_filter_kinds)
notifications_filter.pubkeys = [damus_state.pubkey]
notifications_filter.limit = 500
@@ -454,7 +457,7 @@ class HomeModel: ObservableObject {
}
func handle_metadata_event(_ ev: NostrEvent) {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
}
func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? {
@@ -476,16 +479,17 @@ class HomeModel: ObservableObject {
return
}
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
guard should_show_event(contacts: damus_state.contacts, ev: ev) && !damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey) else {
return
}
damus_state.events.insert(ev)
if let inner_ev = ev.inner_event {
if let inner_ev = ev.get_inner_event(cache: damus_state.events) {
damus_state.events.insert(inner_ev)
}
if !notifications.insert_event(ev) {
if !notifications.insert_event(ev, damus_state: damus_state) {
return
}
@@ -517,6 +521,8 @@ class HomeModel: ObservableObject {
return
}
// TODO: will we need to process this in other places like zap request contents, etc?
process_image_metadata(cache: damus_state.events, ev: ev)
damus_state.replies.count_replies(ev)
damus_state.events.insert(ev)
@@ -527,15 +533,26 @@ class HomeModel: ObservableObject {
}
}
func got_new_dm(notifs: NewEventsBits, ev: NostrEvent) {
self.new_events = notifs
if damus_state.settings.dm_notification {
let convo = ev.decrypted(privkey: self.damus_state.keypair.privkey) ?? NSLocalizedString("New encrypted direct message", comment: "Notification that the user has received a new direct message")
let notify = LocalNotification(type: .dm, event: ev, target: ev, content: convo)
create_local_notification(profiles: damus_state.profiles, notify: notify)
}
}
func handle_dm(_ ev: NostrEvent) {
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
return
}
damus_state.events.insert(ev)
if !should_debounce_dms {
self.incoming_dms.append(ev)
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
self.new_events = notifs
got_new_dm(notifs: notifs, ev: ev)
}
self.incoming_dms = []
return
@@ -545,11 +562,7 @@ class HomeModel: ObservableObject {
dm_debouncer.debounce { [self] in
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
self.new_events = notifs
if damus_state.settings.dm_notification,
let displayName = damus_state.profiles.lookup(id: self.incoming_dms.last!.pubkey)?.display_name {
create_local_notification(displayName: displayName, conversation: "You have received a direct message", type: .dm)
}
got_new_dm(notifs: notifs, ev: ev)
}
self.incoming_dms = []
}
@@ -562,8 +575,8 @@ func update_signal_from_pool(signal: SignalModel, pool: RelayPool) {
signal.max_signal = pool.relays.count
}
if signal.signal != pool.num_connecting {
signal.signal = signal.max_signal - pool.num_connecting
if signal.signal != pool.num_connected {
signal.signal = pool.num_connected
}
}
@@ -657,15 +670,9 @@ func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) {
print("-----")
}
func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEvent) {
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
return
}
func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: Profile, ev: NostrEvent) {
if our_pubkey == ev.pubkey && (profile.deleted ?? false) {
DispatchQueue.main.async {
notify(.deleted_account, ())
}
notify(.deleted_account, ())
return
}
@@ -699,21 +706,57 @@ func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEve
// load pfps asap
let picture = tprof.profile.picture ?? robohash(ev.pubkey)
if URL(string: picture) != nil {
DispatchQueue.main.async {
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
}
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
}
let banner = tprof.profile.banner ?? ""
if URL(string: banner) != nil {
DispatchQueue.main.async {
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
}
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
}
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
}
func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping () -> Void) {
let validated = events.is_event_valid(ev.id)
switch validated {
case .unknown:
Task {
let result = validate_event(ev: ev)
DispatchQueue.main.async {
events.store_event_validation(evid: ev.id, validated: result)
guard result == .ok else {
return
}
callback()
}
}
case .ok:
callback()
case .bad_id: fallthrough
case .bad_sig:
break
}
}
func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Profiles, ev: NostrEvent) {
guard_valid_event(events: events, ev: ev) {
DispatchQueue.global(qos: .background).async {
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
return
}
DispatchQueue.main.async {
process_metadata_profile(our_pubkey: our_pubkey, profiles: profiles, profile: profile, ev: ev)
}
}
}
}
func robohash(_ pk: String) -> String {
return "https://robohash.org/" + pk
}
@@ -770,7 +813,7 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
for d in diff {
changed = true
if new.contains(d) {
if let url = URL(string: d) {
if let url = RelayURL(d) {
add_new_relay(relay_filters: state.relay_filters, metadatas: state.relay_metadata, pool: state.pool, url: url, info: decoded[d] ?? .rw, new_relay_filters: new_relay_filters)
}
} else {
@@ -784,10 +827,10 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
}
}
func add_new_relay(relay_filters: RelayFilters, metadatas: RelayMetadatas, pool: RelayPool, url: URL, info: RelayInfo, new_relay_filters: Bool) {
func add_new_relay(relay_filters: RelayFilters, metadatas: RelayMetadatas, pool: RelayPool, url: RelayURL, info: RelayInfo, new_relay_filters: Bool) {
try? pool.add_relay(url, info: info)
let relay_id = url.absoluteString
let relay_id = url.id
guard metadatas.lookup(relay_id: relay_id) == nil else {
return
}
@@ -852,10 +895,10 @@ func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesM
}
}
for (pk, _) in dms.dms {
if pk == the_pk {
for model in dms.dms {
if model.pubkey == the_pk {
found = true
inserted = insert_uniq_sorted_event(events: &(dms.dms[i].1.events), new_ev: ev) {
inserted = insert_uniq_sorted_event(events: &(dms.dms[i].events), new_ev: ev) {
$0.created_at < $1.created_at
}
@@ -865,8 +908,8 @@ func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesM
}
if !found {
let model = DirectMessageModel(events: [ev], our_pubkey: our_pubkey)
dms.dms.append((the_pk, model))
let model = DirectMessageModel(events: [ev], our_pubkey: our_pubkey, pubkey: the_pk)
dms.dms.append(model)
inserted = true
}
@@ -893,14 +936,53 @@ func handle_incoming_dms(prev_events: NewEventsBits, dms: DirectMessagesModel, o
}
if inserted {
dms.dms = dms.dms.filter({ $0.1.events.count > 0 }).sorted { a, b in
return a.1.events.last!.created_at > b.1.events.last!.created_at
dms.dms = dms.dms.filter({ $0.events.count > 0 }).sorted { a, b in
return a.events.last!.created_at > b.events.last!.created_at
}
}
return new_events
}
func determine_event_notifications(_ ev: NostrEvent) -> NewEventsBits {
guard let kind = ev.known_kind else {
return []
}
if kind == .zap {
return [.zaps]
}
if kind == .boost {
return [.reposts]
}
if kind == .text {
return [.mentions]
}
if kind == .like {
return [.likes]
}
return []
}
func timeline_to_notification_bits(_ timeline: Timeline, ev: NostrEvent?) -> NewEventsBits {
switch timeline {
case .home:
return [.home]
case .notifications:
if let ev {
return determine_event_notifications(ev)
}
return [.notifications]
case .search:
return [.search]
case .dms:
return [.dms]
}
}
/// A helper to determine if we need to notify the user of new events
func handle_last_events(new_events: NewEventsBits, ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) -> NewEventsBits? {
@@ -909,7 +991,7 @@ func handle_last_events(new_events: NewEventsBits, ev: NostrEvent, timeline: Tim
if last_ev == nil || last_ev!.created_at < ev.created_at {
save_last_event(ev, timeline: timeline)
if shouldNotify {
return NewEventsBits(prev: new_events, setting: timeline)
return new_events.union(timeline_to_notification_bits(timeline, ev: ev))
}
}
@@ -975,12 +1057,13 @@ func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale
}
}
func create_in_app_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) {
func create_in_app_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: String) {
let content = UNMutableNotificationContent()
content.title = zap_notification_title(zap)
content.body = zap_notification_body(profiles: profiles, zap: zap, locale: locale)
content.sound = UNNotificationSound.default
content.userInfo = LossyLocalNotification(type: .zap, event_id: evId).to_user_info()
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
@@ -1007,53 +1090,60 @@ func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
return
}
if type == .text && damus_state.settings.mention_notification {
for block in ev.blocks(damus_state.keypair.privkey) {
if case .mention(let mention) = block, mention.ref.ref_id == damus_state.keypair.pubkey,
let displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name {
let justContent = NSAttributedString(render_note_content(ev: ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content).string
create_local_notification(displayName: displayName, conversation: justContent, type: type)
}
}
} else if type == .boost && damus_state.settings.repost_notification,
let displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name {
// Don't show notifications from muted threads.
if damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey) {
return
}
if let inner_ev = ev.inner_event {
create_local_notification(displayName: displayName, conversation: inner_ev.content, type: type)
if type == .text && damus_state.settings.mention_notification {
let blocks = ev.blocks(damus_state.keypair.privkey)
for case .mention(let mention) in blocks where mention.ref.ref_id == damus_state.keypair.pubkey {
let content = NSAttributedString(render_note_content(ev: ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content.attributed).string
let notify = LocalNotification(type: .mention, event: ev, target: ev, content: content)
create_local_notification(profiles: damus_state.profiles, notify: notify )
}
} else if type == .boost && damus_state.settings.repost_notification, let inner_ev = ev.get_inner_event(cache: damus_state.events) {
let notify = LocalNotification(type: .repost, event: ev, target: inner_ev, content: inner_ev.content)
create_local_notification(profiles: damus_state.profiles, notify: notify)
} else if type == .like && damus_state.settings.like_notification,
let displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name,
let e_ref = ev.referenced_ids.first?.ref_id,
let content = damus_state.events.lookup(e_ref)?.content {
create_local_notification(displayName: displayName, conversation: content, type: type)
let evid = ev.referenced_ids.last?.ref_id,
let liked_event = damus_state.events.lookup(evid)
{
let notify = LocalNotification(type: .like, event: ev, target: liked_event, content: liked_event.content)
create_local_notification(profiles: damus_state.profiles, notify: notify)
}
}
func create_local_notification(displayName: String, conversation: String, type: NostrKind) {
func create_local_notification(profiles: Profiles, notify: LocalNotification) {
let content = UNMutableNotificationContent()
var title = ""
var identifier = ""
switch type {
case .text:
let displayName = event_author_name(profiles: profiles, pubkey: notify.event.pubkey)
switch notify.type {
case .mention:
title = String(format: NSLocalizedString("Mentioned by %@", comment: "Mentioned by heading in local notification"), displayName)
identifier = "myMentionNotification"
case .boost:
case .repost:
title = String(format: NSLocalizedString("Reposted by %@", comment: "Reposted by heading in local notification"), displayName)
identifier = "myBoostNotification"
case .like:
title = String(format: NSLocalizedString("Liked by %@", comment: "Liked by heading in local notification"), displayName)
identifier = "myLikeNotification"
case .dm:
title = String(format: NSLocalizedString("DM by %@", comment: "DM by heading in local notification"), displayName)
title = String(format: NSLocalizedString("%@", comment: "DM by heading in local notification"), displayName)
identifier = "myDMNotification"
default:
case .zap:
// not handled here
break
}
content.title = title
content.body = conversation
content.body = notify.content
content.sound = UNNotificationSound.default
content.userInfo = notify.to_lossy().to_user_info()
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
+9
View File
@@ -25,6 +25,15 @@ enum MediaUpload {
return url.pathExtension
}
}
var localURL: URL {
switch self {
case .image(let url):
return url
case .video(let url):
return url
}
}
var is_image: Bool {
if case .image = self {
+12 -4
View File
@@ -7,7 +7,7 @@
import Foundation
enum LibreTranslateServer: String, CaseIterable, Identifiable {
enum LibreTranslateServer: String, CaseIterable, Identifiable, StringCodable {
var id: String { self.rawValue }
struct Model: Identifiable, Hashable {
@@ -17,9 +17,19 @@ enum LibreTranslateServer: String, CaseIterable, Identifiable {
var url: String?
}
func to_string() -> String {
return rawValue
}
init?(from string: String) {
guard let libreTranslateServer = LibreTranslateServer(rawValue: string) else {
return nil
}
self = libreTranslateServer
}
case argosopentech
case terraprint
case vern
case custom
var model: Model {
@@ -28,8 +38,6 @@ enum LibreTranslateServer: String, CaseIterable, Identifiable {
return .init(tag: self.rawValue, displayName: "translate.argosopentech.com", url: "https://translate.argosopentech.com")
case .terraprint:
return .init(tag: self.rawValue, displayName: "translate.terraprint.co", url: "https://translate.terraprint.co")
case .vern:
return .init(tag: self.rawValue, displayName: "lt.vern.cc", url: "https://lt.vern.cc")
case .custom:
return .init(tag: self.rawValue, displayName: NSLocalizedString("Custom", comment: "Dropdown option for selecting a custom translation server."), url: nil)
}
-14
View File
@@ -1,14 +0,0 @@
//
// LocalUserConfig.swift
// damus
//
// Created by William Casarin on 2022-06-15.
//
import Foundation
struct LocalUserConfig: Codable {
let relays: [RelayDescriptor]
}
+103 -11
View File
@@ -21,8 +21,8 @@ enum MentionType {
}
}
struct Mention {
let index: Int
struct Mention: Equatable {
let index: Int?
let type: MentionType
let ref: ReferencedId
}
@@ -58,12 +58,30 @@ struct LightningInvoice<T> {
}
}
enum Block {
enum Block: Equatable {
static func == (lhs: Block, rhs: Block) -> Bool {
switch (lhs, rhs) {
case (.text(let a), .text(let b)):
return a == b
case (.mention(let a), .mention(let b)):
return a == b
case (.hashtag(let a), .hashtag(let b)):
return a == b
case (.url(let a), .url(let b)):
return a == b
case (.invoice(let a), .invoice(let b)):
return a.string == b.string
case (_, _):
return false
}
}
case text(String)
case mention(Mention)
case hashtag(String)
case url(URL)
case invoice(Invoice)
case relay(String)
var is_invoice: Invoice? {
if case .invoice(let invoice) = self {
@@ -114,7 +132,17 @@ func render_blocks(blocks: [Block]) -> String {
return blocks.reduce("") { str, block in
switch block {
case .mention(let m):
return str + "#[\(m.index)]"
if let idx = m.index {
return str + "#[\(idx)]"
} else if m.type == .pubkey, let pk = bech32_pubkey(m.ref.ref_id) {
return str + "nostr:\(pk)"
} else if let note_id = bech32_note_id(m.ref.ref_id) {
return str + "nostr:\(note_id)"
} else {
return str + m.ref.ref_id
}
case .relay(let relay):
return str + relay
case .text(let txt):
return str + txt
case .hashtag(let htag):
@@ -177,14 +205,16 @@ func convert_block(_ b: block_t, tags: [[String]]) -> Block? {
return nil
}
return .text(str)
} else if b.type == BLOCK_MENTION {
return convert_mention_block(ind: b.block.mention, tags: tags)
} else if b.type == BLOCK_MENTION_INDEX {
return convert_mention_index_block(ind: b.block.mention_index, tags: tags)
} else if b.type == BLOCK_URL {
return convert_url_block(b.block.str)
} else if b.type == BLOCK_INVOICE {
return convert_invoice_block(b.block.invoice)
} else if b.type == BLOCK_MENTION_BECH32 {
return convert_mention_bech32_block(b.block.mention_bech32)
}
return nil
}
@@ -307,6 +337,60 @@ func convert_invoice_block(_ b: invoice_block) -> Block? {
return .invoice(Invoice(description: description, amount: amount, string: invstr, expiry: b11.expiry, payment_hash: payment_hash, created_at: created_at))
}
func convert_mention_bech32_block(_ b: mention_bech32_block) -> Block?
{
switch b.bech32.type {
case NOSTR_BECH32_NOTE:
let note = b.bech32.data.note;
let event_id = hex_encode(Data(bytes: note.event_id, count: 32))
let event_id_ref = ReferencedId(ref_id: event_id, relay_id: nil, key: "e")
return .mention(Mention(index: nil, type: .event, ref: event_id_ref))
case NOSTR_BECH32_NEVENT:
let nevent = b.bech32.data.nevent;
let event_id = hex_encode(Data(bytes: nevent.event_id, count: 32))
var relay_id: String? = nil
if nevent.relays.num_relays > 0 {
relay_id = strblock_to_string(nevent.relays.relays.0)
}
let event_id_ref = ReferencedId(ref_id: event_id, relay_id: relay_id, key: "e")
return .mention(Mention(index: nil, type: .event, ref: event_id_ref))
case NOSTR_BECH32_NPUB:
let npub = b.bech32.data.npub
let pubkey = hex_encode(Data(bytes: npub.pubkey, count: 32))
let pubkey_ref = ReferencedId(ref_id: pubkey, relay_id: nil, key: "p")
return .mention(Mention(index: nil, type: .pubkey, ref: pubkey_ref))
case NOSTR_BECH32_NPROFILE:
let nprofile = b.bech32.data.nprofile
let pubkey = hex_encode(Data(bytes: nprofile.pubkey, count: 32))
var relay_id: String? = nil
if nprofile.relays.num_relays > 0 {
relay_id = strblock_to_string(nprofile.relays.relays.0)
}
let pubkey_ref = ReferencedId(ref_id: pubkey, relay_id: relay_id, key: "p")
return .mention(Mention(index: nil, type: .pubkey, ref: pubkey_ref))
case NOSTR_BECH32_NRELAY:
let nrelay = b.bech32.data.nrelay
guard let relay_str = strblock_to_string(nrelay.relay) else {
return nil
}
return .relay(relay_str)
case NOSTR_BECH32_NADDR:
// TODO: wtf do I do with this
guard let naddr = strblock_to_string(b.str) else {
return nil
}
return .text("nostr:" + naddr)
default:
return nil
}
}
func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
if let desc = b11.description {
return .description(String(cString: desc))
@@ -319,7 +403,7 @@ func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
return nil
}
func convert_mention_block(ind: Int32, tags: [[String]]) -> Block?
func convert_mention_index_block(ind: Int32, tags: [[String]]) -> Block?
{
let ind = Int(ind)
@@ -557,7 +641,7 @@ func parse_mention_type(_ c: String) -> MentionType? {
}
/// Convert
func make_post_tags(post_blocks: [PostBlock], tags: [[String]]) -> PostTags {
func make_post_tags(post_blocks: [PostBlock], tags: [[String]], silent_mentions: Bool) -> PostTags {
var new_tags = tags
var blocks: [Block] = []
@@ -567,6 +651,14 @@ func make_post_tags(post_blocks: [PostBlock], tags: [[String]]) -> PostTags {
guard let mention_type = parse_mention_type(ref.key) else {
continue
}
if silent_mentions || mention_type == .event {
let mention = Mention(index: nil, type: mention_type, ref: ref)
let block = Block.mention(mention)
blocks.append(block)
continue
}
if let ind = find_tag_ref(type: ref.key, id: ref.ref_id, tags: tags) {
let mention = Mention(index: ind, type: mention_type, ref: ref)
let block = Block.mention(mention)
@@ -590,9 +682,9 @@ func make_post_tags(post_blocks: [PostBlock], tags: [[String]]) -> PostTags {
}
func post_to_event(post: NostrPost, privkey: String, pubkey: String) -> NostrEvent {
let tags = post.references.map(refid_to_tag)
let tags = post.references.map(refid_to_tag) + post.tags
let post_blocks = parse_post_blocks(content: post.content)
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags)
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags, silent_mentions: false)
let content = render_blocks(blocks: post_tags.blocks)
let new_ev = NostrEvent(content: content, pubkey: pubkey, kind: post.kind.rawValue, tags: post_tags.tags)
new_ev.calculate_id()
+76
View File
@@ -0,0 +1,76 @@
//
// MutedThreadsManager.swift
// damus
//
// Created by Terry Yiu on 4/6/23.
//
import Foundation
fileprivate func getMutedThreadsKey(pubkey: String) -> String {
pk_setting_key(pubkey, key: "muted_threads")
}
func loadMutedThreads(pubkey: String) -> [String] {
let key = getMutedThreadsKey(pubkey: pubkey)
return UserDefaults.standard.stringArray(forKey: key) ?? []
}
func saveMutedThreads(pubkey: String, currentValue: [String], value: [String]) -> Bool {
let uniqueMutedThreads = Array(Set(value))
if uniqueMutedThreads != currentValue {
UserDefaults.standard.set(uniqueMutedThreads, forKey: getMutedThreadsKey(pubkey: pubkey))
return true
}
return false
}
class MutedThreadsManager: ObservableObject {
private let userDefaults = UserDefaults.standard
private let keypair: Keypair
private var _mutedThreadsSet: Set<String>
private var _mutedThreads: [String]
var mutedThreads: [String] {
get {
return _mutedThreads
}
set {
if saveMutedThreads(pubkey: keypair.pubkey, currentValue: _mutedThreads, value: newValue) {
self._mutedThreads = newValue
self.objectWillChange.send()
}
}
}
init(keypair: Keypair) {
self._mutedThreads = loadMutedThreads(pubkey: keypair.pubkey)
self._mutedThreadsSet = Set(_mutedThreads)
self.keypair = keypair
}
func isMutedThread(_ ev: NostrEvent, privkey: String?) -> Bool {
return _mutedThreadsSet.contains(ev.thread_id(privkey: privkey))
}
func updateMutedThread(_ ev: NostrEvent) {
let threadId = ev.thread_id(privkey: nil)
if isMutedThread(ev, privkey: keypair.privkey) {
mutedThreads = mutedThreads.filter { $0 != threadId }
_mutedThreadsSet.remove(threadId)
notify(.unmute_thread, ev)
} else {
mutedThreads.append(threadId)
_mutedThreadsSet.insert(threadId)
notify(.mute_thread, ev)
}
}
func clearAll() {
mutedThreads = []
_mutedThreadsSet.removeAll()
}
}
@@ -29,4 +29,22 @@ class EventGroup {
func insert(_ ev: NostrEvent) -> Bool {
return insert_uniq_sorted_event_created(events: &events, new_ev: ev)
}
func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool {
for ev in events {
if !isIncluded(ev) {
return true
}
}
return false
}
func filter(_ isIncluded: (NostrEvent) -> Bool) -> EventGroup? {
let new_evs = events.filter(isIncluded)
guard new_evs.count > 0 else {
return nil
}
return EventGroup(events: new_evs)
}
}
+21 -4
View File
@@ -30,10 +30,26 @@ class ZapGroup {
}
}
init(zaps: [Zap]) {
self.zaps = zaps
self.msat_total = 0
self.zappers = Set()
func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool {
for zap in zaps {
if !isIncluded(zap.request_ev) {
return true
}
}
return false
}
func filter(_ isIncluded: (NostrEvent) -> Bool) -> ZapGroup? {
let new_zaps = zaps.filter { isIncluded($0.request_ev) }
guard new_zaps.count > 0 else {
return nil
}
let grp = ZapGroup()
for zap in new_zaps {
grp.insert(zap)
}
return grp
}
init() {
@@ -42,6 +58,7 @@ class ZapGroup {
self.zappers = Set()
}
@discardableResult
func insert(_ zap: Zap) -> Bool {
if !insert_uniq_sorted_zap_by_created(zaps: &zaps, new_zap: zap) {
return false
+41 -10
View File
@@ -65,6 +65,37 @@ enum NotificationItem {
return reply.created_at
}
}
func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool {
switch self {
case .repost(_, let evgrp):
return evgrp.would_filter(isIncluded)
case .reaction(_, let evgrp):
return evgrp.would_filter(isIncluded)
case .profile_zap(let zapgrp):
return zapgrp.would_filter(isIncluded)
case .event_zap(_, let zapgrp):
return zapgrp.would_filter(isIncluded)
case .reply(let ev):
return !isIncluded(ev)
}
}
func filter(_ isIncluded: (NostrEvent) -> Bool) -> NotificationItem? {
switch self {
case .repost(let evid, let evgrp):
return evgrp.filter(isIncluded).map { .repost(evid, $0) }
case .reaction(let evid, let evgrp):
return evgrp.filter(isIncluded).map { .reaction(evid, $0) }
case .profile_zap(let zapgrp):
return zapgrp.filter(isIncluded).map { .profile_zap($0) }
case .event_zap(let evid, let zapgrp):
return zapgrp.filter(isIncluded).map { .event_zap(evid, $0) }
case .reply(let ev):
if isIncluded(ev) { return .reply(ev) }
return nil
}
}
}
class NotificationsModel: ObservableObject, ScrollQueue {
@@ -129,7 +160,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
for el in zaps {
let evid = el.key
let zapgrp = el.value
let notif: NotificationItem = .event_zap(evid, zapgrp)
notifs.append(notif)
}
@@ -161,8 +192,8 @@ class NotificationsModel: ObservableObject, ScrollQueue {
}
private func insert_repost(_ ev: NostrEvent) -> Bool {
guard let reposted_ev = ev.inner_event else {
private func insert_repost(_ ev: NostrEvent, cache: EventCache) -> Bool {
guard let reposted_ev = ev.get_inner_event(cache: cache) else {
return false
}
@@ -204,9 +235,9 @@ class NotificationsModel: ObservableObject, ScrollQueue {
}
}
private func insert_event_immediate(_ ev: NostrEvent) -> Bool {
private func insert_event_immediate(_ ev: NostrEvent, cache: EventCache) -> Bool {
if ev.known_kind == .boost {
return insert_repost(ev)
return insert_repost(ev, cache: cache)
} else if ev.known_kind == .like {
return insert_reaction(ev)
} else if ev.known_kind == .text {
@@ -233,12 +264,12 @@ class NotificationsModel: ObservableObject, ScrollQueue {
}
}
func insert_event(_ ev: NostrEvent) -> Bool {
func insert_event(_ ev: NostrEvent, damus_state: DamusState) -> Bool {
if should_queue {
return insert_uniq_sorted_event_created(events: &incoming_events, new_ev: ev)
}
if insert_event_immediate(ev) {
if insert_event_immediate(ev, cache: damus_state.events) {
self.notifications = build_notifications()
return true
}
@@ -246,7 +277,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
return false
}
func insert_zap(_ zap: Zap) -> Bool {
func insert_zap(_ zap: Zap, damus_state: DamusState) -> Bool {
if should_queue {
return insert_uniq_sorted_zap_by_created(zaps: &incoming_zaps, new_zap: zap)
}
@@ -300,7 +331,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
}
}
func flush() -> Bool {
func flush(_ damus_state: DamusState) -> Bool {
var inserted = false
for zap in incoming_zaps {
@@ -308,7 +339,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
}
for event in incoming_events {
inserted = insert_event_immediate(event) || inserted
inserted = insert_event_immediate(event, cache: damus_state.events) || inserted
}
if inserted {
+7 -7
View File
@@ -11,17 +11,17 @@ struct NostrPost {
let kind: NostrKind
let content: String
let references: [ReferencedId]
let tags: [[String]]
init (content: String, references: [ReferencedId]) {
self.content = content
self.references = references
self.kind = .text
}
init (content: String, references: [ReferencedId], kind: NostrKind) {
init (content: String, references: [ReferencedId], kind: NostrKind = .text, tags: [[String]] = []) {
self.content = content
self.references = references
self.kind = kind
self.tags = tags
}
func to_event(keypair: FullKeypair) -> NostrEvent {
return post_to_event(post: self, privkey: keypair.privkey, pubkey: keypair.pubkey)
}
}
+4 -1
View File
@@ -119,7 +119,7 @@ class ProfileModel: ObservableObject, Equatable {
} else if ev.known_kind == .contacts {
handle_profile_contact_event(ev)
} else if ev.known_kind == .metadata {
process_metadata_event(our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev)
process_metadata_event(events: damus.events, our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev)
}
seen_event.insert(ev.id)
}
@@ -140,6 +140,9 @@ class ProfileModel: ObservableObject, Equatable {
case .notice(let notice):
notify(.notice, notice)
case .eose:
if resp.subid == sub_id {
load_profiles(profiles_subid: prof_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus)
}
progress += 1
break
}
+12 -9
View File
@@ -24,7 +24,7 @@ class SearchHomeModel: ObservableObject {
}
func get_base_filter() -> NostrFilter {
var filter = NostrFilter.filter_kinds([1, 42])
var filter = NostrFilter.filter_kinds([NostrKind.text.rawValue, NostrKind.chat.rawValue])
filter.limit = self.limit
filter.until = Int64(Date.now.timeIntervalSince1970)
return filter
@@ -101,10 +101,10 @@ func find_profiles_to_fetch_pk(profiles: Profiles, event_pubkeys: [String]) -> [
return Array(pubkeys)
}
func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad) -> [String] {
func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: EventCache) -> [String] {
switch load {
case .from_events(let events):
return find_profiles_to_fetch_from_events(profiles: profiles, events: events)
return find_profiles_to_fetch_from_events(profiles: profiles, events: events, cache: cache)
case .from_keys(let pks):
return find_profiles_to_fetch_from_keys(profiles: profiles, pks: pks)
}
@@ -124,15 +124,18 @@ func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [String]) -> [Str
return Array(pubkeys)
}
func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent]) -> [String] {
func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent], cache: EventCache) -> [String] {
var pubkeys = Set<String>()
for ev in events {
if profiles.lookup(id: ev.pubkey) != nil {
continue
// lookup profiles from boosted events
if ev.known_kind == .boost, let bev = ev.get_inner_event(cache: cache), profiles.lookup(id: bev.pubkey) == nil {
pubkeys.insert(bev.pubkey)
}
pubkeys.insert(ev.pubkey)
if profiles.lookup(id: ev.pubkey) == nil {
pubkeys.insert(ev.pubkey)
}
}
return Array(pubkeys)
@@ -145,7 +148,7 @@ enum PubkeysToLoad {
func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad, damus_state: DamusState) {
var filter = NostrFilter.filter_profiles
let authors = find_profiles_to_fetch(profiles: damus_state.profiles, load: load)
let authors = find_profiles_to_fetch(profiles: damus_state.profiles, load: load, cache: damus_state.events)
filter.authors = authors
guard !authors.isEmpty else {
@@ -161,7 +164,7 @@ func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad
}
if ev.known_kind == .metadata {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
}
}
+1 -1
View File
@@ -33,7 +33,7 @@ class SearchModel: ObservableObject {
func subscribe() {
// since 1 month
search.limit = self.limit
search.kinds = [1,5,7]
search.kinds = [NostrKind.text.rawValue, NostrKind.like.rawValue]
//likes_filter.ids = ref_events.referenced_ids!
+9 -4
View File
@@ -77,18 +77,23 @@ class ThreadModel: ObservableObject {
var meta_events = NostrFilter()
var event_filter = NostrFilter()
var ref_events = NostrFilter()
//var likes_filter = NostrFilter.filter_kinds(7])
let thread_id = event.thread_id(privkey: nil)
ref_events.referenced_ids = [thread_id, event.id]
ref_events.kinds = [1]
ref_events.kinds = [NostrKind.text.rawValue]
ref_events.limit = 1000
event_filter.ids = [thread_id, event.id]
meta_events.referenced_ids = [event.id]
meta_events.kinds = [9735, 1, 6, 7]
var kinds = [NostrKind.zap.rawValue, NostrKind.text.rawValue, NostrKind.boost.rawValue]
if !damus_state.settings.onlyzaps_mode {
kinds.append(NostrKind.like.rawValue)
}
meta_events.kinds = kinds
meta_events.limit = 1000
/*
@@ -129,7 +134,7 @@ class ThreadModel: ObservableObject {
}
if ev.known_kind == .metadata {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
} else if ev.is_textlike {
self.add_event(ev, privkey: self.damus_state.keypair.privkey)
}
+16 -1
View File
@@ -7,7 +7,19 @@
import Foundation
enum TranslationService: String, CaseIterable, Identifiable {
enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
init?(from string: String) {
guard let ts = TranslationService(rawValue: string) else {
return nil
}
self = ts
}
func to_string() -> String {
return self.rawValue
}
var id: String { self.rawValue }
struct Model: Identifiable, Hashable {
@@ -19,6 +31,7 @@ enum TranslationService: String, CaseIterable, Identifiable {
case none
case libretranslate
case deepl
case nokyctranslate
var model: Model {
switch self {
@@ -28,6 +41,8 @@ enum TranslationService: String, CaseIterable, Identifiable {
return .init(tag: self.rawValue, displayName: NSLocalizedString("LibreTranslate (Open Source)", comment: "Dropdown option for selecting LibreTranslate as the translation service."))
case .deepl:
return .init(tag: self.rawValue, displayName: NSLocalizedString("DeepL (Proprietary, Higher Accuracy)", comment: "Dropdown option for selecting DeepL as the translation service."))
case .nokyctranslate:
return .init(tag: self.rawValue, displayName: NSLocalizedString("NoKYCTranslate.com (Prepay with BTC)", comment: "Dropdown option for selecting NoKYCTranslate.com as the translation service."))
}
}
-150
View File
@@ -1,150 +0,0 @@
//
// Translations.swift
// damus
//
// Created by Terry Yiu on 3/29/23.
//
import Foundation
import NaturalLanguage
class Translations: ObservableObject {
private static let languageDetectionMinConfidence = 0.5
@Published var translations: [NostrEvent: String] = [:]
@Published var languages: [NostrEvent: String] = [:]
let settings: UserSettingsStore
let translator: Translator
let targetLanguage = currentLanguage()
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
init(_ settings: UserSettingsStore) {
self.settings = settings
self.translator = Translator(settings)
}
/**
Attempts to detect the language of the content of a given nostr event using Apple's offline NaturalLanguage API.
The detected language will be returned only if it has a 50% or more confidence.
This is a best effort guess and could be incorrect.
*/
func detectLanguage(_ event: NostrEvent, state: DamusState) -> String? {
if let cachedLanguage = languages[event] {
return cachedLanguage
}
// Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in
// and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer.
let originalBlocks = event.blocks(state.keypair.privkey)
let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ")
// Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate.
let languageRecognizer = NLLanguageRecognizer()
languageRecognizer.processString(originalOnlyText)
guard let locale = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= Translations.languageDetectionMinConfidence })?.key.rawValue else {
return nil
}
// Remove the variant component and just take the language part as translation services typically only supports the variant-less language.
// Moreover, speakers of one variant can generally understand other variants.
let language = localeToLanguage(locale)
languages[event] = language
return language
}
/**
Returns true if the given translation is effectively the same as the original note, ignoring whitespaces and new lines.
*/
private func translationSameAsOriginal(_ translation: String, event: NostrEvent, state: DamusState) -> Bool {
return translation.trimmingCharacters(in: .whitespacesAndNewlines) == event.get_content(state.keypair.privkey).trimmingCharacters(in: .whitespacesAndNewlines)
}
func hasCachedTranslation(_ event: NostrEvent) -> Bool {
return languages[event] != nil
}
func cachedTranslation(_ event: NostrEvent) -> TranslationWithLanguage? {
if let cachedLanguage = languages[event] {
if let cachedTranslation = translations[event] {
return TranslationWithLanguage(translation: cachedTranslation, language: cachedLanguage)
} else {
return nil
}
} else {
return nil
}
}
func translate(_ event: NostrEvent, state: DamusState) async -> TranslationWithLanguage? {
guard shouldTranslate(event, state: state) else {
return nil
}
guard let noteLanguage = detectLanguage(event, state: state) else {
return nil
}
if languages[event] != nil {
return cachedTranslation(event)
}
do {
guard let translationWithLanguage = try await translator.translate(event.get_content(state.keypair.privkey), from: noteLanguage, to: targetLanguage) else {
return nil
}
// If the translated content is identical to the original content, don't return the translation.
if translationSameAsOriginal(translationWithLanguage.translation, event: event, state: state) {
// Nil out the translation as it's the same as the original.
translations[event] = nil
// Leave an entry so that we don't attempt to translate it again in the future.
languages[event] = targetLanguage
return nil
} else {
translations[event] = translationWithLanguage.translation
languages[event] = translationWithLanguage.language
return translationWithLanguage
}
} catch {
return nil
}
}
func shouldTranslate(_ event: NostrEvent, state: DamusState) -> Bool {
// Do not translate self-authored content because if the language recognizer guesses the wrong language for your own note,
// it's annoying and unexpected for the translation to show up.
if event.pubkey == state.pubkey && state.is_privkey_user {
return false
}
// Avoid translating if no translation service is configured.
switch settings.translation_service {
case .none:
return false
case .libretranslate:
if URLComponents(string: settings.libretranslate_url) == nil {
return false
}
case .deepl:
if settings.deepl_api_key == "" {
return false
}
}
// If translation was attempted before, use the results of the cached translation to determine if it should be shown.
if languages[event] != nil {
return translations[event] != nil
}
// Avoid translating notes if language cannot be detected or if it is in one of the user's preferred languages.
guard let noteLanguage = detectLanguage(event, state: state), !preferredLanguages.contains(noteLanguage) else {
return false
}
return true
}
}
+192 -256
View File
@@ -9,198 +9,160 @@ import Foundation
import Vault
import UIKit
func should_show_wallet_selector(_ pubkey: String) -> Bool {
return UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
}
let fallback_zap_amount = 1000
func pk_setting_key(_ pubkey: String, key: String) -> String {
return "\(pubkey)_\(key)"
}
func default_zap_setting_key(pubkey: String) -> String {
return pk_setting_key(pubkey, key: "default_zap_amount")
}
func set_default_zap_amount(pubkey: String, amount: Int) {
let key = default_zap_setting_key(pubkey: pubkey)
UserDefaults.standard.setValue(amount, forKey: key)
}
func get_default_zap_amount(pubkey: String) -> Int? {
let key = default_zap_setting_key(pubkey: pubkey)
let amt = UserDefaults.standard.integer(forKey: key)
if amt == 0 {
return nil
}
return amt
}
func should_disable_image_animation() -> Bool {
return (UserDefaults.standard.object(forKey: "disable_animation") as? Bool)
?? UIAccessibility.isReduceMotionEnabled
}
func get_default_wallet(_ pubkey: String) -> Wallet {
if let defaultWalletName = UserDefaults.standard.string(forKey: "default_wallet"),
let default_wallet = Wallet(rawValue: defaultWalletName)
{
return default_wallet
} else {
return .system_default_wallet
}
}
func get_media_uploader(_ pubkey: String) -> MediaUploader {
if let defaultMediaUploader = UserDefaults.standard.string(forKey: "default_media_uploader"),
let defaultMediaUploader = MediaUploader(rawValue: defaultMediaUploader) {
return defaultMediaUploader
} else {
return .nostrBuild
}
}
private func get_translation_service(_ pubkey: String) -> TranslationService? {
guard let translation_service = UserDefaults.standard.string(forKey: "translation_service") else {
return nil
}
return TranslationService(rawValue: translation_service)
}
private func get_deepl_plan(_ pubkey: String) -> DeepLPlan? {
guard let server_name = UserDefaults.standard.string(forKey: "deepl_plan") else {
return nil
}
return DeepLPlan(rawValue: server_name)
}
private func get_libretranslate_server(_ pubkey: String) -> LibreTranslateServer? {
guard let server_name = UserDefaults.standard.string(forKey: "libretranslate_server") else {
return nil
@propertyWrapper struct Setting<T: Equatable> {
private let key: String
private var value: T
init(key: String, default_value: T) {
self.key = pk_setting_key(UserSettingsStore.pubkey ?? "", key: key)
if let loaded = UserDefaults.standard.object(forKey: self.key) as? T {
self.value = loaded
} else if let loaded = UserDefaults.standard.object(forKey: key) as? T {
// try to load from deprecated non-pubkey-keyed setting
self.value = loaded
} else {
self.value = default_value
}
}
return LibreTranslateServer(rawValue: server_name)
var wrappedValue: T {
get { return value }
set {
guard self.value != newValue else {
return
}
self.value = newValue
UserDefaults.standard.set(newValue, forKey: key)
UserSettingsStore.shared!.objectWillChange.send()
}
}
}
private func get_libretranslate_url(_ pubkey: String, server: LibreTranslateServer) -> String? {
if let url = server.model.url {
return url
@propertyWrapper class StringSetting<T: StringCodable & Equatable> {
private let key: String
private var value: T
init(key: String, default_value: T) {
self.key = pk_setting_key(UserSettingsStore.pubkey ?? "", key: key)
if let loaded = UserDefaults.standard.string(forKey: self.key), let val = T.init(from: loaded) {
self.value = val
} else if let loaded = UserDefaults.standard.string(forKey: key), let val = T.init(from: loaded) {
// try to load from deprecated non-pubkey-keyed setting
self.value = val
} else {
self.value = default_value
}
}
return UserDefaults.standard.object(forKey: "libretranslate_url") as? String
var wrappedValue: T {
get { return value }
set {
guard self.value != newValue else {
return
}
self.value = newValue
UserDefaults.standard.set(newValue.to_string(), forKey: key)
UserSettingsStore.shared!.objectWillChange.send()
}
}
}
class UserSettingsStore: ObservableObject {
@Published var default_wallet: Wallet {
didSet {
UserDefaults.standard.set(default_wallet.rawValue, forKey: "default_wallet")
}
}
static var pubkey: String? = nil
static var shared: UserSettingsStore? = nil
@StringSetting(key: "default_wallet", default_value: .system_default_wallet)
var default_wallet: Wallet
@StringSetting(key: "default_media_uploader", default_value: .nostrBuild)
var default_media_uploader: MediaUploader
@Setting(key: "show_wallet_selector", default_value: true)
var show_wallet_selector: Bool
@Setting(key: "left_handed", default_value: false)
var left_handed: Bool
@Setting(key: "always_show_images", default_value: false)
var always_show_images: Bool
@Published var default_media_uploader: MediaUploader {
didSet {
UserDefaults.standard.set(default_media_uploader.rawValue, forKey: "default_media_uploader")
@Setting(key: "zap_vibration", default_value: true)
var zap_vibration: Bool
@Setting(key: "zap_notification", default_value: true)
var zap_notification: Bool
@Setting(key: "default_zap_amount", default_value: fallback_zap_amount)
var default_zap_amount: Int
@Setting(key: "mention_notification", default_value: true)
var mention_notification: Bool
@StringSetting(key: "zap_type", default_value: ZapType.pub)
var default_zap_type: ZapType
@Setting(key: "repost_notification", default_value: true)
var repost_notification: Bool
@Setting(key: "dm_notification", default_value: true)
var dm_notification: Bool
@Setting(key: "like_notification", default_value: true)
var like_notification: Bool
@Setting(key: "notification_only_from_following", default_value: false)
var notification_only_from_following: Bool
@Setting(key: "translate_dms", default_value: false)
var translate_dms: Bool
@Setting(key: "truncate_timeline_text", default_value: false)
var truncate_timeline_text: Bool
@Setting(key: "truncate_mention_text", default_value: true)
var truncate_mention_text: Bool
@Setting(key: "notification_indicators", default_value: NewEventsBits.all.rawValue)
var notification_indicators: Int
@Setting(key: "auto_translate", default_value: true)
var auto_translate: Bool
@Setting(key: "show_only_preferred_languages", default_value: false)
var show_only_preferred_languages: Bool
@Setting(key: "onlyzaps_mode", default_value: false)
var onlyzaps_mode: Bool
@Setting(key: "disable_animation", default_value: UIAccessibility.isReduceMotionEnabled)
var disable_animation: Bool
// Helper for inverse of disable_animation.
// disable_animation was introduced as a setting first, but it's more natural for the settings UI to show the inverse.
var enable_animation: Bool {
get {
!disable_animation
}
set {
disable_animation = !newValue
}
}
@Published var show_wallet_selector: Bool {
didSet {
UserDefaults.standard.set(show_wallet_selector, forKey: "show_wallet_selector")
}
}
@Published var left_handed: Bool {
didSet {
UserDefaults.standard.set(left_handed, forKey: "left_handed")
}
}
@StringSetting(key: "friend_filter", default_value: .all)
var friend_filter: FriendFilter
@Published var always_show_images: Bool {
didSet {
UserDefaults.standard.set(always_show_images, forKey: "always_show_images")
}
}
@StringSetting(key: "notification_state", default_value: .all)
var notification_state: NotificationFilterState
@Published var zap_vibration: Bool {
didSet {
UserDefaults.standard.set(zap_vibration, forKey: "zap_vibration")
}
}
@StringSetting(key: "translation_service", default_value: .none)
var translation_service: TranslationService
@Published var zap_notification: Bool {
didSet {
UserDefaults.standard.set(zap_notification, forKey: "zap_notification")
}
}
@Published var mention_notification: Bool {
didSet {
UserDefaults.standard.set(mention_notification, forKey: "mention_notification")
}
}
@Published var repost_notification: Bool {
didSet {
UserDefaults.standard.set(repost_notification, forKey: "repost_notification")
}
}
@Published var dm_notification: Bool {
didSet {
UserDefaults.standard.set(dm_notification, forKey: "dm_notification")
}
}
@Published var like_notification: Bool {
didSet {
UserDefaults.standard.set(like_notification, forKey: "like_notification")
}
}
@Published var notification_only_from_following: Bool {
didSet {
UserDefaults.standard.set(notification_only_from_following, forKey: "notification_only_from_following")
}
}
@Published var truncate_timeline_text: Bool {
didSet {
UserDefaults.standard.set(truncate_timeline_text, forKey: "truncate_timeline_text")
}
}
@StringSetting(key: "deepl_plan", default_value: .free)
var deepl_plan: DeepLPlan
@Published var truncate_mention_text: Bool {
didSet {
UserDefaults.standard.set(truncate_mention_text, forKey: "truncate_mention_text")
}
}
@Published var auto_translate: Bool {
didSet {
UserDefaults.standard.set(auto_translate, forKey: "auto_translate")
}
}
@Published var show_only_preferred_languages: Bool {
didSet {
UserDefaults.standard.set(show_only_preferred_languages, forKey: "show_only_preferred_languages")
}
}
@Published var translation_service: TranslationService {
didSet {
UserDefaults.standard.set(translation_service.rawValue, forKey: "translation_service")
}
}
@Published var deepl_plan: DeepLPlan {
didSet {
UserDefaults.standard.set(deepl_plan.rawValue, forKey: "deepl_plan")
}
}
@Published var deepl_api_key: String {
var deepl_api_key: String {
didSet {
do {
if deepl_api_key == "" {
@@ -214,31 +176,14 @@ class UserSettingsStore: ObservableObject {
}
}
@Published var libretranslate_server: LibreTranslateServer {
didSet {
if oldValue == libretranslate_server {
return
}
@StringSetting(key: "libretranslate_server", default_value: .terraprint)
var libretranslate_server: LibreTranslateServer
@Setting(key: "libretranslate_url", default_value: "")
var libretranslate_url: String
UserDefaults.standard.set(libretranslate_server.rawValue, forKey: "libretranslate_server")
libretranslate_api_key = ""
if libretranslate_server == .custom {
libretranslate_url = ""
} else {
libretranslate_url = libretranslate_server.model.url!
}
}
}
@Published var libretranslate_url: String {
didSet {
UserDefaults.standard.set(libretranslate_url, forKey: "libretranslate_url")
}
}
@Published var libretranslate_api_key: String {
@Setting(key: "libretranslate_api_key", default_value: "")
var libretranslate_api_key: String {
didSet {
do {
if libretranslate_api_key == "" {
@@ -252,73 +197,33 @@ class UserSettingsStore: ObservableObject {
}
}
@Published var disable_animation: Bool {
@Published var nokyctranslate_api_key: String {
didSet {
UserDefaults.standard.set(disable_animation, forKey: "disable_animation")
do {
if nokyctranslate_api_key == "" {
try clearNoKYCTranslateApiKey()
} else {
try saveNoKYCTranslateApiKey(nokyctranslate_api_key)
}
} catch {
// No-op.
}
}
}
}
init() {
// TODO: pubkey-scoped settings
let pubkey = ""
self.default_wallet = get_default_wallet(pubkey)
show_wallet_selector = should_show_wallet_selector(pubkey)
always_show_images = UserDefaults.standard.object(forKey: "always_show_images") as? Bool ?? false
default_media_uploader = get_media_uploader(pubkey)
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
zap_vibration = UserDefaults.standard.object(forKey: "zap_vibration") as? Bool ?? false
zap_notification = UserDefaults.standard.object(forKey: "zap_notification") as? Bool ?? true
mention_notification = UserDefaults.standard.object(forKey: "mention_notification") as? Bool ?? true
repost_notification = UserDefaults.standard.object(forKey: "repost_notification") as? Bool ?? true
like_notification = UserDefaults.standard.object(forKey: "like_notification") as? Bool ?? true
dm_notification = UserDefaults.standard.object(forKey: "dm_notification") as? Bool ?? true
notification_only_from_following = UserDefaults.standard.object(forKey: "notification_only_from_following") as? Bool ?? false
truncate_timeline_text = UserDefaults.standard.object(forKey: "truncate_timeline_text") as? Bool ?? false
truncate_mention_text = UserDefaults.standard.object(forKey: "truncate_mention_text") as? Bool ?? false
disable_animation = should_disable_image_animation()
auto_translate = UserDefaults.standard.object(forKey: "auto_translate") as? Bool ?? true
show_only_preferred_languages = UserDefaults.standard.object(forKey: "show_only_preferred_languages") as? Bool ?? false
// Note from @tyiu:
// Default translation service is disabled by default for now until we gain some confidence that it is working well in production.
// Instead of throwing all Damus users onto feature immediately, allow for discovery of feature organically.
// Also, we are connecting to servers listed as mirrors on the official LibreTranslate GitHub README that do not require API keys.
// However, we have not asked them for permission to use, so we're trying to be good neighbors for now.
// Opportunity: spin up dedicated trusted LibreTranslate server that requires an API key for any access (or higher rate limit access).
if let translation_service = get_translation_service(pubkey) {
self.translation_service = translation_service
} else {
self.translation_service = .none
}
if let libretranslate_server = get_libretranslate_server(pubkey) {
self.libretranslate_server = libretranslate_server
self.libretranslate_url = get_libretranslate_url(pubkey, server: libretranslate_server) ?? ""
} else {
// Choose a random server to distribute load.
libretranslate_server = .allCases.filter { $0 != .custom }.randomElement()!
libretranslate_url = ""
}
do {
libretranslate_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
} catch {
libretranslate_api_key = ""
}
if let deepl_plan = get_deepl_plan(pubkey) {
self.deepl_plan = deepl_plan
} else {
self.deepl_plan = .free
}
do {
deepl_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
} catch {
deepl_api_key = ""
}
do {
nokyctranslate_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusNoKYCTranslateKeychainConfiguration())
} catch {
nokyctranslate_api_key = ""
}
}
private func saveLibreTranslateApiKey(_ apiKey: String) throws {
@@ -329,6 +234,14 @@ class UserSettingsStore: ObservableObject {
try Vault.deletePrivateKey(keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
}
private func saveNoKYCTranslateApiKey(_ apiKey: String) throws {
try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusNoKYCTranslateKeychainConfiguration())
}
private func clearNoKYCTranslateApiKey() throws {
try Vault.deletePrivateKey(keychainConfiguration: DamusNoKYCTranslateKeychainConfiguration())
}
private func saveDeepLApiKey(_ apiKey: String) throws {
try Vault.savePrivateKey(apiKey, keychainConfiguration: DamusDeepLKeychainConfiguration())
}
@@ -336,6 +249,19 @@ class UserSettingsStore: ObservableObject {
private func clearDeepLApiKey() throws {
try Vault.deletePrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
}
func can_translate(_ pubkey: String) -> Bool {
switch translation_service {
case .none:
return false
case .libretranslate:
return URLComponents(string: libretranslate_url) != nil
case .deepl:
return deepl_api_key != ""
case .nokyctranslate:
return nokyctranslate_api_key != ""
}
}
}
struct DamusLibreTranslateKeychainConfiguration: KeychainConfiguration {
@@ -349,3 +275,13 @@ struct DamusDeepLKeychainConfiguration: KeychainConfiguration {
var accessGroup: String? = nil
var accountName = "deepl_apikey"
}
struct DamusNoKYCTranslateKeychainConfiguration: KeychainConfiguration {
var serviceName = "damus"
var accessGroup: String? = nil
var accountName = "nokyctranslate_apikey"
}
func pk_setting_key(_ pubkey: String, key: String) -> String {
return "\(pubkey)_\(key)"
}
+12 -1
View File
@@ -7,7 +7,7 @@
import Foundation
enum Wallet: String, CaseIterable, Identifiable {
enum Wallet: String, CaseIterable, Identifiable, StringCodable {
var id: String { self.rawValue }
struct Model: Identifiable, Hashable {
@@ -20,6 +20,17 @@ enum Wallet: String, CaseIterable, Identifiable {
var image: String
}
func to_string() -> String {
return rawValue
}
init?(from string: String) {
guard let w = Wallet(rawValue: string) else {
return nil
}
self = w
}
// New url prefixes needed to be added to LSApplicationQueriesSchemes
case system_default_wallet
case strike
+8 -8
View File
@@ -10,7 +10,6 @@ import Foundation
class ZapsModel: ObservableObject {
let state: DamusState
let target: ZapTarget
var zaps: [Zap]
let zaps_subid = UUID().description
let profiles_subid = UUID().description
@@ -18,11 +17,14 @@ class ZapsModel: ObservableObject {
init(state: DamusState, target: ZapTarget) {
self.state = state
self.target = target
self.zaps = []
}
var zaps: [Zap] {
return state.events.lookup_zaps(target: target)
}
func subscribe() {
var filter = NostrFilter.filter_kinds([9735])
var filter = NostrFilter.filter_kinds([NostrKind.zap.rawValue])
switch target {
case .profile(let profile_id):
filter.pubkeys = [profile_id]
@@ -51,7 +53,7 @@ class ZapsModel: ObservableObject {
case .notice:
break
case .eose:
let events = self.zaps.map { $0.request.ev }
let events = state.events.lookup_zaps(target: target).map { $0.request_ev }
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state)
case .event(_, let ev):
guard ev.kind == 9735 else {
@@ -59,7 +61,7 @@ class ZapsModel: ObservableObject {
}
if let zap = state.zaps.zaps[ev.id] {
if insert_uniq_sorted_zap_by_amount(zaps: &zaps, new_zap: zap) {
if state.events.store_zap(zap: zap) {
objectWillChange.send()
}
} else {
@@ -71,9 +73,7 @@ class ZapsModel: ObservableObject {
return
}
state.zaps.add_zap(zap: zap)
if insert_uniq_sorted_zap_by_amount(zaps: &zaps, new_zap: zap) {
if self.state.add_zap(zap: zap) {
objectWillChange.send()
}
}
+18 -5
View File
@@ -7,7 +7,7 @@
import Foundation
struct Profile: Codable {
class Profile: Codable {
var value: [String: AnyCodable]
init (name: String?, display_name: String?, about: String?, picture: String?, banner: String?, website: String?, lud06: String?, lud16: String?, nip05: String?) {
@@ -39,7 +39,7 @@ struct Profile: Codable {
return s
}
private mutating func set_val<T>(_ key: String, _ val: T?) {
private func set_val<T>(_ key: String, _ val: T?) {
if val == nil {
self.value.removeValue(forKey: key)
return
@@ -48,10 +48,15 @@ struct Profile: Codable {
self.value[key] = AnyCodable.init(val)
}
private mutating func set_str(_ key: String, _ val: String?) {
private func set_str(_ key: String, _ val: String?) {
set_val(key, val)
}
var reactions: Bool? {
get { return get_val("reactions"); }
set(s) { set_val("reactions", s) }
}
var deleted: Bool? {
get { return get_val("deleted"); }
set(s) { set_val("deleted", s) }
@@ -110,13 +115,21 @@ struct Profile: Codable {
}
}
private var _lnurl: String? = nil
var lnurl: String? {
if let _lnurl {
return _lnurl
}
guard let addr = lud16 ?? lud06 else {
return nil;
}
if addr.contains("@") {
return lnaddress_to_lnurl(addr);
// this is a heavy op and is used a lot in views, cache it!
let addr = lnaddress_to_lnurl(addr);
self._lnurl = addr
return addr
}
if !addr.lowercased().hasPrefix("lnurl") {
@@ -139,7 +152,7 @@ struct Profile: Codable {
self.value = [:]
}
init(from decoder: Decoder) throws {
required init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.value = try container.decode([String: AnyCodable].self)
}
+80 -22
View File
@@ -13,11 +13,15 @@ import CryptoKit
import NaturalLanguage
enum ValidationResult: Decodable {
case unknown
case ok
case bad_id
case bad_sig
var is_bad: Bool {
return self == .bad_id || self == .bad_sig
}
}
struct OtherEvent {
@@ -38,6 +42,18 @@ struct ReferencedId: Identifiable, Hashable, Equatable {
var id: String {
return ref_id
}
static func q(_ id: String, relay_id: String? = nil) -> ReferencedId {
return ReferencedId(ref_id: id, relay_id: relay_id, key: "q")
}
static func e(_ id: String, relay_id: String? = nil) -> ReferencedId {
return ReferencedId(ref_id: id, relay_id: relay_id, key: "e")
}
static func p(_ id: String, relay_id: String? = nil) -> ReferencedId {
return ReferencedId(ref_id: id, relay_id: relay_id, key: "p")
}
}
struct EventId: Identifiable, CustomStringConvertible {
@@ -82,7 +98,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
}
var too_big: Bool {
return self.content.count > 16000
return self.content.utf8.count > 16000
}
var should_show_event: Bool {
@@ -93,14 +109,6 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
return calculate_event_id(ev: self) == self.id
}
var is_valid: Bool {
return validity == .ok
}
lazy var validity: ValidationResult = {
return .ok //validate_event(ev: self)
}()
private var _blocks: [Block]? = nil
func blocks(_ privkey: String?) -> [Block] {
if let bs = _blocks {
@@ -115,14 +123,22 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
return parse_mentions(content: content, tags: self.tags)
}
lazy var inner_event: NostrEvent? = {
// don't try to deserialize an inner event if we know there won't be one
if self.known_kind == .boost {
return event_from_json(dat: self.content)
}
return nil
private lazy var inner_event: NostrEvent? = {
return event_from_json(dat: self.content)
}()
func get_inner_event(cache: EventCache) -> NostrEvent? {
guard self.known_kind == .boost else {
return nil
}
if self.content == "", let ref = self.referenced_ids.first {
return cache.lookup(ref.ref_id)
}
return self.inner_event
}
private var _event_refs: [EventRef]? = nil
func event_refs(_ privkey: String?) -> [EventRef] {
if let rs = _event_refs {
@@ -260,6 +276,25 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
return event_is_reply(self, privkey: privkey)
}
func note_language(_ privkey: String?) -> String? {
// Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in
// and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer.
let originalBlocks = blocks(privkey)
let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ")
// Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate.
let languageRecognizer = NLLanguageRecognizer()
languageRecognizer.processString(originalOnlyText)
guard let locale = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= 0.5 })?.key.rawValue else {
return nil
}
// Remove the variant component and just take the language part as translation services typically only supports the variant-less language.
// Moreover, speakers of one variant can generally understand other variants.
return localeToLanguage(locale)
}
public var referenced_ids: [ReferencedId] {
return get_referenced_ids(key: "e")
}
@@ -545,7 +580,7 @@ func make_first_contact_event(keypair: Keypair) -> NostrEvent? {
return ev
}
func make_metadata_event(keypair: Keypair, metadata: NostrMetadata) -> NostrEvent? {
func make_metadata_event(keypair: Keypair, metadata: Profile) -> NostrEvent? {
guard let privkey = keypair.privkey else {
return nil
}
@@ -671,7 +706,7 @@ func generate_private_keypair(our_privkey: String, id: String, created_at: Int64
func make_zap_request_event(keypair: FullKeypair, content: String, relays: [RelayDescriptor], target: ZapTarget, zap_type: ZapType) -> NostrEvent? {
var tags = zap_target_to_tags(target)
var relay_tag = ["relays"]
relay_tag.append(contentsOf: relays.map { $0.url.absoluteString })
relay_tag.append(contentsOf: relays.map { $0.url.id })
tags.append(relay_tag)
var kp = keypair
@@ -705,11 +740,34 @@ func make_zap_request_event(keypair: FullKeypair, content: String, relays: [Rela
return ev
}
func uniq<T: Hashable>(_ xs: [T]) -> [T] {
var s = Set<T>()
var ys: [T] = []
for x in xs {
if s.contains(x) {
continue
}
s.insert(x)
ys.append(x)
}
return ys
}
func gather_reply_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
var ids = get_referenced_ids(tags: from.tags, key: "e").first.map { [$0] } ?? []
ids.append(ReferencedId(ref_id: from.id, relay_id: nil, key: "e"))
ids.append(contentsOf: from.referenced_pubkeys.filter { $0.ref_id != our_pubkey })
ids.append(.e(from.id))
ids.append(contentsOf: uniq(from.referenced_pubkeys.filter { $0.ref_id != our_pubkey }))
if from.pubkey != our_pubkey {
ids.append(ReferencedId(ref_id: from.pubkey, relay_id: nil, key: "p"))
}
return ids
}
func gather_quote_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
var ids: [ReferencedId] = [.q(from.id)]
if from.pubkey != our_pubkey {
ids.append(ReferencedId(ref_id: from.pubkey, relay_id: nil, key: "p"))
}
@@ -960,8 +1018,8 @@ func last_etag(tags: [[String]]) -> String? {
return e
}
func inner_event_or_self(ev: NostrEvent) -> NostrEvent {
guard let inner_ev = ev.inner_event else {
func inner_event_or_self(ev: NostrEvent, cache: EventCache) -> NostrEvent {
guard let inner_ev = ev.get_inner_event(cache: cache) else {
return ev
}
+3 -3
View File
@@ -41,7 +41,7 @@ struct NostrFilter: Codable, Equatable {
}
public static var filter_text: NostrFilter {
return filter_kinds([1])
return filter_kinds([NostrKind.text.rawValue])
}
public static func filter_ids(_ ids: [String]) -> NostrFilter {
@@ -49,11 +49,11 @@ struct NostrFilter: Codable, Equatable {
}
public static var filter_profiles: NostrFilter {
return filter_kinds([0])
return filter_kinds([NostrKind.metadata.rawValue])
}
public static var filter_contacts: NostrFilter {
return filter_kinds([3])
return filter_kinds([NostrKind.contacts.rawValue])
}
public static func filter_authors(_ authors: [String]) -> NostrFilter {
-25
View File
@@ -1,25 +0,0 @@
//
// NostrMetadata.swift
// damus
//
// Created by William Casarin on 2022-05-21.
//
import Foundation
struct NostrMetadata: Codable {
let display_name: String?
let name: String?
let about: String?
let website: String?
let nip05: String?
let picture: String?
let banner: String?
let lud06: String?
let lud16: String?
}
func create_account_to_metadata(_ model: CreateAccountModel) -> NostrMetadata {
return NostrMetadata(display_name: model.real_name, name: model.nick_name, about: model.about, website: nil, nip05: nil, picture: model.profile_image, banner: nil, lud06: nil, lud16: nil)
}
+4 -6
View File
@@ -14,8 +14,8 @@ public struct RelayInfo: Codable {
static let rw = RelayInfo(read: true, write: true)
}
public struct RelayDescriptor: Codable {
public let url: URL
public struct RelayDescriptor {
public let url: RelayURL
public let info: RelayInfo
}
@@ -52,14 +52,12 @@ class Relay: Identifiable {
let descriptor: RelayDescriptor
let connection: RelayConnection
var last_pong: UInt32
var flags: Int
init(descriptor: RelayDescriptor, connection: RelayConnection) {
self.flags = 0
self.descriptor = descriptor
self.connection = connection
self.last_pong = 0
}
func mark_broken() {
@@ -81,6 +79,6 @@ enum RelayError: Error {
case RelayNotFound
}
func get_relay_id(_ url: URL) -> String {
return url.absoluteString
func get_relay_id(_ url: RelayURL) -> String {
return url.url.absoluteString
}
+90 -54
View File
@@ -5,42 +5,52 @@
// Created by William Casarin on 2022-04-02.
//
import Combine
import Foundation
import Starscream
enum NostrConnectionEvent {
case ws_event(WebSocketEvent)
case nostr_event(NostrResponse)
}
final class RelayConnection: WebSocketDelegate {
private(set) var isConnected = false
private(set) var isConnecting = false
private(set) var isReconnecting = false
public struct RelayURL: Hashable {
private(set) var url: URL
private(set) var last_connection_attempt: TimeInterval = 0
private lazy var socket = {
let req = URLRequest(url: url)
let socket = WebSocket(request: req, compressionHandler: .none)
socket.delegate = self
return socket
}()
private var handleEvent: (NostrConnectionEvent) -> ()
private let url: URL
init(url: URL, handleEvent: @escaping (NostrConnectionEvent) -> ()) {
self.url = url
self.handleEvent = handleEvent
var id: String {
return url.absoluteString
}
func reconnect() {
if isConnected {
isReconnecting = true
disconnect()
} else {
// we're already disconnected, so just connect
connect(force: true)
init?(_ str: String) {
guard let url = URL(string: str) else {
return nil
}
guard let scheme = url.scheme else {
return nil
}
guard scheme == "ws" || scheme == "wss" else {
return nil
}
self.url = url
}
}
final class RelayConnection {
private(set) var isConnected = false
private(set) var isConnecting = false
private(set) var last_connection_attempt: TimeInterval = 0
private lazy var socket = WebSocket(url.url)
private var subscriptionToken: AnyCancellable?
private var handleEvent: (NostrConnectionEvent) -> ()
private let url: RelayURL
init(url: RelayURL, handleEvent: @escaping (NostrConnectionEvent) -> ()) {
self.url = url
self.handleEvent = handleEvent
}
func connect(force: Bool = false) {
@@ -50,11 +60,27 @@ final class RelayConnection: WebSocketDelegate {
isConnecting = true
last_connection_attempt = Date().timeIntervalSince1970
subscriptionToken = socket.subject
.receive(on: DispatchQueue.main)
.sink { [weak self] completion in
switch completion {
case .failure(let error):
self?.receive(event: .error(error))
case .finished:
self?.receive(event: .disconnected(.normalClosure, nil))
}
} receiveValue: { [weak self] event in
self?.receive(event: event)
}
socket.connect()
}
func disconnect() {
socket.disconnect()
subscriptionToken = nil
isConnected = false
isConnecting = false
}
@@ -64,34 +90,46 @@ final class RelayConnection: WebSocketDelegate {
print("failed to encode nostr req: \(req)")
return
}
socket.write(string: req)
socket.send(.string(req))
}
// MARK: - WebSocketDelegate
func didReceive(event: WebSocketEvent, client: WebSocket) {
private func receive(event: WebSocketEvent) {
switch event {
case .connected:
self.isConnected = true
self.isConnecting = false
case .disconnected:
self.isConnecting = false
self.isConnected = false
if self.isReconnecting {
self.isReconnecting = false
self.connect()
case .message(let message):
self.receive(message: message)
case .disconnected(let closeCode, let reason):
if closeCode != .normalClosure {
print("⚠️ Warning: RelayConnection (\(self.url)) closed with code \(closeCode), reason: \(String(describing: reason))")
}
case .cancelled, .error:
self.isConnecting = false
self.isConnected = false
case .text(let txt):
if txt.count > 2000 {
isConnected = false
isConnecting = false
reconnect()
case .error(let error):
print("⚠️ Warning: RelayConnection (\(self.url)) error: \(error)")
isConnected = false
isConnecting = false
reconnect()
}
self.handleEvent(.ws_event(event))
}
func reconnect() {
guard !isConnecting else {
return // we're already trying to connect
}
disconnect()
connect()
}
private func receive(message: URLSessionWebSocketTask.Message) {
switch message {
case .string(let messageString):
if messageString.utf8.count > 2000 {
DispatchQueue.global(qos: .default).async {
if let ev = decode_nostr_event(txt: txt) {
if let ev = decode_nostr_event(txt: messageString) {
DispatchQueue.main.async {
self.handleEvent(.nostr_event(ev))
}
@@ -99,20 +137,18 @@ final class RelayConnection: WebSocketDelegate {
}
}
} else {
if let ev = decode_nostr_event(txt: txt) {
if let ev = decode_nostr_event(txt: messageString) {
handleEvent(.nostr_event(ev))
return
}
}
print("decode failed for \(txt)")
// TODO: trigger event error
default:
break
case .data(let messageData):
if let messageString = String(data: messageData, encoding: .utf8) {
receive(message: .string(messageString))
}
@unknown default:
print("An unexpected URLSessionWebSocketTask.Message was received.")
}
handleEvent(.ws_event(event))
}
}
+30 -20
View File
@@ -6,6 +6,7 @@
//
import Foundation
import Network
struct SubscriptionId: Identifiable, CustomStringConvertible {
let id: String
@@ -44,7 +45,24 @@ class RelayPool {
var request_queue: [QueuedRequest] = []
var seen: Set<String> = Set()
var counts: [String: UInt64] = [:]
private let network_monitor = NWPathMonitor()
private let network_monitor_queue = DispatchQueue(label: "io.damus.network_monitor")
private var last_network_status: NWPath.Status = .unsatisfied
init() {
network_monitor.pathUpdateHandler = { [weak self] path in
if (path.status == .satisfied || path.status == .requiresConnection) && self?.last_network_status != path.status {
DispatchQueue.main.async {
self?.connect_to_disconnected()
}
}
self?.last_network_status = path.status
}
network_monitor.start(queue: network_monitor_queue)
}
var descriptors: [RelayDescriptor] {
relays.map { $0.descriptor }
}
@@ -52,6 +70,10 @@ class RelayPool {
var num_connecting: Int {
return relays.reduce(0) { n, r in n + (r.connection.isConnecting ? 1 : 0) }
}
var num_connected: Int {
return relays.reduce(0) { n, r in n + (r.connection.isConnected ? 1 : 0) }
}
func remove_handler(sub_id: String) {
self.handlers = handlers.filter { $0.sub_id != sub_id }
@@ -84,7 +106,7 @@ class RelayPool {
}
}
func add_relay(_ url: URL, info: RelayInfo) throws {
func add_relay(_ url: RelayURL, info: RelayInfo) throws {
let relay_id = get_relay_id(url)
if get_relay(relay_id) != nil {
throw RelayError.RelayAlreadyExists
@@ -102,11 +124,11 @@ class RelayPool {
for relay in relays {
let c = relay.connection
let is_connecting = c.isReconnecting || c.isConnecting
let is_connecting = c.isConnecting
if is_connecting && (Date.now.timeIntervalSince1970 - c.last_connection_attempt) > 5 {
print("stale connection detected (\(relay.descriptor.url.absoluteString)). retrying...")
relay.connection.connect(force: true)
print("stale connection detected (\(relay.descriptor.url.url.absoluteString)). retrying...")
relay.connection.reconnect()
} else if relay.is_broken || is_connecting || c.isConnected {
continue
} else {
@@ -204,19 +226,6 @@ class RelayPool {
relays.first(where: { $0.id == id })
}
func record_last_pong(relay_id: String, event: NostrConnectionEvent) {
if case .ws_event(let ws_event) = event {
if case .pong = ws_event {
for relay in relays {
if relay.id == relay_id {
relay.last_pong = UInt32(Date.now.timeIntervalSince1970)
return
}
}
}
}
}
func run_queue(_ relay_id: String) {
self.request_queue = request_queue.reduce(into: Array<QueuedRequest>()) { (q, req) in
guard req.relay == relay_id else {
@@ -246,7 +255,6 @@ class RelayPool {
}
func handle_event(relay_id: String, event: NostrConnectionEvent) {
record_last_pong(relay_id: relay_id, event: event)
record_seen(relay_id: relay_id, event: event)
// run req queue when we reconnect
@@ -263,8 +271,10 @@ class RelayPool {
}
func add_rw_relay(_ pool: RelayPool, _ url: String) {
let url_ = URL(string: url)!
try? pool.add_relay(url_, info: RelayInfo.rw)
guard let url = RelayURL(url) else {
return
}
try? pool.add_relay(url, info: RelayInfo.rw)
}
+87
View File
@@ -0,0 +1,87 @@
//
// WebSocket.swift
// damus
//
// Created by Bryan Montz on 4/13/23.
//
import Combine
import Foundation
enum WebSocketEvent {
case connected
case message(URLSessionWebSocketTask.Message)
case disconnected(URLSessionWebSocketTask.CloseCode, String?)
case error(Error)
}
final class WebSocket: NSObject, URLSessionWebSocketDelegate {
private let url: URL
private let session: URLSession
private lazy var webSocketTask: URLSessionWebSocketTask = {
let task = session.webSocketTask(with: url)
task.delegate = self
return task
}()
let subject = PassthroughSubject<WebSocketEvent, Never>()
init(_ url: URL, session: URLSession = .shared) {
self.url = url
self.session = session
}
func connect() {
resume()
}
func disconnect(closeCode: URLSessionWebSocketTask.CloseCode = .normalClosure, reason: Data? = nil) {
webSocketTask.cancel(with: closeCode, reason: reason)
// reset after disconnecting to be ready for reconnecting
let task = session.webSocketTask(with: url)
task.delegate = self
webSocketTask = task
let reason_str: String?
if let reason {
reason_str = String(data: reason, encoding: .utf8)
} else {
reason_str = nil
}
subject.send(.disconnected(closeCode, reason_str))
}
func send(_ message: URLSessionWebSocketTask.Message) {
webSocketTask.send(message) { [weak self] error in
if let error {
self?.subject.send(.error(error))
}
}
}
private func resume() {
webSocketTask.receive { [weak self] result in
switch result {
case .success(let message):
self?.subject.send(.message(message))
self?.resume()
case .failure(let error):
self?.subject.send(.error(error))
}
}
webSocketTask.resume()
}
// MARK: - URLSessionWebSocketDelegate
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol theProtocol: String?) {
subject.send(.connected)
}
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
disconnect(closeCode: closeCode, reason: reason)
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ import Foundation
func created_deleted_account_profile(keypair: FullKeypair) -> NostrEvent {
var profile = Profile()
let profile = Profile()
profile.deleted = true
profile.about = "account deleted"
profile.name = "nobody"
+146
View File
@@ -0,0 +1,146 @@
import UIKit
extension UIImage {
public convenience init?(blurHash: String, size: CGSize, punch: Float = 1) {
guard blurHash.count >= 6 else { return nil }
let sizeFlag = String(blurHash[0]).decode83()
let numY = (sizeFlag / 9) + 1
let numX = (sizeFlag % 9) + 1
let quantisedMaximumValue = String(blurHash[1]).decode83()
let maximumValue = Float(quantisedMaximumValue + 1) / 166
guard blurHash.count == 4 + 2 * numX * numY else { return nil }
let colours: [(Float, Float, Float)] = (0 ..< numX * numY).map { i in
if i == 0 {
let value = String(blurHash[2 ..< 6]).decode83()
return decodeDC(value)
} else {
let value = String(blurHash[4 + i * 2 ..< 4 + i * 2 + 2]).decode83()
return decodeAC(value, maximumValue: maximumValue * punch)
}
}
let width = Int(size.width)
let height = Int(size.height)
let bytesPerRow = width * 3
guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) else { return nil }
CFDataSetLength(data, bytesPerRow * height)
guard let pixels = CFDataGetMutableBytePtr(data) else { return nil }
for y in 0 ..< height {
for x in 0 ..< width {
var r: Float = 0
var g: Float = 0
var b: Float = 0
for j in 0 ..< numY {
for i in 0 ..< numX {
let basis = cos(Float.pi * Float(x) * Float(i) / Float(width)) * cos(Float.pi * Float(y) * Float(j) / Float(height))
let colour = colours[i + j * numX]
r += colour.0 * basis
g += colour.1 * basis
b += colour.2 * basis
}
}
let intR = UInt8(linearTosRGB(r))
let intG = UInt8(linearTosRGB(g))
let intB = UInt8(linearTosRGB(b))
pixels[3 * x + 0 + y * bytesPerRow] = intR
pixels[3 * x + 1 + y * bytesPerRow] = intG
pixels[3 * x + 2 + y * bytesPerRow] = intB
}
}
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue)
guard let provider = CGDataProvider(data: data) else { return nil }
guard let cgImage = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 24, bytesPerRow: bytesPerRow,
space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: bitmapInfo, provider: provider, decode: nil, shouldInterpolate: true, intent: .defaultIntent) else { return nil }
self.init(cgImage: cgImage)
}
}
private func decodeDC(_ value: Int) -> (Float, Float, Float) {
let intR = value >> 16
let intG = (value >> 8) & 255
let intB = value & 255
return (sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB))
}
private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) {
let quantR = value / (19 * 19)
let quantG = (value / 19) % 19
let quantB = value % 19
let rgb = (
signPow((Float(quantR) - 9) / 9, 2) * maximumValue,
signPow((Float(quantG) - 9) / 9, 2) * maximumValue,
signPow((Float(quantB) - 9) / 9, 2) * maximumValue
)
return rgb
}
private func signPow(_ value: Float, _ exp: Float) -> Float {
return copysign(pow(abs(value), exp), value)
}
private func linearTosRGB(_ value: Float) -> Int {
let v = max(0, min(1, value))
if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) }
else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) }
}
private func sRGBToLinear<Type: BinaryInteger>(_ value: Type) -> Float {
let v = Float(Int64(value)) / 255
if v <= 0.04045 { return v / 12.92 }
else { return pow((v + 0.055) / 1.055, 2.4) }
}
private let encodeCharacters: [String] = {
return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) }
}()
private let decodeCharacters: [String: Int] = {
var dict: [String: Int] = [:]
for (index, character) in encodeCharacters.enumerated() {
dict[character] = index
}
return dict
}()
extension String {
func decode83() -> Int {
var value: Int = 0
for character in self {
if let digit = decodeCharacters[String(character)] {
value = value * 83 + digit
}
}
return value
}
}
private extension String {
subscript (offset: Int) -> Character {
return self[index(startIndex, offsetBy: offset)]
}
subscript (bounds: CountableClosedRange<Int>) -> Substring {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return self[start...end]
}
subscript (bounds: CountableRange<Int>) -> Substring {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return self[start..<end]
}
}
+145
View File
@@ -0,0 +1,145 @@
import UIKit
extension UIImage {
public func blurHash(numberOfComponents components: (Int, Int)) -> String? {
let pixelWidth = Int(round(size.width * scale))
let pixelHeight = Int(round(size.height * scale))
let context = CGContext(
data: nil,
width: pixelWidth,
height: pixelHeight,
bitsPerComponent: 8,
bytesPerRow: pixelWidth * 4,
space: CGColorSpace(name: CGColorSpace.sRGB)!,
bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue
)!
context.scaleBy(x: scale, y: -scale)
context.translateBy(x: 0, y: -size.height)
UIGraphicsPushContext(context)
draw(at: .zero)
UIGraphicsPopContext()
guard let cgImage = context.makeImage(),
let dataProvider = cgImage.dataProvider,
let data = dataProvider.data,
let pixels = CFDataGetBytePtr(data) else {
assertionFailure("Unexpected error!")
return nil
}
let width = cgImage.width
let height = cgImage.height
let bytesPerRow = cgImage.bytesPerRow
var factors: [(Float, Float, Float)] = []
for y in 0 ..< components.1 {
for x in 0 ..< components.0 {
let normalisation: Float = (x == 0 && y == 0) ? 1 : 2
let factor = multiplyBasisFunction(pixels: pixels, width: width, height: height, bytesPerRow: bytesPerRow, bytesPerPixel: cgImage.bitsPerPixel / 8, pixelOffset: 0) {
normalisation * cos(Float.pi * Float(x) * $0 / Float(width)) as Float * cos(Float.pi * Float(y) * $1 / Float(height)) as Float
}
factors.append(factor)
}
}
let dc = factors.first!
let ac = factors.dropFirst()
var hash = ""
let sizeFlag = (components.0 - 1) + (components.1 - 1) * 9
hash += sizeFlag.encode83(length: 1)
let maximumValue: Float
if ac.count > 0 {
let actualMaximumValue = ac.map({ max(abs($0.0), abs($0.1), abs($0.2)) }).max()!
let quantisedMaximumValue = Int(max(0, min(82, floor(actualMaximumValue * 166 - 0.5))))
maximumValue = Float(quantisedMaximumValue + 1) / 166
hash += quantisedMaximumValue.encode83(length: 1)
} else {
maximumValue = 1
hash += 0.encode83(length: 1)
}
hash += encodeDC(dc).encode83(length: 4)
for factor in ac {
hash += encodeAC(factor, maximumValue: maximumValue).encode83(length: 2)
}
return hash
}
private func multiplyBasisFunction(pixels: UnsafePointer<UInt8>, width: Int, height: Int, bytesPerRow: Int, bytesPerPixel: Int, pixelOffset: Int, basisFunction: (Float, Float) -> Float) -> (Float, Float, Float) {
var r: Float = 0
var g: Float = 0
var b: Float = 0
let buffer = UnsafeBufferPointer(start: pixels, count: height * bytesPerRow)
for x in 0 ..< width {
for y in 0 ..< height {
let basis = basisFunction(Float(x), Float(y))
r += basis * sRGBToLinear(buffer[bytesPerPixel * x + pixelOffset + 0 + y * bytesPerRow])
g += basis * sRGBToLinear(buffer[bytesPerPixel * x + pixelOffset + 1 + y * bytesPerRow])
b += basis * sRGBToLinear(buffer[bytesPerPixel * x + pixelOffset + 2 + y * bytesPerRow])
}
}
let scale = 1 / Float(width * height)
return (r * scale, g * scale, b * scale)
}
}
private func encodeDC(_ value: (Float, Float, Float)) -> Int {
let roundedR = linearTosRGB(value.0)
let roundedG = linearTosRGB(value.1)
let roundedB = linearTosRGB(value.2)
return (roundedR << 16) + (roundedG << 8) + roundedB
}
private func encodeAC(_ value: (Float, Float, Float), maximumValue: Float) -> Int {
let quantR = Int(max(0, min(18, floor(signPow(value.0 / maximumValue, 0.5) * 9 + 9.5))))
let quantG = Int(max(0, min(18, floor(signPow(value.1 / maximumValue, 0.5) * 9 + 9.5))))
let quantB = Int(max(0, min(18, floor(signPow(value.2 / maximumValue, 0.5) * 9 + 9.5))))
return quantR * 19 * 19 + quantG * 19 + quantB
}
private func signPow(_ value: Float, _ exp: Float) -> Float {
return copysign(pow(abs(value), exp), value)
}
private func linearTosRGB(_ value: Float) -> Int {
let v = max(0, min(1, value))
if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) }
else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) }
}
private func sRGBToLinear<Type: BinaryInteger>(_ value: Type) -> Float {
let v = Float(Int64(value)) / 255
if v <= 0.04045 { return v / 12.92 }
else { return pow((v + 0.055) / 1.055, 2.4) }
}
private let encodeCharacters: [String] = {
return "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".map { String($0) }
}()
extension BinaryInteger {
func encode83(length: Int) -> String {
var result = ""
for i in 1 ... length {
let digit = (Int(self) / pow(83, length - i)) % 83
result += encodeCharacters[Int(digit)]
}
return result
}
}
private func pow(_ base: Int, _ exponent: Int) -> Int {
return (0 ..< exponent).reduce(1) { value, _ in value * base }
}
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2018 Wolt Enterprises
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+45
View File
@@ -0,0 +1,45 @@
# BlurHash for iOS, in Swift
## Standalone decoder and encoder
[BlurHashDecode.swift](BlurHashDecode.swift) and [BlurHashEncode.swift](BlurHashEncode.swift) contain a decoder
and encoder for BlurHash to and from `UIImage`. Both files are completeiy standalone, and can simply be copied into your
project directly.
### Decoding
[BlurHashDecode.swift](BlurHashDecode.swift) implements the following extension on `UIImage`:
public convenience init?(blurHash: String, size: CGSize, punch: Float = 1)
This creates a UIImage containing the placeholder image decoded from the BlurHash string, or returns nil if decoding failed.
The parameters are:
* `blurHash` - A string containing the BlurHash.
* `size` - The requested output size. You should keep this small, and let UIKit scale it up for you. 32 pixels wide is plenty.
* `punch` - Adjusts the contrast of the output image. Tweak it if you want a different look for your placeholders.
### Encoding
[BlurHashEncode.swift](BlurHashEncode.swift) implements the following extension on `UIImage`:
public func blurHash(numberOfComponents components: (Int, Int)) -> String?
This returns a string containing the BlurHash for the image, or nil if the image was in a weird format that is not supported.
The parameters are:
* `numberOfComponents` - a Tuple of integers specifying the number of components in the X and Y directions. Both must be
between 1 and 9 inclusive, or the function will return nil. 3 to 5 is usually a good range.
## BlurHashKit
This is a more advanced library, currently in development. It will let you do more advanced operations using BlurHashes,
such testing whether various parts of an image are dark and light, or generating BlurHashes as gradients from corner colours.
It is currently not documented or finalised, but feel free to look into the different files and what they implement, or look at
how it is used by the test app.
## BlurHashTest.app
This is a simple test app that shows how to use the various pieces of BlurHash functionality, and lets you play with the
algorithm.
+44
View File
@@ -0,0 +1,44 @@
//
// CompatibleAttribute.swift
// damus
//
// Created by William Casarin on 2023-04-06.
//
import Foundation
import SwiftUI
class CompatibleText: Equatable {
var text: Text
var attributed: AttributedString
init() {
self.text = Text("")
self.attributed = AttributedString(stringLiteral: "")
}
init(stringLiteral: String) {
self.text = Text(stringLiteral)
self.attributed = AttributedString(stringLiteral: stringLiteral)
}
init(text: Text, attributed: AttributedString) {
self.text = text
self.attributed = attributed
}
init(attributed: AttributedString) {
self.text = Text(attributed)
self.attributed = attributed
}
static func == (lhs: CompatibleText, rhs: CompatibleText) -> Bool {
return lhs.attributed == rhs.attributed
}
static func +(lhs: CompatibleText, rhs: CompatibleText) -> CompatibleText {
let combinedText = lhs.text + rhs.text
let combinedAttributes = lhs.attributed + rhs.attributed
return CompatibleText(text: combinedText, attributed: combinedAttributes)
}
}
+95 -1
View File
@@ -9,10 +9,51 @@ import Combine
import Foundation
import UIKit
class ImageMetadataState {
var state: ImageMetaProcessState
var meta: ImageMetadata
init(state: ImageMetaProcessState, meta: ImageMetadata) {
self.state = state
self.meta = meta
}
}
enum ImageMetaProcessState {
case processing
case failed
case processed(UIImage)
var img: UIImage? {
switch self {
case .processed(let img):
return img
default:
return nil
}
}
}
class EventData: ObservableObject {
@Published var translations: TranslateStatus?
@Published var artifacts: NoteArtifacts?
@Published var zaps: [Zap]
var validated: ValidationResult
init(zaps: [Zap] = []) {
self.translations = nil
self.artifacts = nil
self.zaps = zaps
self.validated = .unknown
}
}
class EventCache {
private var events: [String: NostrEvent] = [:]
private var replies = ReplyMap()
private var cancellable: AnyCancellable?
private var image_metadata: [String: ImageMetadataState] = [:]
private var event_data: [String: EventData] = [:]
//private var thread_latest: [String: Int64]
@@ -24,13 +65,65 @@ class EventCache {
}
}
private func get_cache_data(_ evid: String) -> EventData {
guard let data = event_data[evid] else {
let data = EventData()
event_data[evid] = data
return data
}
return data
}
func is_event_valid(_ evid: String) -> ValidationResult {
return get_cache_data(evid).validated
}
func store_event_validation(evid: String, validated: ValidationResult) {
get_cache_data(evid).validated = validated
}
func store_translation_artifacts(evid: String, translated: TranslateStatus) {
get_cache_data(evid).translations = translated
}
func store_artifacts(evid: String, artifacts: NoteArtifacts) {
get_cache_data(evid).artifacts = artifacts
}
@discardableResult
func store_zap(zap: Zap) -> Bool {
var data = get_cache_data(zap.target.id)
return insert_uniq_sorted_zap_by_amount(zaps: &data.zaps, new_zap: zap)
}
func lookup_zaps(target: ZapTarget) -> [Zap] {
return get_cache_data(target.id).zaps
}
func store_img_metadata(url: URL, meta: ImageMetadataState) {
self.image_metadata[url.absoluteString.lowercased()] = meta
}
func lookup_artifacts(evid: String) -> NoteArtifacts? {
return get_cache_data(evid).artifacts
}
func lookup_img_metadata(url: URL) -> ImageMetadataState? {
return image_metadata[url.absoluteString.lowercased()]
}
func lookup_translated_artifacts(evid: String) -> TranslateStatus? {
return get_cache_data(evid).translations
}
func parent_events(event: NostrEvent) -> [NostrEvent] {
var parents: [NostrEvent] = []
var ev = event
while true {
guard let direct_reply = ev.direct_replies(nil).first else {
guard let direct_reply = ev.direct_replies(nil).last else {
break
}
@@ -87,6 +180,7 @@ class EventCache {
private func prune() {
events = [:]
event_data = [:]
replies.replies = [:]
}
}
+10 -2
View File
@@ -10,7 +10,7 @@ import Kingfisher
extension KFOptionSetter {
func imageContext(_ imageContext: ImageContext) -> Self {
func imageContext(_ imageContext: ImageContext, disable_animation: Bool) -> Self {
options.callbackQueue = .dispatch(.global(qos: .background))
options.processingQueue = .dispatch(.global(qos: .background))
options.downloader = CustomImageDownloader.shared
@@ -22,10 +22,18 @@ extension KFOptionSetter {
maxSize: imageContext.maxMebibyteSize(),
downsampleSize: imageContext.downsampleSize()
)
options.loadDiskFileSynchronously = false
options.backgroundDecode = true
options.cacheOriginalImage = true
options.scaleFactor = UIScreen.main.scale
options.onlyLoadFirstFrame = should_disable_image_animation()
options.onlyLoadFirstFrame = disable_animation
return self
}
func image_fade(duration: TimeInterval) -> Self {
options.transition = ImageTransition.fade(duration)
options.keepCurrentImageWhileLoading = false
return self
}
+70
View File
@@ -0,0 +1,70 @@
//
// Hashtags.swift
// damus
//
// Created by William Casarin on 2023-04-06.
//
import Foundation
import SwiftUI
struct CustomHashtag {
let name: String
let offset: CGFloat?
let color: Color?
init(name: String, color: Color? = nil, offset: CGFloat? = nil) {
self.name = name
self.color = color
self.offset = offset
}
static let coffee = CustomHashtag(name: "coffee", color: DamusColors.brown, offset: -1.0)
static let bitcoin = CustomHashtag(name: "bitcoin", color: Color.orange, offset: -3.0)
static let nostr = CustomHashtag(name: "nostr", color: DamusColors.purple, offset: -2.0)
static let plebchain = CustomHashtag(name: "plebchain", color: DamusColors.deepPurple, offset: -3.0)
static let zap = CustomHashtag(name: "zap", color: DamusColors.yellow, offset: -4.0)
}
let custom_hashtags: [String: CustomHashtag] = [
"bitcoin": CustomHashtag.bitcoin,
"btc": CustomHashtag.bitcoin,
"nostr": CustomHashtag.nostr,
"coffee": CustomHashtag.coffee,
"coffeechain": CustomHashtag.coffee,
"plebchain": CustomHashtag.plebchain,
"zap": CustomHashtag.zap,
"zaps": CustomHashtag.zap,
"zapathon": CustomHashtag.zap,
"onlyzaps": CustomHashtag.zap,
]
func hashtag_str(_ htag: String) -> CompatibleText {
var attributedString = AttributedString(stringLiteral: "#\(htag)")
attributedString.link = URL(string: "damus:t:\(htag)")
let lowertag = htag.lowercased()
var text = Text(attributedString)
if let custom_hashtag = custom_hashtags[lowertag] {
if let col = custom_hashtag.color {
attributedString.foregroundColor = col
}
let name = custom_hashtag.name
if let img = UIImage(named: "\(name)-hashtag") {
attributedString = attributedString + " "
attributed_string_attach_icon(&attributedString, img: img)
}
text = Text(attributedString)
let img = Image("\(name)-hashtag")
text = text + Text("\(img)").baselineOffset(custom_hashtag.offset ?? 0.0)
} else {
attributedString.foregroundColor = DamusColors.purple
}
return CompatibleText(text: text, attributed: attributedString)
}
+208
View File
@@ -0,0 +1,208 @@
//
// ImageMetadata.swift
// damus
//
// Created by William Casarin on 2023-04-25.
//
import Foundation
import UIKit
struct ImageMetaDim: Equatable, StringCodable {
init(width: Int, height: Int) {
self.width = width
self.height = height
}
init?(from string: String) {
guard let dim = parse_image_meta_dim(string) else {
return nil
}
self = dim
}
func to_string() -> String {
"\(width)x\(height)"
}
var size: CGSize {
return CGSize(width: CGFloat(self.width), height: CGFloat(self.height))
}
let width: Int
let height: Int
}
struct ProcessedImageMetadata {
let blurhash: UIImage?
let dim: ImageMetaDim?
}
struct ImageMetadata: Equatable {
let url: URL
let blurhash: String?
let dim: ImageMetaDim?
init(url: URL, blurhash: String? = nil, dim: ImageMetaDim? = nil) {
self.url = url
self.blurhash = blurhash
self.dim = dim
}
init?(tag: [String]) {
guard let meta = decode_image_metadata(tag) else {
return nil
}
self = meta
}
func to_tag() -> [String] {
return image_metadata_to_tag(self)
}
}
func process_blurhash(blurhash: String, size: CGSize?) async -> UIImage? {
let res = Task.init {
let size = get_blurhash_size(img_size: size ?? CGSize(width: 100.0, height: 100.0))
guard let img = UIImage.init(blurHash: blurhash, size: size) else {
let noimg: UIImage? = nil
return noimg
}
return img
}
return await res.value
}
func image_metadata_to_tag(_ meta: ImageMetadata) -> [String] {
var tags = ["imeta", "url \(meta.url.absoluteString)"]
if let blurhash = meta.blurhash {
tags.append("blurhash \(blurhash)")
}
if let dim = meta.dim {
tags.append("dim \(dim.to_string())")
}
return tags
}
func decode_image_metadata(_ parts: [String]) -> ImageMetadata? {
var url: URL? = nil
var blurhash: String? = nil
var dim: ImageMetaDim? = nil
for part in parts {
if part == "imeta" {
continue
}
let ps = part.split(separator: " ")
guard ps.count == 2 else {
return nil
}
let pname = ps[0]
let pval = ps[1]
if pname == "blurhash" {
blurhash = String(pval)
} else if pname == "dim" {
dim = parse_image_meta_dim(String(pval))
} else if pname == "url" {
url = URL(string: String(pval))
}
}
guard let url else {
return nil
}
return ImageMetadata(url: url, blurhash: blurhash, dim: dim)
}
func parse_image_meta_dim(_ pval: String) -> ImageMetaDim? {
let parts = pval.split(separator: "x")
guard parts.count == 2,
let width = Int(parts[0]),
let height = Int(parts[1]) else {
return nil
}
return ImageMetaDim(width: width, height: height)
}
extension UIImage {
func resized(to size: CGSize) -> UIImage {
return UIGraphicsImageRenderer(size: size).image { _ in
draw(in: CGRect(origin: .zero, size: size))
}
}
}
func get_blurhash_size(img_size: CGSize) -> CGSize {
return CGSize(width: 100.0, height: (100.0/img_size.width) * img_size.height)
}
func calculate_blurhash(img: UIImage) async -> String? {
guard img.size.height > 0 else {
return nil
}
let res = Task.init {
let bhs = get_blurhash_size(img_size: img.size)
let smaller = img.resized(to: bhs)
guard let blurhash = smaller.blurHash(numberOfComponents: (5,5)) else {
let meta: String? = nil
return meta
}
return blurhash
}
return await res.value
}
func calculate_image_metadata(url: URL, img: UIImage, blurhash: String) -> ImageMetadata {
let width = Int(img.size.width)
let height = Int(img.size.height)
let dim = ImageMetaDim(width: width, height: height)
return ImageMetadata(url: url, blurhash: blurhash, dim: dim)
}
func process_image_metadata(cache: EventCache, ev: NostrEvent) {
for tag in ev.tags {
guard tag.count >= 2 && tag[0] == "imeta" else {
continue
}
guard let meta = ImageMetadata(tag: tag) else {
continue
}
guard cache.lookup_img_metadata(url: meta.url) == nil else {
continue
}
let state = ImageMetadataState(state: .processing, meta: meta)
cache.store_img_metadata(url: meta.url, meta: state)
if let blurhash = meta.blurhash {
Task.init {
let img = await process_blurhash(blurhash: blurhash, size: meta.dim?.size)
DispatchQueue.main.async {
if let img {
state.state = .processed(img)
} else {
state.state = .failed
}
}
}
}
}
}
+47
View File
@@ -0,0 +1,47 @@
//
// LocalNotification.swift
// damus
//
// Created by William Casarin on 2023-04-15.
//
import Foundation
struct LossyLocalNotification {
let type: LocalNotificationType
let event_id: String
func to_user_info() -> [AnyHashable: Any] {
return [
"type": self.type.rawValue,
"evid": self.event_id
]
}
static func from_user_info(user_info: [AnyHashable: Any]) -> LossyLocalNotification {
let target_id = user_info["evid"] as! String
let typestr = user_info["type"] as! String
let type = LocalNotificationType(rawValue: typestr)!
return LossyLocalNotification(type: type, event_id: target_id)
}
}
struct LocalNotification {
let type: LocalNotificationType
let event: NostrEvent
let target: NostrEvent
let content: String
func to_lossy() -> LossyLocalNotification {
return LossyLocalNotification(type: self.type, event_id: self.target.id)
}
}
enum LocalNotificationType: String {
case dm
case like
case mention
case repost
case zap
}
-8
View File
@@ -22,14 +22,6 @@ func localizedStringFormat(key: String, locale: Locale?) -> String {
return bundle.localizedString(forKey: key, value: fallback, table: nil)
}
func currentLanguage() -> String {
if #available(iOS 16, *) {
return Locale.current.language.languageCode?.identifier ?? "en"
} else {
return Locale.current.languageCode ?? "en"
}
}
/**
Removes the variant part of a locale code so that it contains only the language code.
*/
+15 -3
View File
@@ -20,9 +20,6 @@ extension Notification.Name {
static var select_quote: Notification.Name {
return Notification.Name("select quote")
}
static var reply: Notification.Name {
return Notification.Name("reply")
}
static var profile_updated: Notification.Name {
return Notification.Name("profile_updated")
}
@@ -56,6 +53,9 @@ extension Notification.Name {
static var post: Notification.Name {
return Notification.Name("send post")
}
static var compose: Notification.Name {
return Notification.Name("compose")
}
static var boost: Notification.Name {
return Notification.Name("boost")
}
@@ -104,6 +104,18 @@ extension Notification.Name {
static var zapping: Notification.Name {
return Notification.Name("zapping")
}
static var mute_thread: Notification.Name {
return Notification.Name("mute_thread")
}
static var unmute_thread: Notification.Name {
return Notification.Name("unmute_thread")
}
static var local_notification: Notification.Name {
return Notification.Name("local_notification")
}
static var onlyzaps_mode: Notification.Name {
return Notification.Name("hide_reactions")
}
}
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {
+48
View File
@@ -0,0 +1,48 @@
//
// BinaryParser.swift
// damus
//
// Created by William Casarin on 2023-04-25.
//
import Foundation
class BinaryParser {
var pos: Int
var buf: [UInt8]
init(buf: [UInt8], pos: Int = 0) {
self.pos = pos
self.buf = buf
}
func read_byte() -> UInt8? {
guard pos < buf.count else {
return nil
}
let v = buf[pos]
pos += 1
return v
}
func read_bytes(_ n: Int) -> [UInt8]? {
guard pos + n < buf.count else {
return nil
}
let v = [UInt8](self.buf[pos...pos+n])
return v
}
func read_u16() -> UInt16? {
let start = self.pos
guard let b1 = read_byte(), let b2 = read_byte() else {
self.pos = start
return nil
}
return (UInt16(b1) << 8) | UInt16(b2)
}
}
+1 -3
View File
@@ -109,9 +109,7 @@ class PostBox {
return
}
let remaining = pool.descriptors.map {
$0.url.absoluteString
}
let remaining = pool.descriptors.map { $0.url.id }
let posted_ev = PostedEvent(event: event, remaining: remaining)
events[event.id] = posted_ev
+1 -1
View File
@@ -89,6 +89,6 @@ func load_relay_filters(_ pubkey: String) -> Set<RelayFilter>? {
func determine_to_relays(pool: RelayPool, filters: RelayFilters) -> [String] {
return pool.descriptors
.map { $0.url.absoluteString }
.map { $0.url.url.absoluteString }
.filter { !filters.is_filtered(timeline: .search, relay_id: $0) }
}
+13
View File
@@ -0,0 +1,13 @@
//
// StringCodable.swift
// damus
//
// Created by William Casarin on 2023-04-21.
//
import Foundation
protocol StringCodable {
init?(from string: String)
func to_string() -> String
}
+31 -31
View File
@@ -20,28 +20,20 @@ public struct Translator {
self.userSettingsStore = userSettingsStore
}
/**
Translates a string from source language to target language.
If the translation provider supports its own language detection, it may determine the source language by itself that could be
different from what is passed in as the sourceLanguage argument.
The source language that is actually used in the translation will be returned as part of the TranslationWithLanguage object.
If the translation was unable to be fetched for whatever reason, nil is returned.
*/
public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> TranslationWithLanguage? {
public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
switch userSettingsStore.translation_service {
case .libretranslate:
return try await translateWithLibreTranslate(text, from: sourceLanguage, to: targetLanguage)
case .nokyctranslate:
return try await translateWithNoKYCTranslate(text, from: sourceLanguage, to: targetLanguage)
case .deepl:
return try await translateWithDeepL(text, to: targetLanguage)
return try await translateWithDeepL(text, from: sourceLanguage, to: targetLanguage)
case .none:
return nil
return text
}
}
/**
Translates a string from sourceLanguage to targetLanguage using LibreTranslate. We do not rely on LibreTranslate's language detection API as it requires a separate API call. Instead, we rely on the passed in sourceLanguage argument.
*/
private func translateWithLibreTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> TranslationWithLanguage? {
private func translateWithLibreTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
let url = try makeURL(userSettingsStore.libretranslate_url, path: "/translate")
var request = URLRequest(url: url)
@@ -61,15 +53,10 @@ public struct Translator {
let translatedText: String
}
let response: Response = try await decodedData(for: request)
let translation = response.translatedText
return TranslationWithLanguage(translation: translation, language: targetLanguage)
return response.translatedText
}
/**
Translates a string to targetLanguage using DeepL. We do not accept a sourceLanguage as an argument as DeepL performs language detection within the translate API, its models are generally fairly accurate, and does not require a separate API call like LibreTranslate.
*/
private func translateWithDeepL(_ text: String, to targetLanguage: String) async throws -> TranslationWithLanguage? {
private func translateWithDeepL(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
if userSettingsStore.deepl_api_key == "" {
return nil
}
@@ -83,9 +70,10 @@ public struct Translator {
struct RequestBody: Encodable {
let text: [String]
let source_lang: String
let target_lang: String
}
let body = RequestBody(text: [text], target_lang: targetLanguage.uppercased())
let body = RequestBody(text: [text], source_lang: sourceLanguage.uppercased(), target_lang: targetLanguage.uppercased())
request.httpBody = try encoder.encode(body)
struct Response: Decodable {
@@ -97,13 +85,30 @@ public struct Translator {
}
let response: Response = try await decodedData(for: request)
return response.translations.map { $0.text }.joined(separator: " ")
}
private func translateWithNoKYCTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
let url = try makeURL("https://translate.nokyctranslate.com", path: "/translate")
if response.translations.isEmpty {
return nil
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
struct RequestBody: Encodable {
let q: String
let source: String
let target: String
let api_key: String?
}
let body = RequestBody(q: text, source: sourceLanguage, target: targetLanguage, api_key: userSettingsStore.nokyctranslate_api_key)
request.httpBody = try encoder.encode(body)
let translation = response.translations.map { $0.text }.joined(separator: " ")
return TranslationWithLanguage(translation: translation, language: response.translations.first!.detected_source_language)
struct Response: Decodable {
let translatedText: String
}
let response: Response = try await decodedData(for: request)
return response.translatedText
}
private func makeURL(_ baseUrl: String, path: String) throws -> URL {
@@ -124,11 +129,6 @@ public struct Translator {
}
}
public struct TranslationWithLanguage {
let translation: String
let language: String
}
private extension URLSession {
func data(for request: URLRequest) async throws -> Data {
var task: URLSessionDataTask?
+4
View File
@@ -52,6 +52,10 @@ struct Zap {
public let is_anon: Bool
public let private_request: NostrEvent?
var request_ev: NostrEvent {
return private_request ?? self.request.ev
}
public static func from_zap_event(zap_ev: NostrEvent, zapper: String, our_privkey: String?) -> Zap? {
/// Make sure that we only create a zap event if it is authorized by the profile or event
guard zapper == zap_ev.pubkey else {
+41
View File
@@ -0,0 +1,41 @@
//
// BigButton.swift
// damus
//
// Created by William Casarin on 2023-04-19.
//
import SwiftUI
struct BigButton: View {
let text: String
let action: () -> ()
@Environment(\.colorScheme) var colorScheme
init(_ text: String, action: @escaping () -> ()) {
self.text = text
self.action = action
}
var body: some View {
Button(action: {
action()
}) {
Text(text)
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .center)
.foregroundColor(colorScheme == .light ? DamusColors.black : DamusColors.white)
.overlay {
RoundedRectangle(cornerRadius: 24)
.stroke(colorScheme == .light ? DamusColors.mediumGrey : DamusColors.white, lineWidth: 1)
}
.padding(EdgeInsets(top: 10, leading: 50, bottom: 25, trailing: 50))
}
}
}
struct BigButton_Previews: PreviewProvider {
static var previews: some View {
BigButton("Cancel", action: {})
}
}
+51 -56
View File
@@ -8,53 +8,50 @@
import SwiftUI
import UIKit
enum ActionBarSheet: Identifiable {
case reply
var id: String {
switch self {
case .reply: return "reply"
}
}
}
struct EventActionBar: View {
let damus_state: DamusState
let event: NostrEvent
let test_lnurl: String?
let generator = UIImpactFeedbackGenerator(style: .medium)
// just used for previews
@State var sheet: ActionBarSheet? = nil
@State var show_share_sheet: Bool = false
@State var show_share_action: Bool = false
@State var show_repost_action: Bool = false
@ObservedObject var bar: ActionBarModel
@Environment(\.colorScheme) var colorScheme
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel? = nil, test_lnurl: String? = nil) {
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel? = nil) {
self.damus_state = damus_state
self.event = event
self.test_lnurl = test_lnurl
_bar = ObservedObject(wrappedValue: bar ?? make_actionbar_model(ev: event.id, damus: damus_state))
}
@Environment(\.colorScheme) var colorScheme
var lnurl: String? {
test_lnurl ?? damus_state.profiles.lookup(id: event.pubkey)?.lnurl
damus_state.profiles.lookup(id: event.pubkey)?.lnurl
}
var show_like: Bool {
if damus_state.settings.onlyzaps_mode {
return false
}
return true
}
var body: some View {
HStack {
if damus_state.keypair.privkey != nil {
HStack(spacing: 4) {
EventActionButton(img: "bubble.left", col: bar.replied ? Color.blue : Color.gray) {
notify(.reply, event)
EventActionButton(img: "bubble.left", col: bar.replied ? DamusColors.purple : Color.gray) {
notify(.compose, PostAction.replying_to(event))
}
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
Text(verbatim: "\(bar.replies > 0 ? "\(bar.replies)" : "")")
.font(.footnote.weight(.medium))
.foregroundColor(bar.replied ? Color.blue : Color.gray)
.foregroundColor(bar.replied ? DamusColors.purple : Color.gray)
}
}
Spacer()
@@ -64,7 +61,7 @@ struct EventActionBar: View {
if bar.boosted {
notify(.delete, bar.our_boost)
} else {
send_boost()
self.show_repost_action = true
}
}
.accessibilityLabel(NSLocalizedString("Boosts", comment: "Accessibility label for boosts button"))
@@ -72,22 +69,25 @@ struct EventActionBar: View {
.font(.footnote.weight(.medium))
.foregroundColor(bar.boosted ? Color.green : Color.gray)
}
Spacer()
HStack(spacing: 4) {
LikeButton(liked: bar.liked) {
if bar.liked {
notify(.delete, bar.our_like)
} else {
send_like()
if show_like {
Spacer()
HStack(spacing: 4) {
LikeButton(liked: bar.liked) {
if bar.liked {
notify(.delete, bar.our_like)
} else {
send_like()
}
}
Text(verbatim: "\(bar.likes > 0 ? "\(bar.likes)" : "")")
.font(.footnote.weight(.medium))
.nip05_colorized(gradient: bar.liked)
}
Text(verbatim: "\(bar.likes > 0 ? "\(bar.likes)" : "")")
.font(.footnote.weight(.medium))
.nip05_colorized(gradient: bar.liked)
}
if let lnurl = self.lnurl {
Spacer()
ZapButton(damus_state: damus_state, event: event, lnurl: lnurl, bar: bar)
@@ -99,26 +99,35 @@ struct EventActionBar: View {
}
.accessibilityLabel(NSLocalizedString("Share", comment: "Button to share a post"))
}
.sheet(isPresented: $show_share_action) {
.onAppear {
self.bar.update(damus: damus_state, evid: self.event.id)
}
.sheet(isPresented: $show_share_action, onDismiss: { self.show_share_action = false }) {
if #available(iOS 16.0, *) {
ShareAction(event: event, bookmarks: damus_state.bookmarks, show_share_sheet: $show_share_sheet, show_share_action: $show_share_action)
ShareAction(event: event, bookmarks: damus_state.bookmarks, show_share: $show_share_sheet)
.presentationDetents([.height(300)])
.presentationDragIndicator(.visible)
} else {
if let note_id = bech32_note_id(event.id) {
if let url = URL(string: "https://damus.io/" + note_id) {
ShareSheet(activityItems: [url])
}
}
ShareAction(event: event, bookmarks: damus_state.bookmarks, show_share: $show_share_sheet)
}
}
.sheet(isPresented: $show_share_sheet) {
.sheet(isPresented: $show_share_sheet, onDismiss: { self.show_share_sheet = false }) {
if let note_id = bech32_note_id(event.id) {
if let url = URL(string: "https://damus.io/" + note_id) {
ShareSheet(activityItems: [url])
}
}
}
.sheet(isPresented: $show_repost_action, onDismiss: { self.show_repost_action = false }) {
if #available(iOS 16.0, *) {
RepostAction(damus_state: self.damus_state, event: event)
.presentationDetents([.height(300)])
.presentationDragIndicator(.visible)
} else {
RepostAction(damus_state: self.damus_state, event: event)
}
}
.onReceive(handle_notify(.update_stats)) { n in
let target = n.object as! String
guard target == self.event.id else { return }
@@ -136,18 +145,6 @@ struct EventActionBar: View {
}
}
func send_boost() {
guard let privkey = self.damus_state.keypair.privkey else {
return
}
let boost = make_boost_event(pubkey: damus_state.keypair.pubkey, privkey: privkey, boosted: self.event)
self.bar.our_boost = boost
notify(.boost, boost)
}
func send_like() {
guard let privkey = damus_state.keypair.privkey else {
return
@@ -249,8 +246,6 @@ struct EventActionBar_Previews: PreviewProvider {
EventActionBar(damus_state: ds, event: ev, bar: extra_max_bar)
EventActionBar(damus_state: ds, event: ev, bar: mega_max_bar)
EventActionBar(damus_state: ds, event: ev, bar: zapbar, test_lnurl: "lnurl")
}
.padding(20)
}
+1 -1
View File
@@ -32,7 +32,7 @@ struct EventDetailBar: View {
.buttonStyle(PlainButtonStyle())
}
if bar.likes > 0 {
if bar.likes > 0 && !state.settings.onlyzaps_mode {
NavigationLink(destination: ReactionsView(damus_state: state, model: ReactionsModel(state: state, target: target))) {
let noun = Text(verbatim: "\(reactionsCountString(bar.likes))").foregroundColor(.gray)
Text("\(Text("\(bar.likes)").font(.body.bold())) \(noun)", comment: "Sentence 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'.")
+62
View File
@@ -0,0 +1,62 @@
//
// RepostAction.swift
// damus
//
// Created by William Casarin on 2023-04-19.
//
import SwiftUI
struct RepostAction: View {
let damus_state: DamusState
let event: NostrEvent
@Environment(\.dismiss) var dismiss
var body: some View {
VStack {
Text("Repost Note", comment: "Title text to indicate that the buttons below are meant to be used to repost a note to others.")
.padding()
.font(.system(size: 17, weight: .bold))
Spacer()
HStack(alignment: .top, spacing: 100) {
ShareActionButton(img: "arrow.2.squarepath", text: NSLocalizedString("Repost", comment: "Button to repost a note")) {
dismiss()
guard let privkey = self.damus_state.keypair.privkey else {
return
}
let boost = make_boost_event(pubkey: damus_state.keypair.pubkey, privkey: privkey, boosted: self.event)
damus_state.postbox.send(boost)
}
ShareActionButton(img: "quote.opening", text: NSLocalizedString("Quote", comment: "Button to compose a quoted note")) {
dismiss()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
notify(.compose, PostAction.quoting(self.event))
}
}
}
Spacer()
HStack {
BigButton(NSLocalizedString("Cancel", comment: "Button to cancel a repost.")) {
dismiss()
}
}
}
}
}
struct RepostAction_Previews: PreviewProvider {
static var previews: some View {
RepostAction(damus_state: test_damus_state(), event: test_event)
}
}
+14 -49
View File
@@ -12,25 +12,22 @@ struct ShareAction: View {
let bookmarks: BookmarksManager
@State private var isBookmarked: Bool = false
@Binding var show_share_sheet: Bool
@Binding var show_share_action: Bool
@Binding var show_share: Bool
@Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) var dismiss
init(event: NostrEvent, bookmarks: BookmarksManager, show_share_sheet: Binding<Bool>, show_share_action: Binding<Bool>) {
init(event: NostrEvent, bookmarks: BookmarksManager, show_share: Binding<Bool>) {
let bookmarked = bookmarks.isBookmarked(event)
self._isBookmarked = State(initialValue: bookmarked)
self.bookmarks = bookmarks
self.event = event
self._show_share_sheet = show_share_sheet
self._show_share_action = show_share_action
self._show_share = show_share
}
var body: some View {
let col = colorScheme == .light ? DamusColors.mediumGrey : DamusColors.white
VStack {
Text("Share Note", comment: "Title text to indicate that the buttons below are meant to be used to share a note with others.")
.padding()
@@ -40,28 +37,27 @@ struct ShareAction: View {
HStack(alignment: .top, spacing: 25) {
ShareActionButton(img: "link", text: NSLocalizedString("Copy Link", comment: "Button to copy link to note"), col: col) {
show_share_action = false
ShareActionButton(img: "link", text: NSLocalizedString("Copy Link", comment: "Button to copy link to note")) {
UIPasteboard.general.string = "https://damus.io/" + (bech32_note_id(event.id) ?? event.id)
}
let bookmarkImg = isBookmarked ? "bookmark.slash" : "bookmark"
let bookmarkTxt = isBookmarked ? NSLocalizedString("Remove Bookmark", comment: "Button text to remove bookmark from a note.") : NSLocalizedString("Add Bookmark", comment: "Button text to add bookmark to a note.")
let boomarkCol = isBookmarked ? Color(.red) : col
let boomarkCol = isBookmarked ? Color(.red) : nil
ShareActionButton(img: bookmarkImg, text: bookmarkTxt, col: boomarkCol) {
show_share_action = false
dismiss()
self.bookmarks.updateBookmark(event)
isBookmarked = self.bookmarks.isBookmarked(event)
}
ShareActionButton(img: "globe", text: NSLocalizedString("Broadcast", comment: "Button to broadcast note to all your relays"), col: col) {
show_share_action = false
ShareActionButton(img: "globe", text: NSLocalizedString("Broadcast", comment: "Button to broadcast note to all your relays")) {
dismiss()
NotificationCenter.default.post(name: .broadcast_event, object: event)
}
ShareActionButton(img: "square.and.arrow.up", text: NSLocalizedString("Share Via...", comment: "Button to present iOS share sheet"), col: col) {
show_share_action = false
show_share_sheet = true
ShareActionButton(img: "square.and.arrow.up", text: NSLocalizedString("Share Via...", comment: "Button to present iOS share sheet")) {
show_share = true
dismiss()
}
}
@@ -69,42 +65,11 @@ struct ShareAction: View {
Spacer()
HStack {
Button(action: {
show_share_action = false
}) {
Text(NSLocalizedString("Cancel", comment: "Button to cancel a repost."))
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .center)
.foregroundColor(colorScheme == .light ? DamusColors.black : DamusColors.white)
.overlay {
RoundedRectangle(cornerRadius: 24)
.stroke(colorScheme == .light ? DamusColors.mediumGrey : DamusColors.white, lineWidth: 1)
}
.padding(EdgeInsets(top: 10, leading: 50, bottom: 25, trailing: 50))
BigButton(NSLocalizedString("Cancel", comment: "Button to cancel a repost.")) {
dismiss()
}
}
}
}
}
func ShareActionButton(img: String, text: String, col: Color, action: @escaping () -> ()) -> some View {
Button(action: action) {
VStack() {
Image(systemName: img)
.foregroundColor(col)
.font(.system(size: 23, weight: .bold))
.overlay {
Circle()
.stroke(col, lineWidth: 1)
.frame(width: 55.0, height: 55.0)
}
.frame(height: 25)
Text(verbatim: text)
.foregroundColor(col)
.font(.footnote)
.multilineTextAlignment(.center)
.padding(.top)
}
}
}

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