Compare commits

..

141 Commits

Author SHA1 Message Date
tyiu e6bf6a6b8a Fix auto-translations bug where languages in preferred language still gets translated
Changelog-Fixed: Fix auto-translations bug where languages in preferred language still gets translated
2023-05-02 14:28:21 -04:00
William Casarin 91fc0039eb build 19 2023-05-02 08:23:58 -07:00
William Casarin 8eb013f1f7 Search hashtags automatically
Changelog-Changed: Search hashtags automatically
2023-05-02 08:22:36 -07:00
William Casarin 4f459d128a Load profiles in hashtag searches
Changelog-Fixed: Load profiles in hashtag searched
2023-05-02 07:53:06 -07:00
William Casarin 58e53631c6 Use cached note language in search model
We should never call event.note_language on the main thread
2023-05-02 07:53:06 -07:00
William Casarin 7b2e178f5b Fix warning 2023-05-02 07:53:06 -07:00
William Casarin da99130b78 build 18 2023-05-02 07:51:29 -07:00
William Casarin b79d361016 Preload profile pictures while scrolling
Changelog-Added: Preload profile pictures while scrolling
2023-05-02 07:33:54 -07:00
William Casarin 4889c0a7d9 Fix weird #\[0] artifacts appearing in posts and translated from english bugs
This changes the preloader to load things right away and fixes a bunch
of bugs

Changelog-Fixed: Fix weird #\[0] artifacts appearing in posts
Changelog-Fixed: Fix "translated from english" bugs
2023-05-02 06:48:02 -07:00
William Casarin 61c9732acd Fix crash when loading DMs in the background
Changelog-Fixed: Fix crash when loading DMs in the background
2023-05-02 06:48:02 -07:00
William Casarin ee6c080af8 Fix blurhash appearing behind loaded images when swiping on carousel
Changelog-Fixed: Fixed blurhash appearing behind loaded images when swiping on carousel
2023-05-02 06:48:02 -07:00
William Casarin a18ba86157 refactor: remove redundant rectangle in ImageCarousel 2023-05-02 06:48:02 -07:00
Bryan Montz 03931ef70e Save keys when logging in and when creating new keypair
Changelog-Added: Save keys when logging in and when creating new keypair
Closes: #1042
2023-05-02 06:47:29 -07:00
Swift 3284832eb0 Fix camera not dismissing
Changelog-Fixed: Fix camera not dismissing
Closes: #964
2023-05-01 15:39:09 -07:00
Bryan Montz c679be9644 Top-level tab state restoration
Changelog-Added: Top-level tab state restoration
Closes: #634
2023-05-01 15:22:54 -07:00
tyiu 9f701a7d44 Fix bug with reaction notifications referencing the wrong event
Changelog-Fixed: Fix bug with reaction notifications referencing the wrong event
Closes: #1047
2023-05-01 15:21:01 -07:00
Bryan Montz cb11087034 Fix Copy Link action does not dismiss ShareAction view
Changelog-Fixed: Fix Copy Link action does not dismiss ShareAction view
Closes: #1049
2023-05-01 15:20:10 -07:00
Ben Weeks 49b7aee74e Save Jack's soul
Very minor amendment to stop the bouncing of the bolt symbol as you click it.

Changelog-Fixed: Saved Jack's soul.
Closes: #1060
2023-05-01 15:19:26 -07:00
William Casarin 5b97906138 build 17 2023-04-30 22:30:25 -07:00
William Casarin c74d3e4938 fix some translation bugs 2023-04-30 22:04:33 -07:00
William Casarin df6911f9cb cache note language 2023-04-30 21:40:11 -07:00
William Casarin 1ca0519e25 Event Preloading
Changelog-Added: Added event preloading when scrolling
Changelog-Added: Preload images so they don't pop in
Changelog-Fixed: Fixed preview elements popping in
Changelog-Changed: Cached various UI elements so its not as laggy
Changelog-Fixed: Fixed glitchy preview
2023-04-30 20:06:38 -07:00
William Casarin c87f19b479 v1.4.3-15 changelog 2023-04-29 08:46:43 -07: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
115 changed files with 2993 additions and 1189 deletions
+54 -1
View File
@@ -1,3 +1,57 @@
## [1.4.3-15] - 2023-04-29
### Added
- Add q tag to quoted renotes (William Casarin)
- Add confirmation alert when clearing all bookmarks (Swift)
- Show blurhash placeholders from image metadata (William Casarin)
- Add image metadata to image uploads (William Casarin)
### Changed
- Load zaps instantly on events (William Casarin)
### Fixed
- Fix thread incompatibility for clients that add more than one reply tag (amethyst, plebstr)
- Preserve order of bookmarks when saving (William Casarin)
- Fix crash when you have invalid relays in your relay list (William Casarin)
[1.4.3-14]: https://github.com/damus-io/damus/releases/tag/v1.4.3-14
## [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
@@ -1038,4 +1092,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.
+74 -23
View File
@@ -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 */; };
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 */; };
4C198DF829F89323004C165C /* BinaryParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C198DF729F89323004C165C /* BinaryParser.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 */; };
@@ -155,6 +160,7 @@
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 */; };
@@ -198,6 +204,9 @@
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 */; };
@@ -212,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 */; };
@@ -249,7 +257,9 @@
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 */; };
50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B5685229F97CB400A23243 /* CredentialHandler.swift */; };
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; };
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
@@ -413,6 +423,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>"; };
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>"; };
4C198DF729F89323004C165C /* BinaryParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryParser.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>"; };
@@ -526,7 +542,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>"; };
@@ -564,6 +579,7 @@
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>"; };
@@ -607,6 +623,9 @@
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>"; };
@@ -661,7 +680,9 @@
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>"; };
50B5685229F97CB400A23243 /* CredentialHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialHandler.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>"; };
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
@@ -700,7 +721,6 @@
files = (
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
6C7DE41F2955169800E66263 /* Vault in Frameworks */,
4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */,
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -829,7 +849,6 @@
4C5F9117283D88E40052CD1C /* FollowingModel.swift */,
4C987B56283FD07F0042CE38 /* FollowersModel.swift */,
4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */,
4C649843285A952100EAE2B3 /* LocalUserConfig.swift */,
4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */,
4C216F372871EDE300040376 /* DirectMessageModel.swift */,
4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */,
@@ -850,6 +869,33 @@
path = Models;
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>";
};
4C198DF629F89317004C165C /* Parser */ = {
isa = PBXGroup;
children = (
4C198DF729F89323004C165C /* BinaryParser.swift */,
);
path = Parser;
sourceTree = "<group>";
};
4C1A9A1B29DDCF8B00516EAC /* Settings */ = {
isa = PBXGroup;
children = (
@@ -965,6 +1011,7 @@
4CACA9DB280C38C000D9BBE8 /* Profiles.swift */,
4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */,
4C363A8F28247A1D006E126D /* NostrLink.swift */,
50088DA029E8271A008A1FDF /* WebSocket.swift */,
);
path = Nostr;
sourceTree = "<group>";
@@ -972,6 +1019,9 @@
4C7FF7D628233637009601DB /* Util */ = {
isa = PBXGroup;
children = (
4C198DF629F89317004C165C /* Parser */,
4C198DF329F88D23004C165C /* Images */,
4C198DEA29F88C6B004C165C /* BlurHash */,
4CE4F0F329D779B5005914DB /* PostBox.swift */,
7C0F392D29B57C8F0039859C /* Extensions */,
4CE879492995B58700F758CC /* Relays */,
@@ -1011,6 +1061,7 @@
4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */,
4CDA128B29EB19C40006FA5A /* LocalNotification.swift */,
4CA5588229F33F5B00DC6A45 /* StringCodable.swift */,
50B5685229F97CB400A23243 /* CredentialHandler.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -1045,6 +1096,9 @@
4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */,
4CB88388296AF99A00DC99E7 /* EventDetailBar.swift */,
5CF72FC129B9142F00124A13 /* ShareAction.swift */,
4CE1398F29F0661A00AC6A0B /* RepostAction.swift */,
4CE1399129F0666100AC6A0B /* ShareActionButton.swift */,
4CE1399329F0669900AC6A0B /* BigButton.swift */,
);
path = ActionBar;
sourceTree = "<group>";
@@ -1253,6 +1307,7 @@
children = (
4CE879572996C45300F758CC /* ZapsView.swift */,
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */,
4CA3FA0F29F593D000FDB3C3 /* ZapTypePicker.swift */,
);
path = Zaps;
sourceTree = "<group>";
@@ -1336,7 +1391,6 @@
);
name = damus;
packageProductDependencies = (
4CE6DF1127F7A2B300C66700 /* Starscream */,
4C649880286E0EE300EAE2B3 /* secp256k1 */,
4C06670328FC7EC500038D2A /* Kingfisher */,
6C7DE41E2955169800E66263 /* Vault */,
@@ -1442,7 +1496,6 @@
);
mainGroup = 4CE6DEDA27F7A08100C66700;
packageReferences = (
4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */,
4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */,
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */,
6C7DE41D2955169800E66263 /* XCRemoteSwiftPackageReference "Vault" */,
@@ -1467,6 +1520,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;
@@ -1531,6 +1586,7 @@
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 */,
@@ -1561,7 +1617,6 @@
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 */,
@@ -1573,6 +1628,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 */,
@@ -1593,6 +1649,7 @@
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 */,
@@ -1636,16 +1693,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 */,
@@ -1662,6 +1724,7 @@
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 */,
@@ -1701,6 +1764,7 @@
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
4C1A9A1A29DCA17E00516EAC /* ReplyCounter.swift in Sources */,
50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */,
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */,
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */,
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
@@ -2024,7 +2088,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 19;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -2071,7 +2135,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 19;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -2243,14 +2307,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";
@@ -2272,11 +2328,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",
+90 -47
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,41 +91,70 @@ struct ImageCarousel: View {
}
var height: CGFloat {
image_fill?.height ?? 100
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 {
TabView {
ForEach(urls, id: \.absoluteString) { url in
Rectangle()
.foregroundColor(Color.clear)
.overlay {
GeometryReader { geo in
KFAnimatedImage(url)
.callbackQueue(.dispatch(.global(qos:.background)))
.backgroundDecode(true)
.imageContext(.note)
.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)
image_fill = fill
}
.aspectRatio(contentMode: filling ? .fill : .fit)
.tabItem {
Text(url.absoluteString)
}
.id(url.absoluteString)
GeometryReader { geo in
KFAnimatedImage(url)
.callbackQueue(.dispatch(.global(qos:.background)))
.backgroundDecode(true)
.imageContext(.note, disable_animation: state.settings.disable_animation)
.image_fade(duration: 0.25)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
}
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in
state.previews.cache_image_meta(evid: evid, image_fill: fill)
// blur hash can be discarded when we have the url
// NOTE: this is the wrong place for this... we need to remove
// it when the image is loaded in memory. This may happen
// earlier than this (by the preloader, etc)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
state.events.lookup_img_metadata(url: url)?.state = .not_needed
}
image_fill = fill
}
.background {
Placeholder(url: url, geo_size: geo.size)
}
.aspectRatio(contentMode: filling ? .fill : .fit)
.tabItem {
Text(url.absoluteString)
}
.id(url.absoluteString)
}
}
}
.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
}
@@ -134,25 +188,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
@@ -169,7 +212,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)
}
}
+59 -91
View File
@@ -16,7 +16,6 @@ struct Translated: Equatable {
enum TranslateStatus: Equatable {
case havent_tried
case trying
case translating
case translated(Translated)
case not_needed
@@ -26,40 +25,19 @@ struct TranslateView: View {
let damus_state: DamusState
let event: NostrEvent
let size: EventViewKind
let currentLanguage: String
@State var translated: TranslateStatus
@ObservedObject var translations_model: TranslationModel
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind) {
self.damus_state = damus_state
self.event = event
self.size = size
if #available(iOS 16, *) {
self.currentLanguage = Locale.current.language.languageCode?.identifier ?? "en"
} else {
self.currentLanguage = Locale.current.languageCode ?? "en"
}
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)
}
self._translations_model = ObservedObject(wrappedValue: damus_state.events.get_cache_data(event.id).translations_model)
}
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
var TranslateButton: some View {
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
self.translated = .trying
translate()
}
.translate_button_style()
}
@@ -80,78 +58,43 @@ struct TranslateView: View {
}
}
func failed_attempt() {
DispatchQueue.main.async {
self.translated = .not_needed
damus_state.events.store_translation_artifacts(evid: event.id, translated: .not_needed)
func translate() {
Task {
guard let note_language = translations_model.note_language else {
return
}
let res = await translate_note(profiles: damus_state.profiles, privkey: damus_state.keypair.privkey, event: event, settings: damus_state.settings, note_lang: note_language)
DispatchQueue.main.async {
self.translations_model.state = res
}
}
}
func attempt_translation() async {
guard case .trying = translated else {
func attempt_translation() {
guard should_translate(event: event, our_keypair: damus_state.keypair, settings: damus_state.settings, note_lang: self.translations_model.note_language), damus_state.settings.auto_translate 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
}
// 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)
}
translate()
}
func should_transl(_ note_lang: String) -> Bool {
should_translate(event: event, our_keypair: damus_state.keypair, settings: damus_state.settings, note_lang: note_lang)
}
var body: some View {
Group {
switch translated {
switch self.translations_model.state {
case .havent_tried:
if damus_state.settings.auto_translate {
Text("")
} else if let note_lang = translations_model.note_language, should_transl(note_lang) {
TranslateButton
} else {
TranslateButton
Text("")
}
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)
Text("")
case .translated(let translated):
let languageName = Locale.current.localizedString(forLanguageCode: translated.language)
TranslatedView(lang: languageName, artifacts: translated.artifacts)
@@ -159,17 +102,8 @@ struct TranslateView: View {
Text("")
}
}
.onChange(of: translated) { val in
guard case .trying = translated else {
return
}
Task {
await attempt_translation()
}
}
.task {
await attempt_translation()
attempt_translation()
}
}
}
@@ -189,3 +123,37 @@ struct TranslateView_Previews: PreviewProvider {
TranslateView(damus_state: ds, event: test_event, size: .normal)
}
}
func translate_note(profiles: Profiles, privkey: String?, event: NostrEvent, settings: UserSettingsStore, note_lang: String) async -> TranslateStatus {
// If the note language is different from our preferred languages, send a translation request.
let translator = Translator(settings)
let originalContent = event.get_content(privkey)
let translated_note = try? await translator.translate(originalContent, from: note_lang, to: current_language())
guard let translated_note else {
// if its the same, give up and don't retry
return .not_needed
}
guard originalContent != translated_note else {
// if its the same, give up and don't retry
return .not_needed
}
// Render translated note
let translated_blocks = event.get_blocks(content: translated_note)
let artifacts = render_blocks(blocks: translated_blocks, profiles: profiles, privkey: privkey)
// and cache it
return .translated(Translated(artifacts: artifacts, language: note_lang))
}
func current_language() -> String {
if #available(iOS 16, *) {
return Locale.current.language.languageCode?.identifier ?? "en"
} else {
return Locale.current.languageCode ?? "en"
}
}
+1 -1
View File
@@ -37,7 +37,7 @@ struct UserView: View {
VStack {
HStack {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey)
+7 -6
View File
@@ -47,7 +47,7 @@ struct ZapButton: View {
return "bolt"
}
return "bolt.horizontal.fill"
return "bolt.fill"
}
var zap_color: Color? {
@@ -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)
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 {
+9 -80
View File
@@ -6,7 +6,6 @@
//
import SwiftUI
import Starscream
struct TimestampedProfile {
let profile: Profile
@@ -63,7 +62,7 @@ struct ContentView: View {
@State var status: String = "Not connected"
@State var active_sheet: Sheets? = nil
@State var damus_state: DamusState? = nil
@State var selected_timeline: Timeline? = .home
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home
@State var is_deleted_account: Bool = false
@State var is_profile_open: Bool = false
@State var event: NostrEvent? = nil
@@ -77,11 +76,9 @@ 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
@SceneStorage("ContentView.filter_state") var filter_state : FilterState = .posts_and_replies
@State private var isSideBarOpened = false
@StateObject var home: HomeModel = HomeModel()
@State var shouldShowBoostAlert = false
// connect retry timer
let timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect()
@@ -183,9 +180,6 @@ struct ContentView: View {
case .dms:
DirectMessagesView(damus_state: damus_state!, model: damus_state!.dms, settings: damus_state!.settings)
case .none:
EmptyView()
}
}
.navigationBarTitle(timeline_name(selected_timeline), displayMode: .inline)
@@ -212,7 +206,7 @@ struct ContentView: View {
var MaybeSearchView: some View {
Group {
if let search = self.active_search {
SearchView(appstate: damus_state!, search: SearchModel(contacts: damus_state!.contacts, pool: damus_state!.pool, search: search))
SearchView(appstate: damus_state!, search: SearchModel(state: damus_state!, search: search))
} else {
EmptyView()
}
@@ -261,7 +255,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)
}
@@ -314,7 +308,7 @@ struct ContentView: View {
case .event:
EventDetailView()
case .filter:
let timeline = selected_timeline ?? .home
let timeline = selected_timeline
if #available(iOS 16.0, *) {
RelayFilterView(state: damus_state!, timeline: timeline)
.presentationDetents([.height(550)])
@@ -349,19 +343,9 @@ struct ContentView: View {
}
}
.onReceive(handle_notify(.boost)) { notif in
guard let ev = notif.object as? NostrEvent else {
return
}
current_boost = ev
shouldShowBoostAlert = true
}
.onReceive(handle_notify(.reply)) { notif in
let ev = notif.object as! NostrEvent
self.active_sheet = .post(.replying_to(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
@@ -605,36 +589,6 @@ struct ContentView: View {
Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.")
}
})
.confirmationDialog("Repost", isPresented: $shouldShowBoostAlert) {
Button(NSLocalizedString("Repost", comment: "Title of alert for confirming to repost a post.")) {
guard let current_boost else {
return
}
guard let privkey = self.damus_state?.keypair.privkey else {
return
}
guard let damus_state else {
return
}
let boost = make_boost_event(pubkey: damus_state.keypair.pubkey, privkey: privkey, boosted: current_boost)
damus_state.postbox.send(boost)
}
Button(NSLocalizedString("Quote", comment: "Title of alert for confirming to make a quoted post.")) {
guard let current_boost else {
return
}
self.active_sheet = .post(.quoting(current_boost))
}
}
.onChange(of: shouldShowBoostAlert) { v in
if v == false {
self.current_boost = nil
}
}
}
func switch_timeline(_ timeline: Timeline) {
@@ -672,7 +626,7 @@ 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)
}
}
@@ -728,31 +682,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
+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
+1 -1
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)
+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
}
}
+8
View File
@@ -31,6 +31,14 @@ struct DamusState {
let replies: ReplyCounter
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
}
+46 -28
View File
@@ -128,7 +128,7 @@ 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
@@ -229,16 +229,22 @@ 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 validate_event(ev: inner_ev) == .ok else {
return
Task.init {
guard validate_event(ev: inner_ev) == .ok else {
return
}
if inner_ev.is_textlike {
DispatchQueue.main.async {
self.handle_text_event(sub_id: sub_id, ev)
}
}
}
if inner_ev.is_textlike {
handle_text_event(sub_id: sub_id, ev)
}
}
guard let e = boost_ev_id else {
@@ -271,8 +277,8 @@ class HomeModel: ObservableObject {
case .success(let n):
handle_notification(ev: ev)
let liked = Counted(event: ev, id: e.ref_id, total: n)
notify(.liked, liked)
notify(.update_stats, e.ref_id)
//notify(.liked, liked)
//notify(.update_stats, e.ref_id)
}
}
@@ -290,17 +296,12 @@ 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
}
@@ -310,11 +311,13 @@ class HomeModel: ObservableObject {
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):
@@ -488,7 +491,7 @@ class HomeModel: ObservableObject {
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)
}
@@ -524,6 +527,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)
@@ -690,6 +695,7 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P
profiles.add(id: ev.pubkey, profile: tprof)
if let nip05 = profile.nip05, old_nip05 != profile.nip05 {
Task.detached(priority: .background) {
let validated = await validate_nip05(pubkey: ev.pubkey, nip05_str: nip05)
if validated != nil {
@@ -705,17 +711,22 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P
}
// load pfps asap
var changed = false
let picture = tprof.profile.picture ?? robohash(ev.pubkey)
if URL(string: picture) != nil {
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
changed = true
}
let banner = tprof.profile.banner ?? ""
if URL(string: banner) != nil {
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
changed = true
}
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
if changed {
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
}
}
func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping () -> Void) {
@@ -727,7 +738,7 @@ func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping (
let result = validate_event(ev: ev)
DispatchQueue.main.async {
events.validation[ev.id] = result
events.store_event_validation(evid: ev.id, validated: result)
guard result == .ok else {
return
}
@@ -751,6 +762,8 @@ func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Pr
return
}
profile.cache_lnurl()
DispatchQueue.main.async {
process_metadata_profile(our_pubkey: our_pubkey, profiles: profiles, profile: profile, ev: ev)
}
@@ -814,7 +827,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 {
@@ -828,10 +841,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
}
@@ -937,8 +950,13 @@ func handle_incoming_dms(prev_events: NewEventsBits, dms: DirectMessagesModel, o
}
if inserted {
dms.dms = dms.dms.filter({ $0.events.count > 0 }).sorted { a, b in
return a.events.last!.created_at > b.events.last!.created_at
Task.init {
let new_dms = Array(dms.dms.filter({ $0.events.count > 0 })).sorted { a, b in
return a.events.last!.created_at > b.events.last!.created_at
}
DispatchQueue.main.async {
dms.dms = new_dms
}
}
}
@@ -1104,11 +1122,11 @@ func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
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.inner_event {
} 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 evid = ev.referenced_ids.first?.ref_id,
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)
-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]
}
+1 -1
View File
@@ -682,7 +682,7 @@ func make_post_tags(post_blocks: [PostBlock], tags: [[String]], silent_mentions:
}
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, silent_mentions: false)
let content = render_blocks(blocks: post_tags.blocks)
+16 -8
View File
@@ -110,6 +110,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
var reposts: [String: EventGroup]
var replies: [NostrEvent]
var has_reply: Set<String>
var has_ev: Set<String>
@Published var notifications: [NotificationItem]
@@ -124,6 +125,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
self.incoming_events = []
self.profile_zaps = ZapGroup()
self.notifications = []
self.has_ev = Set()
}
func set_should_queue(_ val: Bool) {
@@ -192,8 +194,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
}
@@ -235,9 +237,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 {
@@ -265,11 +267,17 @@ class NotificationsModel: ObservableObject, ScrollQueue {
}
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 has_ev.contains(ev.id) {
return false
}
if insert_event_immediate(ev) {
if should_queue {
incoming_events.append(ev)
has_ev.insert(ev.id)
return true
}
if insert_event_immediate(ev, cache: damus_state.events) {
self.notifications = build_notifications()
return true
}
@@ -339,7 +347,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)
}
}
+5 -5
View File
@@ -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,12 +124,12 @@ 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 {
// lookup profiles from boosted events
if ev.known_kind == .boost, let bev = ev.inner_event, profiles.lookup(id: bev.pubkey) == nil {
if ev.known_kind == .boost, let bev = ev.get_inner_event(cache: cache), profiles.lookup(id: bev.pubkey) == nil {
pubkeys.insert(bev.pubkey)
}
@@ -148,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 {
+18 -13
View File
@@ -9,24 +9,23 @@ import Foundation
class SearchModel: ObservableObject {
let state: DamusState
var events: EventHolder = EventHolder()
@Published var loading: Bool = false
@Published var channel_name: String? = nil
let pool: RelayPool
var search: NostrFilter
let contacts: Contacts
let sub_id = UUID().description
let profiles_subid = UUID().description
let limit: UInt32 = 500
init(contacts: Contacts, pool: RelayPool, search: NostrFilter) {
self.contacts = contacts
self.pool = pool
init(state: DamusState, search: NostrFilter) {
self.state = state
self.search = search
}
func filter_muted() {
self.events.filter { should_show_event(contacts: contacts, ev: $0) }
self.events.filter { should_show_event(contacts: state.contacts, ev: $0) }
self.objectWillChange.send()
}
@@ -38,13 +37,13 @@ class SearchModel: ObservableObject {
//likes_filter.ids = ref_events.referenced_ids!
print("subscribing to search '\(search)' with sub_id \(sub_id)")
pool.register_handler(sub_id: sub_id, handler: handle_event)
state.pool.register_handler(sub_id: sub_id, handler: handle_event)
loading = true
pool.send(.subscribe(.init(filters: [search], sub_id: sub_id)))
state.pool.send(.subscribe(.init(filters: [search], sub_id: sub_id)))
}
func unsubscribe() {
self.pool.unsubscribe(sub_id: sub_id)
state.pool.unsubscribe(sub_id: sub_id)
loading = false
print("unsubscribing from search '\(search)' with sub_id \(sub_id)")
}
@@ -54,7 +53,7 @@ class SearchModel: ObservableObject {
return
}
guard should_show_event(contacts: contacts, ev: ev) else {
guard should_show_event(contacts: state.contacts, ev: ev) else {
return
}
@@ -74,7 +73,7 @@ class SearchModel: ObservableObject {
}
func handle_event(relay_id: String, ev: NostrConnectionEvent) {
let (_, done) = handle_subid_event(pool: pool, relay_id: relay_id, ev: ev) { sub_id, ev in
let (sub_id, done) = handle_subid_event(pool: state.pool, relay_id: relay_id, ev: ev) { sub_id, ev in
if ev.is_textlike && ev.should_show_event {
self.add_event(ev)
} else if ev.known_kind == .channel_create {
@@ -84,8 +83,14 @@ class SearchModel: ObservableObject {
}
}
if done {
loading = false
guard done else {
return
}
self.loading = false
if sub_id == self.sub_id {
load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(self.events.all_events), damus_state: state)
}
}
}
+3
View File
@@ -31,6 +31,7 @@ enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
case none
case libretranslate
case deepl
case nokyctranslate
var model: Model {
switch self {
@@ -40,6 +41,8 @@ enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
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."))
}
}
+55 -74
View File
@@ -9,6 +9,8 @@ import Foundation
import Vault
import UIKit
let fallback_zap_amount = 1000
@propertyWrapper struct Setting<T: Equatable> {
private let key: String
private var value: T
@@ -92,8 +94,14 @@ class UserSettingsStore: ObservableObject {
@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
@@ -130,12 +138,20 @@ class UserSettingsStore: ObservableObject {
@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
}
}
@StringSetting(key: "friend_filter", default_value: .all)
var friend_filter: FriendFilter
@StringSetting(key: "notification_state", default_value: .all)
var notification_state: NotificationFilterState
@StringSetting(key: "translation_service", default_value: .none)
var translation_service: TranslationService
@@ -177,6 +193,20 @@ class UserSettingsStore: ObservableObject {
}
}
}
@Published var nokyctranslate_api_key: String {
didSet {
do {
if nokyctranslate_api_key == "" {
try clearNoKYCTranslateApiKey()
} else {
try saveNoKYCTranslateApiKey(nokyctranslate_api_key)
}
} catch {
// No-op.
}
}
}
init() {
do {
@@ -184,6 +214,13 @@ class UserSettingsStore: ObservableObject {
} catch {
deepl_api_key = ""
}
do {
nokyctranslate_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusNoKYCTranslateKeychainConfiguration())
} catch {
nokyctranslate_api_key = ""
}
}
private func saveLibreTranslateApiKey(_ apiKey: String) throws {
@@ -194,6 +231,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())
}
@@ -202,7 +247,7 @@ class UserSettingsStore: ObservableObject {
try Vault.deletePrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
}
func can_translate(_ pubkey: String) -> Bool {
var can_translate: Bool {
switch translation_service {
case .none:
return false
@@ -210,6 +255,8 @@ class UserSettingsStore: ObservableObject {
return URLComponents(string: libretranslate_url) != nil
case .deepl:
return deepl_api_key != ""
case .nokyctranslate:
return nokyctranslate_api_key != ""
}
}
}
@@ -226,78 +273,12 @@ struct DamusDeepLKeychainConfiguration: KeychainConfiguration {
var accountName = "deepl_apikey"
}
func should_show_wallet_selector(_ pubkey: String) -> Bool {
return UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
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)"
}
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)
}
let fallback_zap_amount = 1000
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 fallback_zap_amount
}
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_libretranslate_url(_ pubkey: String, server: LibreTranslateServer) -> String? {
if let url = server.model.url {
return url
}
return UserDefaults.standard.object(forKey: "libretranslate_url") as? String
}
private func get_libretranslate_server(_ pubkey: String) -> LibreTranslateServer? {
guard let server_name = UserDefaults.standard.string(forKey: "libretranslate_server") else {
return nil
}
return LibreTranslateServer(rawValue: server_name)
}
+7 -7
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,7 +17,10 @@ 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() {
@@ -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()
}
}
+12
View File
@@ -115,6 +115,18 @@ class Profile: Codable {
}
}
func cache_lnurl() {
guard self._lnurl == nil else {
return
}
guard let addr = lud16 ?? lud06 else {
return
}
self._lnurl = lnaddress_to_lnurl(addr)
}
private var _lnurl: String? = nil
var lnurl: String? {
if let _lnurl {
+31 -11
View File
@@ -42,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 {
@@ -111,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 {
@@ -686,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
@@ -738,7 +758,7 @@ func uniq<T: Hashable>(_ xs: [T]) -> [T] {
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(.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"))
@@ -747,7 +767,7 @@ func gather_reply_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
}
func gather_quote_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
var ids: [ReferencedId] = []
var ids: [ReferencedId] = [.q(from.id)]
if from.pubkey != our_pubkey {
ids.append(ReferencedId(ref_id: from.pubkey, relay_id: nil, key: "p"))
}
@@ -998,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
}
+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
}
+91 -60
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,53 +90,58 @@ 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.utf8.count > 2000 {
DispatchQueue.global(qos: .default).async {
if let ev = decode_nostr_event(txt: txt) {
DispatchQueue.main.async {
self.handleEvent(.nostr_event(ev))
}
return
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):
DispatchQueue.global(qos: .default).async {
if let ev = decode_nostr_event(txt: messageString) {
DispatchQueue.main.async {
self.handleEvent(.nostr_event(ev))
}
}
} else {
if let ev = decode_nostr_event(txt: txt) {
handleEvent(.nostr_event(ev))
return
}
}
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))
}
}
+26 -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 }
}
@@ -88,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
@@ -106,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 {
@@ -208,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 {
@@ -250,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
@@ -267,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)
}
}
+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.
+48
View File
@@ -0,0 +1,48 @@
//
// CredentialHandler.swift
// damus
//
// Created by Bryan Montz on 4/26/23.
//
import Foundation
import AuthenticationServices
final class CredentialHandler: NSObject, ASAuthorizationControllerDelegate {
func check_credentials() {
let requests: [ASAuthorizationRequest] = [ASAuthorizationPasswordProvider().createRequest()]
let authorizationController = ASAuthorizationController(authorizationRequests: requests)
authorizationController.delegate = self
authorizationController.performRequests()
}
func save_credential(pubkey: String, privkey: String) {
SecAddSharedWebCredential("damus.io" as CFString, pubkey as CFString, privkey as CFString, { error in
if let error {
print("⚠️ An error occurred while saving credentials: \(error)")
}
})
}
// MARK: - ASAuthorizationControllerDelegate
func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
guard let cred = authorization.credential as? ASPasswordCredential,
let parsedKey = parse_key(cred.password) else {
return
}
Task {
switch parsedKey {
case .pub, .priv:
try? await process_login(parsedKey, is_pubkey: false)
default:
break
}
}
}
func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
print("⚠️ Warning: authentication failed with error: \(error)")
}
}
+322 -15
View File
@@ -8,14 +8,127 @@
import Combine
import Foundation
import UIKit
import LinkPresentation
import Kingfisher
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)
case not_needed
var img: UIImage? {
switch self {
case .processed(let img):
return img
default:
return nil
}
}
}
class TranslationModel: ObservableObject {
@Published var note_language: String?
@Published var state: TranslateStatus
init(state: TranslateStatus) {
self.state = state
self.note_language = nil
}
}
class NoteArtifactsModel: ObservableObject {
@Published var state: NoteArtifactState
init(state: NoteArtifactState) {
self.state = state
}
}
class PreviewModel: ObservableObject {
@Published var state: PreviewState
func store(preview: LPLinkMetadata?) {
state = .loaded(Preview(meta: preview))
}
init(state: PreviewState) {
self.state = state
}
}
class ZapsDataModel: ObservableObject {
@Published var zaps: [Zap]
init(_ zaps: [Zap]) {
self.zaps = zaps
}
}
class RelativeTimeModel: ObservableObject {
private(set) var last_update: Int64
@Published var value: String {
didSet {
self.last_update = Int64(Date().timeIntervalSince1970)
}
}
init(value: String) {
self.last_update = 0
self.value = ""
}
}
class EventData {
var translations_model: TranslationModel
var artifacts_model: NoteArtifactsModel
var preview_model: PreviewModel
var zaps_model : ZapsDataModel
var relative_time: RelativeTimeModel
var validated: ValidationResult
var translations: TranslateStatus {
return translations_model.state
}
var artifacts: NoteArtifactState {
return artifacts_model.state
}
var preview: PreviewState {
return preview_model.state
}
var zaps: [Zap] {
return zaps_model.zaps
}
init(zaps: [Zap] = []) {
self.translations_model = .init(state: .havent_tried)
self.artifacts_model = .init(state: .not_loaded)
self.zaps_model = .init(zaps)
self.validated = .unknown
self.preview_model = .init(state: .not_loaded)
self.relative_time = .init(value: "")
}
}
class EventCache {
private var events: [String: NostrEvent] = [:]
private var replies = ReplyMap()
private var cancellable: AnyCancellable?
private var translations: [String: TranslateStatus] = [:]
private var artifacts: [String: NoteArtifacts] = [:]
var validation: [String: ValidationResult] = [:]
private var image_metadata: [String: ImageMetadataState] = [:]
private var event_data: [String: EventData] = [:]
//private var thread_latest: [String: Int64]
@@ -27,28 +140,56 @@ class EventCache {
}
}
func is_event_valid(_ evid: String) -> ValidationResult {
guard let result = validation[evid] else {
return .unknown
func get_cache_data(_ evid: String) -> EventData {
guard let data = event_data[evid] else {
let data = EventData()
event_data[evid] = data
return data
}
return result
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) {
self.translations[evid] = translated
get_cache_data(evid).translations_model.state = translated
}
func store_artifacts(evid: String, artifacts: NoteArtifacts) {
self.artifacts[evid] = artifacts
get_cache_data(evid).artifacts_model.state = .loaded(artifacts)
}
func lookup_artifacts(evid: String) -> NoteArtifacts? {
return self.artifacts[evid]
@discardableResult
func store_zap(zap: Zap) -> Bool {
let data = get_cache_data(zap.target.id).zaps_model
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_model.zaps
}
func store_img_metadata(url: URL, meta: ImageMetadataState) {
self.image_metadata[url.absoluteString.lowercased()] = meta
}
func lookup_artifacts(evid: String) -> NoteArtifactState {
return get_cache_data(evid).artifacts_model.state
}
func lookup_img_metadata(url: URL) -> ImageMetadataState? {
return image_metadata[url.absoluteString.lowercased()]
}
func lookup_translated_artifacts(evid: String) -> TranslateStatus? {
return self.translations[evid]
return get_cache_data(evid).translations_model.state
}
func parent_events(event: NostrEvent) -> [NostrEvent] {
@@ -57,7 +198,7 @@ class EventCache {
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
}
@@ -114,8 +255,174 @@ class EventCache {
private func prune() {
events = [:]
translations = [:]
artifacts = [:]
event_data = [:]
replies.replies = [:]
}
}
func should_translate(event: NostrEvent, our_keypair: Keypair, settings: UserSettingsStore, note_lang: String?) -> Bool {
guard settings.can_translate else {
return false
}
// Do not translate self-authored notes if logged in with a private key
// as we can assume the user can understand their own notes.
// 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.
if our_keypair.privkey != nil && our_keypair.pubkey == event.pubkey {
return false
}
if let note_lang {
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
// Don't translate if its in our preferred languages
guard !preferredLanguages.contains(note_lang) else {
// if its the same, give up and don't retry
return false
}
}
// we should start translating if we have auto_translate on
return true
}
func should_preload_translation(event: NostrEvent, our_keypair: Keypair, current_status: TranslateStatus, settings: UserSettingsStore, note_lang: String?) -> Bool {
switch current_status {
case .havent_tried:
return should_translate(event: event, our_keypair: our_keypair, settings: settings, note_lang: note_lang) && settings.auto_translate
case .translating: return false
case .translated: return false
case .not_needed: return false
}
}
struct PreloadPlan {
let data: EventData
let event: NostrEvent
let load_artifacts: Bool
let load_translations: Bool
let load_preview: Bool
}
func load_preview(artifacts: NoteArtifacts) async -> Preview? {
guard let link = artifacts.links.first else {
return nil
}
let meta = await Preview.fetch_metadata(for: link)
return Preview(meta: meta)
}
func get_preload_plan(cache: EventData, ev: NostrEvent, our_keypair: Keypair, settings: UserSettingsStore) -> PreloadPlan? {
let load_artifacts = cache.artifacts.should_preload
if load_artifacts {
cache.artifacts_model.state = .loading
}
// Cached event might not have the note language determined yet, so determine the language here before figuring out if translations should be preloaded.
let note_lang = cache.translations_model.note_language ?? ev.note_language(our_keypair.privkey) ?? current_language()
let load_translations = should_preload_translation(event: ev, our_keypair: our_keypair, current_status: cache.translations, settings: settings, note_lang: note_lang)
if load_translations {
cache.translations_model.state = .translating
}
let load_preview = cache.preview.should_preload
if load_preview {
cache.preview_model.state = .loading
}
if !load_artifacts && !load_translations && !load_preview {
return nil
}
return PreloadPlan(data: cache, event: ev, load_artifacts: load_artifacts, load_translations: load_translations, load_preview: load_preview)
}
func preload_image(url: URL) {
if ImageCache.default.isCached(forKey: url.absoluteString) {
print("Preloaded image \(url.absoluteString) found in cache")
// looks like we already have it cached. no download needed
return
}
print("Preloading image \(url.absoluteString)")
KingfisherManager.shared.retrieveImage(with: ImageResource(downloadURL: url)) { val in
print("Preloaded image \(url.absoluteString)")
}
}
func preload_event(plan: PreloadPlan, profiles: Profiles, our_keypair: Keypair, settings: UserSettingsStore) async {
var artifacts: NoteArtifacts? = plan.data.artifacts.artifacts
print("Preloading event \(plan.event.content)")
// preload pfp
if let profile = profiles.lookup(id: plan.event.pubkey),
let picture = profile.picture,
let url = URL(string: picture) {
preload_image(url: url)
}
if artifacts == nil && plan.load_artifacts {
let arts = render_note_content(ev: plan.event, profiles: profiles, privkey: our_keypair.privkey)
artifacts = arts
// we need these asap
DispatchQueue.main.async {
plan.data.artifacts_model.state = .loaded(arts)
}
for url in arts.images {
preload_image(url: url)
}
}
if plan.load_preview {
let arts = artifacts ?? render_note_content(ev: plan.event, profiles: profiles, privkey: our_keypair.privkey)
let preview = await load_preview(artifacts: arts)
DispatchQueue.main.async {
if let preview {
plan.data.preview_model.state = .loaded(preview)
} else {
plan.data.preview_model.state = .loaded(.failed)
}
}
}
let note_language = plan.data.translations_model.note_language ?? plan.event.note_language(our_keypair.privkey) ?? current_language()
var translations: TranslateStatus? = nil
// We have to recheck should_translate here now that we have note_language
if plan.load_translations && should_translate(event: plan.event, our_keypair: our_keypair, settings: settings, note_lang: note_language) && settings.auto_translate
{
translations = await translate_note(profiles: profiles, privkey: our_keypair.privkey, event: plan.event, settings: settings, note_lang: note_language)
}
let timeago = format_relative_time(plan.event.created_at)
let ts = translations
DispatchQueue.main.async {
if let ts {
plan.data.translations_model.state = ts
}
plan.data.relative_time.value = timeago
plan.data.translations_model.note_language = note_language
}
}
func preload_events(event_cache: EventCache, events: [NostrEvent], profiles: Profiles, our_keypair: Keypair, settings: UserSettingsStore) {
let plans = events.compactMap { ev in
get_preload_plan(cache: event_cache.get_cache_data(ev.id), ev: ev, our_keypair: our_keypair, settings: settings)
}
Task.init {
for plan in plans {
await preload_event(plan: plan, profiles: profiles, our_keypair: our_keypair, settings: settings)
}
}
}
+9 -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
@@ -26,7 +26,14 @@ extension KFOptionSetter {
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
}
+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
}
}
}
}
}
}
+3 -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")
}
+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
+41 -9
View File
@@ -21,6 +21,47 @@ class CachedMetadata {
enum Preview {
case value(CachedMetadata)
case failed
init(meta: LPLinkMetadata?) {
if let meta {
self = .value(CachedMetadata(meta: meta))
} else {
self = .failed
}
}
static func fetch_metadata(for url: URL) async -> LPLinkMetadata? {
// iOS 15 is crashing for some reason
guard #available(iOS 16, *) else {
return nil
}
let provider = LPMetadataProvider()
do {
return try await provider.startFetchingMetadata(for: url)
} catch {
return nil
}
}
}
enum PreviewState {
case not_loaded
case loading
case loaded(Preview)
var should_preload: Bool {
switch self {
case .loaded:
return false
case .loading:
return false
case .not_loaded:
return true
}
}
}
class PreviewCache {
@@ -39,15 +80,6 @@ class PreviewCache {
self.image_meta[evid] = image_fill
}
func store(evid: String, preview: LPLinkMetadata?) {
switch preview {
case .none:
previews[evid] = .failed
case .some(let meta):
previews[evid] = .value(CachedMetadata(meta: meta))
}
}
init() {
self.previews = [:]
self.image_meta = [:]
+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) }
}
+25
View File
@@ -24,6 +24,8 @@ public struct Translator {
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, from: sourceLanguage, to: targetLanguage)
case .none:
@@ -85,6 +87,29 @@ 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")
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)
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 {
guard var components = URLComponents(string: baseUrl) 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: {})
}
}
+29 -39
View File
@@ -8,46 +8,33 @@
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
@ObservedObject var settings: UserSettingsStore
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel? = nil) {
self.damus_state = damus_state
self.event = event
_bar = ObservedObject(wrappedValue: bar ?? make_actionbar_model(ev: event.id, damus: damus_state))
}
@Environment(\.colorScheme) var colorScheme
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel? = nil, test_lnurl: String? = 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))
_settings = ObservedObject(wrappedValue: damus_state.settings)
}
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 settings.onlyzaps_mode {
if damus_state.settings.onlyzaps_mode {
return false
}
@@ -59,7 +46,7 @@ struct EventActionBar: View {
if damus_state.keypair.privkey != nil {
HStack(spacing: 4) {
EventActionButton(img: "bubble.left", col: bar.replied ? DamusColors.purple : Color.gray) {
notify(.reply, event)
notify(.compose, PostAction.replying_to(event))
}
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
Text(verbatim: "\(bar.replies > 0 ? "\(bar.replies)" : "")")
@@ -74,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"))
@@ -112,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 }
@@ -149,10 +145,6 @@ struct EventActionBar: View {
}
}
func send_boost() {
notify(.boost, self.event)
}
func send_like() {
guard let privkey = damus_state.keypair.privkey else {
return
@@ -254,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)
}
+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)
}
}
+15 -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,28 @@ 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")) {
dismiss()
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 +66,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)
}
}
}
@@ -0,0 +1,62 @@
//
// ShareActionButton.swift
// damus
//
// Created by William Casarin on 2023-04-19.
//
import SwiftUI
struct ShareActionButton: View {
let img: String
let text: String
let color: Color?
let action: () -> ()
init(img: String, text: String, col: Color?, action: @escaping () -> ()) {
self.img = img
self.text = text
self.color = col
self.action = action
}
init(img: String, text: String, action: @escaping () -> ()) {
self.img = img
self.text = text
self.action = action
self.color = nil
}
var col: Color {
colorScheme == .light ? DamusColors.mediumGrey : DamusColors.white
}
@Environment(\.colorScheme) var colorScheme
var body: 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)
}
}
}
}
struct ShareActionButton_Previews: PreviewProvider {
static var previews: some View {
ShareActionButton(img: "figure.flexibility", text: "Stretch", action: {})
}
}
+13 -19
View File
@@ -62,13 +62,8 @@ func create_upload_request(mediaToUpload: MediaUpload, mediaUploader: MediaUploa
do {
let (data, _) = try await URLSession.shared.data(for: request, delegate: progress)
guard let responseString = String(data: data, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue)) else {
print("Upload failed getting response string")
return .failed(nil)
}
guard let url = mediaUploader.getMediaURL(from: responseString, mediaIsImage: mediaToUpload.is_image) else {
guard let url = mediaUploader.getMediaURL(from: data) else {
print("Upload failed getting media url")
return .failed(nil)
}
@@ -144,28 +139,27 @@ enum MediaUploader: String, CaseIterable, Identifiable, StringCodable {
var postAPI: String {
switch self {
case .nostrBuild:
return "https://nostr.build/upload.php"
return "https://nostr.build/api/upload/ios.php"
case .nostrImg:
return "https://nostrimg.com/api/upload"
}
}
func getMediaURL(from responseString: String, mediaIsImage: Bool) -> String? {
func getMediaURL(from data: Data) -> String? {
switch self {
case .nostrBuild:
guard let startIndex = responseString.range(of: "nostr.build_")?.lowerBound else {
do {
return try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? String
} catch {
print("Failed JSONSerialization")
return nil
}
let stringContainingName = responseString[startIndex..<responseString.endIndex]
guard let endIndex = stringContainingName.range(of: "<")?.lowerBound else {
return nil
}
let nostrBuildImageName = responseString[startIndex..<endIndex]
let nostrBuildURL = mediaIsImage ? "https://nostr.build/i/\(nostrBuildImageName)" : "https://nostr.build/av/\(nostrBuildImageName)"
return nostrBuildURL
case .nostrImg:
guard let responseString = String(data: data, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue)) else {
print("Upload failed getting response string")
return nil
}
guard let startIndex = responseString.range(of: "https://i.nostrimg.com/")?.lowerBound else {
return nil
}
+9 -5
View File
@@ -9,7 +9,7 @@ import SwiftUI
import Kingfisher
struct InnerBannerImageView: View {
let disable_animation: Bool
let url: URL?
let defaultImage = UIImage(named: "profile-banner") ?? UIImage()
@@ -19,7 +19,7 @@ struct InnerBannerImageView: View {
if (url != nil) {
KFAnimatedImage(url)
.imageContext(.banner)
.imageContext(.banner, disable_animation: disable_animation)
.configure { view in
view.framePreloadCount = 3
}
@@ -35,19 +35,21 @@ struct InnerBannerImageView: View {
}
struct BannerImageView: View {
let disable_animation: Bool
let pubkey: String
let profiles: Profiles
@State var banner: String?
init (pubkey: String, profiles: Profiles, banner: String? = nil) {
init (pubkey: String, profiles: Profiles, disable_animation: Bool, banner: String? = nil) {
self.pubkey = pubkey
self.profiles = profiles
self._banner = State(initialValue: banner)
self.disable_animation = disable_animation
}
var body: some View {
InnerBannerImageView(url: get_banner_url(banner: banner, pubkey: pubkey, profiles: profiles))
InnerBannerImageView(disable_animation: disable_animation, url: get_banner_url(banner: banner, pubkey: pubkey, profiles: profiles))
.onReceive(handle_notify(.profile_updated)) { notif in
let updated = notif.object as! ProfileUpdate
@@ -76,7 +78,9 @@ struct BannerImageView_Previews: PreviewProvider {
static var previews: some View {
BannerImageView(
pubkey: pubkey,
profiles: make_preview_profiles(pubkey))
profiles: make_preview_profiles(pubkey),
disable_animation: false
)
}
}
+9 -1
View File
@@ -11,6 +11,7 @@ struct BookmarksView: View {
let state: DamusState
private let noneFilter: (NostrEvent) -> Bool = { _ in true }
private let bookmarksTitle = NSLocalizedString("Bookmarks", comment: "Title of bookmarks view")
@State private var clearAllAlert: Bool = false
@ObservedObject var manager: BookmarksManager
@@ -45,10 +46,17 @@ struct BookmarksView: View {
.toolbar {
if !bookmarks.isEmpty {
Button(NSLocalizedString("Clear All", comment: "Button for clearing bookmarks data.")) {
manager.clearAll()
clearAllAlert = true
}
}
}
.alert(NSLocalizedString("Are you sure you want to delete all of your bookmarks?", comment: "Alert for deleting all of the bookmarks."), isPresented: $clearAllAlert) {
Button(NSLocalizedString("Cancel", comment: "Cancel deleting bookmarks."), role: .cancel) {
}
Button(NSLocalizedString("Continue", comment: "Continue with bookmarks.")) {
manager.clearAll()
}
}
}
}
+5 -1
View File
@@ -71,11 +71,15 @@ struct ChatView: View {
@Environment(\.colorScheme) var colorScheme
var disable_animation: Bool {
self.damus_state.settings.disable_animation
}
var body: some View {
HStack {
VStack {
if is_active || just_started {
ProfilePicView(pubkey: event.pubkey, size: 32, highlight: is_active ? .main : .none, profiles: damus_state.profiles)
ProfilePicView(pubkey: event.pubkey, size: 32, highlight: is_active ? .main : .none, profiles: damus_state.profiles, disable_animation: disable_animation)
}
Spacer()
+1 -1
View File
@@ -43,7 +43,7 @@ struct DMChatView: View {
let profile_page = ProfileView(damus_state: damus_state, pubkey: pubkey)
return NavigationLink(destination: profile_page) {
HStack {
ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles)
ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: true)
}
+1 -1
View File
@@ -32,7 +32,7 @@ struct EventView: View {
var body: some View {
VStack {
if event.known_kind == .boost {
if let inner_ev = event.inner_event {
if let inner_ev = event.get_inner_event(cache: damus.events) {
RepostedEvent(damus: damus, event: event, inner_ev: inner_ev, options: options)
} else {
EmptyView()
+1 -1
View File
@@ -70,7 +70,7 @@ struct BuilderEventView: View {
var body: some View {
VStack {
if let event {
let ev = event.inner_event ?? event
let ev = event.get_inner_event(cache: damus.events) ?? event
let thread = ThreadModel(event: ev, damus_state: damus)
let dest = ThreadView(state: damus, thread: thread)
NavigationLink(destination: dest) {
+5 -1
View File
@@ -28,11 +28,15 @@ struct EventProfile: View {
eventview_pfp_size(size)
}
var disable_animation: Bool {
damus_state.settings.disable_animation
}
var body: some View {
HStack(alignment: .center) {
VStack {
NavigationLink(destination: ProfileView(damus_state: damus_state, pubkey: pubkey)) {
ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles)
ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles, disable_animation: disable_animation)
}
}
+2 -2
View File
@@ -31,9 +31,9 @@ struct MutedEventView: View {
.foregroundColor(DamusColors.adaptableGrey)
HStack {
Text("Post from a user you've blocked", comment: "Text to indicate that what is being shown is a post from a user who has been blocked.")
Text("Post from a user you've muted", comment: "Text to indicate that what is being shown is a post from a user who has been muted.")
Spacer()
Button(shown ? NSLocalizedString("Hide", comment: "Button to hide a post from a user who has been blocked.") : NSLocalizedString("Show", comment: "Button to show a post from a user who has been blocked.")) {
Button(shown ? NSLocalizedString("Hide", comment: "Button to hide a post from a user who has been muted.") : NSLocalizedString("Show", comment: "Button to show a post from a user who has been muted.")) {
shown.toggle()
}
}
+32 -8
View File
@@ -23,11 +23,30 @@ struct EventViewOptions: OptionSet {
static let embedded: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested]
}
struct RelativeTime: View {
@ObservedObject var time: RelativeTimeModel
var body: some View {
Text(verbatim: "\(time.value)")
.font(.system(size: 16))
.foregroundColor(.gray)
}
}
struct TextEvent: View {
let damus: DamusState
let event: NostrEvent
let pubkey: String
let options: EventViewOptions
let evdata: EventData
init(damus: DamusState, event: NostrEvent, pubkey: String, options: EventViewOptions) {
self.damus = damus
self.event = event
self.pubkey = pubkey
self.options = options
self.evdata = damus.events.get_cache_data(event.id)
}
var has_action_bar: Bool {
!options.contains(.no_action_bar)
@@ -55,7 +74,7 @@ struct TextEvent: View {
HStack(alignment: .center, spacing: 0) {
ProfileName(is_anon: is_anon)
TimeDot
Time
RelativeTime(time: self.evdata.relative_time)
Spacer()
ContextButton
}
@@ -106,12 +125,6 @@ struct TextEvent: View {
.foregroundColor(.gray)
}
var Time: some View {
Text(verbatim: "\(format_relative_time(event.created_at))")
.font(.system(size: 16))
.foregroundColor(.gray)
}
var ContextButton: some View {
EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks, muted_threads: damus.muted_threads)
.padding([.bottom], 4)
@@ -124,7 +137,17 @@ struct TextEvent: View {
}
func EvBody(options: EventViewOptions) -> some View {
return EventBody(damus_state: damus, event: event, size: .normal, options: options)
let show_imgs = should_show_images(settings: damus.settings, contacts: damus.contacts, ev: event, our_pubkey: damus.pubkey)
let artifacts = damus.events.get_cache_data(event.id).artifacts.artifacts ?? .just_content(event.get_content(damus.keypair.privkey))
return NoteContentView(
damus_state: damus,
event: event,
show_images: show_imgs,
size: .normal,
artifacts: artifacts,
options: options
)
.fixedSize(horizontal: false, vertical: true)
}
func Mention(_ mention: Mention) -> some View {
@@ -162,6 +185,7 @@ struct TextEvent: View {
TopPart(is_anon: is_anon)
ReplyPart
EvBody(options: self.options)
if let mention = get_mention() {
+2 -2
View File
@@ -13,6 +13,7 @@ struct ImagePicker: UIViewControllerRepresentable {
@Environment(\.presentationMode)
private var presentationMode
let uploader: MediaUploader
let sourceType: UIImagePickerController.SourceType
let pubkey: String
@Binding var image_upload_confirm: Bool
@@ -108,9 +109,8 @@ struct ImagePicker: UIViewControllerRepresentable {
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = sourceType
let mediaUploader = get_media_uploader(pubkey)
picker.mediaTypes = ["public.image", "com.compuserve.gif"]
if mediaUploader.supportsVideo && !imagesOnly {
if uploader.supportsVideo && !imagesOnly {
picker.mediaTypes.append("public.movie")
}
picker.delegate = context.coordinator
+4 -4
View File
@@ -9,14 +9,14 @@ import SwiftUI
import Kingfisher
// lots of overlap between this and ImageContainerView
struct ImageContainerView: View {
let url: URL?
@State private var image: UIImage?
@State private var showShareSheet = false
let disable_animation: Bool
private struct ImageHandler: ImageModifier {
@Binding var handler: UIImage?
@@ -29,7 +29,7 @@ struct ImageContainerView: View {
var body: some View {
KFAnimatedImage(url)
.imageContext(.note)
.imageContext(.note, disable_animation: disable_animation)
.configure { view in
view.framePreloadCount = 3
}
@@ -46,6 +46,6 @@ let test_image_url = URL(string: "https://jb55.com/red-me.jpg")!
struct ImageContainerView_Previews: PreviewProvider {
static var previews: some View {
ImageContainerView(url: test_image_url)
ImageContainerView(url: test_image_url, disable_animation: false)
}
}
+4 -2
View File
@@ -16,6 +16,8 @@ struct ImageView: View {
@State private var selectedIndex = 0
@State var showMenu = true
let disable_animation: Bool
var tabViewIndicator: some View {
HStack(spacing: 10) {
ForEach(urls.indices, id: \.self) { index in
@@ -37,7 +39,7 @@ struct ImageView: View {
TabView(selection: $selectedIndex) {
ForEach(urls.indices, id: \.self) { index in
ZoomableScrollView {
ImageContainerView(url: urls[index])
ImageContainerView(url: urls[index], disable_animation: disable_animation)
.aspectRatio(contentMode: .fit)
.padding(.top, Theme.safeAreaInsets?.top)
.padding(.bottom, Theme.safeAreaInsets?.bottom)
@@ -77,6 +79,6 @@ struct ImageView: View {
struct ImageView_Previews: PreviewProvider {
static var previews: some View {
ImageView(urls: [URL(string: "https://jb55.com/red-me.jpg")])
ImageView(urls: [URL(string: "https://jb55.com/red-me.jpg")], disable_animation: false)
}
}
+8 -5
View File
@@ -8,12 +8,13 @@ import SwiftUI
import Kingfisher
struct ProfileImageContainerView: View {
let url: URL?
@State private var image: UIImage?
@State private var showShareSheet = false
let disable_animation: Bool
private struct ImageHandler: ImageModifier {
@Binding var handler: UIImage?
@@ -26,7 +27,7 @@ struct ProfileImageContainerView: View {
var body: some View {
KFAnimatedImage(url)
.imageContext(.pfp)
.imageContext(.pfp, disable_animation: disable_animation)
.configure { view in
view.framePreloadCount = 3
}
@@ -61,9 +62,9 @@ struct NavDismissBarView: View {
}
struct ProfilePicImageView: View {
let pubkey: String
let profiles: Profiles
let disable_animation: Bool
@Environment(\.presentationMode) var presentationMode
@@ -73,7 +74,7 @@ struct ProfilePicImageView: View {
.ignoresSafeArea()
ZoomableScrollView {
ProfileImageContainerView(url: get_profile_url(picture: nil, pubkey: pubkey, profiles: profiles))
ProfileImageContainerView(url: get_profile_url(picture: nil, pubkey: pubkey, profiles: profiles), disable_animation: disable_animation)
.aspectRatio(contentMode: .fit)
.padding(.top, Theme.safeAreaInsets?.top)
.padding(.bottom, Theme.safeAreaInsets?.bottom)
@@ -94,6 +95,8 @@ struct ProfileZoomView_Previews: PreviewProvider {
static var previews: some View {
ProfilePicImageView(
pubkey: pubkey,
profiles: make_preview_profiles(pubkey))
profiles: make_preview_profiles(pubkey),
disable_animation: false
)
}
}
+101 -88
View File
@@ -36,6 +36,7 @@ struct LoginView: View {
@State var key: String = ""
@State var is_pubkey: Bool = false
@State var error: String? = nil
@State private var credential_handler = CredentialHandler()
func get_error(parsed_key: ParsedKey?) -> String? {
if self.error != nil {
@@ -43,85 +44,12 @@ struct LoginView: View {
}
if !key.isEmpty && parsed_key == nil {
return "Invalid key"
return LoginError.invalid_key.errorDescription
}
return nil
}
func process_login(_ key: ParsedKey, is_pubkey: Bool) -> Bool {
switch key {
case .priv(let priv):
do {
try save_privkey(privkey: priv)
} catch {
return false
}
guard let pk = privkey_to_pubkey(privkey: priv) else {
return false
}
save_pubkey(pubkey: pk)
case .pub(let pub):
do {
try clear_saved_privkey()
} catch {
return false
}
save_pubkey(pubkey: pub)
case .nip05(let id):
Task.init {
guard let nip05 = await get_nip05_pubkey(id: id) else {
self.error = "Could not fetch pubkey"
return
}
// this is a weird way to login anyways
/*
var bootstrap_relays = load_bootstrap_relays(pubkey: nip05.pubkey)
for relay in nip05.relays {
if !(bootstrap_relays.contains { $0 == relay }) {
bootstrap_relays.append(relay)
}
}
*/
save_pubkey(pubkey: nip05.pubkey)
notify(.login, ())
}
case .hex(let hexstr):
if is_pubkey {
do {
try clear_saved_privkey()
} catch {
return false
}
save_pubkey(pubkey: hexstr)
} else {
do {
try save_privkey(privkey: hexstr)
} catch {
return false
}
guard let pk = privkey_to_pubkey(privkey: hexstr) else {
return false
}
save_pubkey(pubkey: pk)
}
}
notify(.login, ())
return true
}
var body: some View {
ZStack(alignment: .top) {
DamusGradient()
@@ -158,17 +86,26 @@ struct LoginView: View {
.foregroundColor(.white)
.padding()
}
Spacer()
if let p = parsed {
DamusWhiteButton(NSLocalizedString("Login", comment: "Button to log into account.")) {
if !process_login(p, is_pubkey: self.is_pubkey) {
self.error = NSLocalizedString("Invalid key", comment: "Error message indicating that an invalid account key was entered for login.")
Task {
do {
try await process_login(p, is_pubkey: is_pubkey)
} catch {
self.error = error.localizedDescription
}
}
}
}
}
.padding()
}
.onAppear {
credential_handler.check_credentials()
}
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: BackNav())
}
@@ -212,6 +149,71 @@ func parse_key(_ thekey: String) -> ParsedKey? {
return nil
}
enum LoginError: LocalizedError {
case invalid_key
case nip05_failed
var errorDescription: String? {
switch self {
case .invalid_key:
return NSLocalizedString("Invalid key", comment: "Error message indicating that an invalid account key was entered for login.")
case .nip05_failed:
return "Could not fetch pubkey"
}
}
}
func process_login(_ key: ParsedKey, is_pubkey: Bool) async throws {
switch key {
case .priv(let priv):
try handle_privkey(priv)
case .pub(let pub):
try clear_saved_privkey()
save_pubkey(pubkey: pub)
case .nip05(let id):
guard let nip05 = await get_nip05_pubkey(id: id) else {
throw LoginError.nip05_failed
}
// this is a weird way to login anyways
/*
var bootstrap_relays = load_bootstrap_relays(pubkey: nip05.pubkey)
for relay in nip05.relays {
if !(bootstrap_relays.contains { $0 == relay }) {
bootstrap_relays.append(relay)
}
}
*/
save_pubkey(pubkey: nip05.pubkey)
case .hex(let hexstr):
if is_pubkey {
try clear_saved_privkey()
save_pubkey(pubkey: hexstr)
} else {
try handle_privkey(hexstr)
}
}
func handle_privkey(_ privkey: String) throws {
try save_privkey(privkey: privkey)
guard let pk = privkey_to_pubkey(privkey: privkey) else {
throw LoginError.invalid_key
}
if let pub = bech32_pubkey(pk), let priv = bech32_privkey(privkey) {
CredentialHandler().save_credential(pubkey: pub, privkey: priv)
}
save_pubkey(pubkey: pk)
}
await MainActor.run {
notify(.login, ())
}
}
struct NIP05Result: Decodable {
let names: Dictionary<String, String>
let relays: Dictionary<String, [String]>?
@@ -268,18 +270,29 @@ struct KeyInput: View {
}
var body: some View {
TextField("", text: key)
.placeholder(when: key.wrappedValue.isEmpty) {
Text(title).foregroundColor(.white.opacity(0.6))
ZStack(alignment: .leading) {
TextField("", text: key)
.placeholder(when: key.wrappedValue.isEmpty) {
Text(title).foregroundColor(.white.opacity(0.6))
}
.padding()
.padding(.leading, 20)
.background {
RoundedRectangle(cornerRadius: 4.0).opacity(0.2)
}
.autocapitalization(.none)
.foregroundColor(.white)
.font(.body.monospaced())
.textContentType(.password)
Label("", systemImage: "doc.on.clipboard")
.padding(.leading, 10)
.onTapGesture {
if let pastedkey = UIPasteboard.general.string {
self.key.wrappedValue = pastedkey
}
}
.padding()
.background {
RoundedRectangle(cornerRadius: 4.0).opacity(0.2)
}
.autocapitalization(.none)
.foregroundColor(.white)
.font(.body.monospaced())
.textContentType(.password)
}
}
}
+2 -2
View File
@@ -37,7 +37,7 @@ func show_indicator(timeline: Timeline, current: NewEventsBits, indicator_settin
struct TabButton: View {
let timeline: Timeline
let img: String
@Binding var selected: Timeline?
@Binding var selected: Timeline
@Binding var new_events: NewEventsBits
let settings: UserSettingsStore
@@ -75,7 +75,7 @@ struct TabButton: View {
struct TabBar: View {
@Binding var new_events: NewEventsBits
@Binding var selected: Timeline?
@Binding var selected: Timeline
let settings: UserSettingsStore
let action: (Timeline) -> ()
+1 -1
View File
@@ -29,7 +29,7 @@ struct MutelistView: View {
damus_state.postbox.send(new_ev)
users = get_mutelist_users(new_ev)
} label: {
Label(NSLocalizedString("Delete", comment: "Button to remove a user from their blocklist."), systemImage: "trash")
Label(NSLocalizedString("Delete", comment: "Button to remove a user from their mutelist."), systemImage: "trash")
}
.tint(.red)
}
+72 -45
View File
@@ -30,8 +30,12 @@ struct NoteContentView: View {
let preview_height: CGFloat?
let options: EventViewOptions
@State var artifacts: NoteArtifacts
@State var preview: LinkViewRepresentable?
@ObservedObject var artifacts_model: NoteArtifactsModel
@ObservedObject var preview_model: PreviewModel
var artifacts: NoteArtifacts {
return self.artifacts_model.state.artifacts ?? .just_content(event.get_content(damus_state.keypair.privkey))
}
init(damus_state: DamusState, event: NostrEvent, show_images: Bool, size: EventViewKind, artifacts: NoteArtifacts, options: EventViewOptions) {
self.damus_state = damus_state
@@ -39,16 +43,10 @@ struct NoteContentView: View {
self.show_images = show_images
self.size = size
self.options = options
self._artifacts = State(initialValue: artifacts)
self.preview_height = lookup_cached_preview_size(previews: damus_state.previews, evid: event.id)
self._preview = State(initialValue: load_cached_preview(previews: damus_state.previews, evid: event.id))
if let cache = damus_state.events.lookup_artifacts(evid: event.id) {
self._artifacts = State(initialValue: cache)
} else {
let artifacts = render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
damus_state.events.store_artifacts(evid: event.id, artifacts: artifacts)
self._artifacts = State(initialValue: artifacts)
}
let cached = damus_state.events.get_cache_data(event.id)
self._preview_model = ObservedObject(wrappedValue: cached.preview_model)
self._artifacts_model = ObservedObject(wrappedValue: cached.artifacts_model)
}
var truncate: Bool {
@@ -59,6 +57,16 @@ struct NoteContentView: View {
return options.contains(.pad_content)
}
var preview: LinkViewRepresentable? {
guard show_images,
case .loaded(let preview) = preview_model.state,
case .value(let cached) = preview else {
return nil
}
return LinkViewRepresentable(meta: .linkmeta(cached))
}
var truncatedText: some View {
Group {
if truncate {
@@ -72,7 +80,7 @@ struct NoteContentView: View {
}
var invoicesView: some View {
InvoicesView(our_pubkey: damus_state.keypair.pubkey, invoices: artifacts.invoices)
InvoicesView(our_pubkey: damus_state.keypair.pubkey, invoices: artifacts.invoices, settings: damus_state.settings)
}
var translateView: some View {
@@ -123,10 +131,10 @@ struct NoteContentView: View {
}
if show_images && artifacts.images.count > 0 {
ImageCarousel(previews: damus_state.previews, evid: event.id, urls: artifacts.images)
ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.images)
} else if !show_images && artifacts.images.count > 0 {
ZStack {
ImageCarousel(previews: damus_state.previews, evid: event.id, urls: artifacts.images)
ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.images)
Blur()
.disabled(true)
}
@@ -151,6 +159,14 @@ struct NoteContentView: View {
}
}
func load() async {
guard let plan = get_preload_plan(cache: damus_state.events.get_cache_data(event.id), ev: event, our_keypair: damus_state.keypair, settings: damus_state.settings) else {
return
}
await preload_event(plan: plan, profiles: damus_state.profiles, our_keypair: damus_state.keypair, settings: damus_state.settings)
}
var body: some View {
MainContent
.onReceive(handle_notify(.profile_updated)) { notif in
@@ -160,7 +176,11 @@ struct NoteContentView: View {
switch block {
case .mention(let m):
if m.type == .pubkey && m.ref.ref_id == profile.pubkey {
self.artifacts = render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
self.artifacts_model.state = .loading
Task.init {
await load()
}
return
}
case .relay: return
case .text: return
@@ -171,39 +191,10 @@ struct NoteContentView: View {
}
}
.task {
guard self.preview == nil else {
return
}
if show_images, artifacts.links.count == 1 {
let meta = await getMetaData(for: artifacts.links.first!)
damus_state.previews.store(evid: self.event.id, preview: meta)
guard case .value(let cached) = damus_state.previews.lookup(self.event.id) else {
return
}
let view = LinkViewRepresentable(meta: .linkmeta(cached))
self.preview = view
}
await load()
}
}
func getMetaData(for url: URL) async -> LPLinkMetadata? {
// iOS 15 is crashing for some reason
guard #available(iOS 16, *) else {
return nil
}
let provider = LPMetadataProvider()
do {
return try await provider.startFetchingMetadata(for: url)
} catch {
return nil
}
}
}
enum ImageName {
@@ -274,6 +265,42 @@ struct NoteArtifacts: Equatable {
}
}
enum NoteArtifactState {
case not_loaded
case loading
case loaded(NoteArtifacts)
var artifacts: NoteArtifacts? {
if case .loaded(let artifacts) = self {
return artifacts
}
return nil
}
var is_loaded: Bool {
switch self {
case .not_loaded:
return false
case .loading:
return false
case .loaded:
return true
}
}
var should_preload: Bool {
switch self {
case .loaded:
return false
case .loading:
return false
case .not_loaded:
return true
}
}
}
func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -> NoteArtifacts {
let blocks = ev.blocks(privkey)
@@ -204,7 +204,7 @@ struct EventGroupView: View {
.frame(width: PFP_SIZE + 10)
VStack(alignment: .leading) {
ProfilePicturesView(state: state, events: group.events)
ProfilePicturesView(state: state, pubkeys: group.events.map { $0.pubkey })
if let event {
let thread = ThreadModel(event: event, damus_state: state)
@@ -28,7 +28,7 @@ enum FriendFilter: String, StringCodable {
case .all:
return true
case .friends:
return contacts.is_in_friendosphere(pubkey)
return contacts.is_friend_or_self(pubkey)
}
}
}
@@ -74,25 +74,13 @@ class NotificationFilter: ObservableObject, Equatable {
}
}
enum NotificationFilterState: String, StringCodable {
enum NotificationFilterState: String {
case all
case zaps
case replies
init?(from string: String) {
guard let val = NotificationFilterState(rawValue: string) else {
return nil
}
self = val
}
func to_string() -> String {
self.rawValue
}
func is_other( item: NotificationItem) -> Bool {
return item.is_zap == nil && item.is_reply == nil
item.is_zap == nil && item.is_reply == nil
}
func filter(_ item: NotificationItem) -> Bool {
@@ -110,7 +98,8 @@ enum NotificationFilterState: String, StringCodable {
struct NotificationsView: View {
let state: DamusState
@ObservedObject var notifications: NotificationsModel
@StateObject var filter_state: NotificationFilter = NotificationFilter()
@StateObject var filter = NotificationFilter()
@SceneStorage("NotificationsView.filter_state") var filter_state: NotificationFilterState = .all
@Environment(\.colorScheme) var colorScheme
@@ -123,14 +112,14 @@ struct NotificationsView: View {
}
var body: some View {
TabView(selection: $filter_state.state) {
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
NotificationTab(
NotificationFilter(
state: .all,
fine_filter: filter_state.fine_filter
fine_filter: filter.fine_filter
)
)
.tag(NotificationFilterState.all)
@@ -138,7 +127,7 @@ struct NotificationsView: View {
NotificationTab(
NotificationFilter(
state: .zaps,
fine_filter: filter_state.fine_filter
fine_filter: filter.fine_filter
)
)
.tag(NotificationFilterState.zaps)
@@ -146,31 +135,31 @@ struct NotificationsView: View {
NotificationTab(
NotificationFilter(
state: .replies,
fine_filter: filter_state.fine_filter
fine_filter: filter.fine_filter
)
)
.tag(NotificationFilterState.replies)
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
if would_filter_non_friends_from_notifications(contacts: state.contacts, state: self.filter_state.state, items: self.notifications.notifications) {
FriendsButton(filter: $filter_state.fine_filter)
if would_filter_non_friends_from_notifications(contacts: state.contacts, state: filter_state, items: self.notifications.notifications) {
FriendsButton(filter: $filter.fine_filter)
}
}
}
.onChange(of: filter_state.fine_filter) { val in
.onChange(of: filter.fine_filter) { val in
state.settings.friend_filter = val
}
.onChange(of: filter_state.state) { val in
state.settings.notification_state = val
.onChange(of: filter_state) { val in
filter.state = val
}
.onAppear {
self.filter_state.fine_filter = state.settings.friend_filter
self.filter_state.state = state.settings.notification_state
self.filter.fine_filter = state.settings.friend_filter
filter.state = filter_state
}
.safeAreaInset(edge: .top, spacing: 0) {
VStack(spacing: 0) {
CustomPicker(selection: $filter_state.state, content: {
CustomPicker(selection: $filter_state, content: {
Text("All", comment: "Label for filter for all notifications.")
.tag(NotificationFilterState.all)
@@ -221,7 +210,7 @@ struct NotificationsView: View {
struct NotificationsView_Previews: PreviewProvider {
static var previews: some View {
NotificationsView(state: test_damus_state(), notifications: NotificationsModel(), filter_state: NotificationFilter())
NotificationsView(state: test_damus_state(), notifications: NotificationsModel(), filter: NotificationFilter())
}
}
@@ -9,7 +9,7 @@ import SwiftUI
struct ProfilePicturesView: View {
let state: DamusState
let events: [NostrEvent]
let pubkeys: [String]
@State var nav_target: String? = nil
@State var navigating: Bool = false
@@ -19,10 +19,10 @@ struct ProfilePicturesView: View {
EmptyView()
}
HStack {
ForEach(events.prefix(8)) { ev in
ProfilePicView(pubkey: ev.pubkey, size: 32.0, highlight: .none, profiles: state.profiles)
ForEach(pubkeys.prefix(8), id: \.self) { pubkey in
ProfilePicView(pubkey: pubkey, size: 32.0, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation)
.onTapGesture {
nav_target = ev.pubkey
nav_target = pubkey
navigating = true
}
}
@@ -32,6 +32,6 @@ struct ProfilePicturesView: View {
struct ProfilePicturesView_Previews: PreviewProvider {
static var previews: some View {
ProfilePicturesView(state: test_damus_state(), events: [test_event, test_event])
ProfilePicturesView(state: test_damus_state(), pubkeys: ["a", "b"])
}
}
+1 -1
View File
@@ -51,7 +51,7 @@ struct ParticipantsView: View {
ForEach(originalReferences.pRefs) { participant in
let pubkey = participant.id
HStack {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey)
+33 -14
View File
@@ -82,6 +82,8 @@ struct PostView: View {
var content = self.post.string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: " ")
let img_meta_tags = uploadedMedias.compactMap { $0.metadata?.to_tag() }
content.append(" " + imagesString + " ")
@@ -89,7 +91,7 @@ struct PostView: View {
content.append(" nostr:" + id)
}
let new_post = NostrPost(content: content, references: references, kind: kind)
let new_post = NostrPost(content: content, references: references, kind: kind, tags: img_meta_tags)
NotificationCenter.default.post(name: .post, object: NostrPostResult.post(new_post))
@@ -246,9 +248,11 @@ struct PostView: View {
}
func handle_upload(media: MediaUpload) {
let uploader = get_media_uploader(damus_state.pubkey)
let uploader = damus_state.settings.default_media_uploader
Task.init {
let img = getImage(media: media)
print("img size w:\(img.size.width) h:\(img.size.height)")
async let blurhash = calculate_blurhash(img: img)
let res = await image_upload.start(media: media, uploader: uploader)
switch res {
@@ -257,7 +261,9 @@ struct PostView: View {
self.error = "Error uploading image :("
return
}
let uploadedMedia = UploadedMedia(localURL: media.localURL, uploadedURL: url, representingImage: img)
let blurhash = await blurhash
let meta = blurhash.map { bh in calculate_image_metadata(url: url, img: img, blurhash: bh) }
let uploadedMedia = UploadedMedia(localURL: media.localURL, uploadedURL: url, representingImage: img, metadata: meta)
uploadedMedias.append(uploadedMedia)
case .failed(let error):
@@ -271,21 +277,24 @@ struct PostView: View {
}
}
var has_artifacts: Bool {
var multiply_factor: CGFloat {
if case .quoting = action {
return true
return 0.4
} else if !uploadedMedias.isEmpty {
return 0.2
} else {
return 1.0
}
return !uploadedMedias.isEmpty
}
func Editor(deviceSize: GeometryProxy) -> some View {
VStack(alignment: .leading, spacing: 0) {
HStack(alignment: .top) {
ProfilePicView(pubkey: damus_state.pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
ProfilePicView(pubkey: damus_state.pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
TextEntry
}
.frame(height: has_artifacts ? deviceSize.size.height*0.4 : deviceSize.size.height)
.frame(height: deviceSize.size.height * multiply_factor)
.id("post")
PVImageCarouselView(media: $uploadedMedias, deviceWidth: deviceSize.size.width)
@@ -336,12 +345,12 @@ struct PostView: View {
}
}
.sheet(isPresented: $attach_media) {
ImagePicker(sourceType: .photoLibrary, pubkey: damus_state.pubkey, image_upload_confirm: $image_upload_confirm) { img in
ImagePicker(uploader: damus_state.settings.default_media_uploader, sourceType: .photoLibrary, pubkey: damus_state.pubkey, image_upload_confirm: $image_upload_confirm) { img in
self.mediaToUpload = .image(img)
} onVideoPicked: { url in
self.mediaToUpload = .video(url)
}
.alert(NSLocalizedString("Are you sure you want to upload this image?", comment: "Alert message asking if the user wants to upload an image."), isPresented: $image_upload_confirm) {
.alert(NSLocalizedString("Are you sure you want to upload this media?", comment: "Alert message asking if the user wants to upload an image."), isPresented: $image_upload_confirm) {
Button(NSLocalizedString("Upload", comment: "Button to proceed with uploading."), role: .none) {
if let mediaToUpload {
self.handle_upload(media: mediaToUpload)
@@ -352,11 +361,20 @@ struct PostView: View {
}
}
.sheet(isPresented: $attach_camera) {
// image_upload_confirm isn't handled here, I don't know we need to display it here too tbh
ImagePicker(sourceType: .camera, pubkey: damus_state.pubkey, image_upload_confirm: $image_upload_confirm) { img in
handle_upload(media: .image(img))
ImagePicker(uploader: damus_state.settings.default_media_uploader, sourceType: .camera, pubkey: damus_state.pubkey, image_upload_confirm: $image_upload_confirm) { img in
self.mediaToUpload = .image(img)
} onVideoPicked: { url in
handle_upload(media: .video(url))
self.mediaToUpload = .video(url)
}
.alert("Are you sure you want to upload this media?", isPresented: $image_upload_confirm) {
Button(NSLocalizedString("Upload", comment: "Button to proceed with uploading."), role: .none) {
if let mediaToUpload {
self.handle_upload(media: mediaToUpload)
self.attach_camera = false
}
}
Button(NSLocalizedString("Cancel", comment: "Button to cancel the upload."), role: .cancel) {}
}
}
.onAppear() {
@@ -501,6 +519,7 @@ struct UploadedMedia: Equatable {
let localURL: URL
let uploadedURL: URL
let representingImage: UIImage
let metadata: ImageMetadata?
}
+1 -1
View File
@@ -125,7 +125,7 @@ struct EditMetadataView: View {
var TopSection: some View {
ZStack(alignment: .top) {
GeometryReader { geo in
BannerImageView(pubkey: damus_state.pubkey, profiles: damus_state.profiles)
BannerImageView(pubkey: damus_state.pubkey, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
.aspectRatio(contentMode: .fill)
.frame(width: geo.size.width, height: BANNER_HEIGHT)
.clipped()
@@ -8,7 +8,7 @@
import SwiftUI
struct EditProfilePictureControl: View {
let uploader: MediaUploader
let pubkey: String
@Binding var profile_image: URL?
@ObservedObject var viewModel: ProfileUploadingViewModel
@@ -20,6 +20,8 @@ struct EditProfilePictureControl: View {
@State private var show_library = false
@State var image_upload_confirm: Bool = false
@State var mediaToUpload: MediaUpload? = nil
var body: some View {
Menu {
Button(action: {
@@ -45,26 +47,43 @@ struct EditProfilePictureControl: View {
}
}
.sheet(isPresented: $show_camera) {
// The alert may not be required for the profile pic upload case. Not showing the confirm check alert for this scenario
ImagePicker(sourceType: .camera, pubkey: pubkey, image_upload_confirm: $image_upload_confirm, imagesOnly: true) { img in
handle_upload(media: .image(img))
ImagePicker(uploader: uploader, sourceType: .camera, pubkey: pubkey, image_upload_confirm: $image_upload_confirm, imagesOnly: true) { img in
self.mediaToUpload = .image(img)
} onVideoPicked: { url in
print("Cannot upload videos as profile image")
}
.alert("Are you sure you want to upload this image?", isPresented: $image_upload_confirm) {
Button(NSLocalizedString("Upload", comment: "Button to proceed with uploading."), role: .none) {
if let mediaToUpload {
self.handle_upload(media: mediaToUpload)
self.show_camera = false
}
}
Button(NSLocalizedString("Cancel", comment: "Button to cancel the upload."), role: .cancel) {}
}
}
.sheet(isPresented: $show_library) {
// The alert may not be required for the profile pic upload case. Not showing the confirm check alert for this scenario
ImagePicker(sourceType: .photoLibrary, pubkey: pubkey, image_upload_confirm: $image_upload_confirm, imagesOnly: true) { img in
handle_upload(media: .image(img))
ImagePicker(uploader: uploader, sourceType: .photoLibrary, pubkey: pubkey, image_upload_confirm: $image_upload_confirm, imagesOnly: true) { img in
self.mediaToUpload = .image(img)
} onVideoPicked: { url in
print("Cannot upload videos as profile image")
}
.alert("Are you sure you want to upload this image?", isPresented: $image_upload_confirm) {
Button(NSLocalizedString("Upload", comment: "Button to proceed with uploading."), role: .none) {
if let mediaToUpload {
self.handle_upload(media: mediaToUpload)
self.show_library = false
}
}
Button(NSLocalizedString("Cancel", comment: "Button to cancel the upload."), role: .cancel) {}
}
}
}
private func handle_upload(media: MediaUpload) {
viewModel.isLoading = true
let uploader = get_media_uploader(pubkey)
Task {
let res = await image_upload.start(media: media, uploader: uploader)
+1 -1
View File
@@ -35,7 +35,7 @@ struct MaybeAnonPfpView: View {
.frame(width: size, height: size)
} else {
NavigationLink(destination: ProfileView(damus_state: state, pubkey: pubkey)) {
ProfilePicView(pubkey: pubkey, size: size, highlight: .none, profiles: state.profiles)
ProfilePicView(pubkey: pubkey, size: size, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation)
}
}
}
+14 -6
View File
@@ -53,13 +53,17 @@ struct EditProfilePictureView: View {
.overlay(Circle().stroke(highlight_color(highlight), lineWidth: pfp_line_width(highlight)))
.padding(2)
}
var disable_animation: Bool {
damus_state?.settings.disable_animation ?? false
}
var body: some View {
ZStack {
Color(uiColor: .systemBackground)
KFAnimatedImage(get_profile_url())
.imageContext(.pfp)
.imageContext(.pfp, disable_animation: disable_animation)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
@@ -87,12 +91,12 @@ struct EditProfilePictureView: View {
}
struct InnerProfilePicView: View {
let url: URL?
let fallbackUrl: URL?
let pubkey: String
let size: CGFloat
let highlight: Highlight
let disable_animation: Bool
var PlaceholderColor: Color {
return id_to_color(pubkey)
@@ -111,7 +115,7 @@ struct InnerProfilePicView: View {
Color(uiColor: .systemBackground)
KFAnimatedImage(url)
.imageContext(.pfp)
.imageContext(.pfp, disable_animation: disable_animation)
.onFailure(fallbackUrl: fallbackUrl, cacheKey: url?.absoluteString)
.cancelOnDisappear(true)
.configure { view in
@@ -133,19 +137,21 @@ struct ProfilePicView: View {
let size: CGFloat
let highlight: Highlight
let profiles: Profiles
let disable_animation: Bool
@State var picture: String?
init (pubkey: String, size: CGFloat, highlight: Highlight, profiles: Profiles, picture: String? = nil) {
init (pubkey: String, size: CGFloat, highlight: Highlight, profiles: Profiles, disable_animation: Bool, picture: String? = nil) {
self.pubkey = pubkey
self.profiles = profiles
self.size = size
self.highlight = highlight
self._picture = State(initialValue: picture)
self.disable_animation = disable_animation
}
var body: some View {
InnerProfilePicView(url: get_profile_url(picture: picture, pubkey: pubkey, profiles: profiles), fallbackUrl: URL(string: robohash(pubkey)), pubkey: pubkey, size: size, highlight: highlight)
InnerProfilePicView(url: get_profile_url(picture: picture, pubkey: pubkey, profiles: profiles), fallbackUrl: URL(string: robohash(pubkey)), pubkey: pubkey, size: size, highlight: highlight, disable_animation: disable_animation)
.onReceive(handle_notify(.profile_updated)) { notif in
let updated = notif.object as! ProfileUpdate
@@ -185,7 +191,9 @@ struct ProfilePicView_Previews: PreviewProvider {
pubkey: pubkey,
size: 100,
highlight: .none,
profiles: make_preview_profiles(pubkey))
profiles: make_preview_profiles(pubkey),
disable_animation: false
)
}
}
@@ -23,11 +23,15 @@ struct ProfilePictureSelector: View {
@State var profile_image: URL? = nil
var uploader: MediaUploader {
damus_state?.settings.default_media_uploader ?? .nostrBuild
}
var body: some View {
let highlight: Highlight = .custom(Color.white, 2.0)
ZStack {
EditProfilePictureView(url: $profile_image, pubkey: pubkey, size: size, highlight: highlight, damus_state: damus_state)
EditProfilePictureControl(pubkey: pubkey, profile_image: $profile_image, viewModel: viewModel, callback: callback)
EditProfilePictureControl(uploader: uploader, pubkey: pubkey, profile_image: $profile_image, viewModel: viewModel, callback: callback)
}
}
}
+7 -6
View File
@@ -165,7 +165,7 @@ struct ProfileView: View {
return AnyView(
VStack(spacing: 0) {
ZStack {
BannerImageView(pubkey: profile.pubkey, profiles: damus_state.profiles)
BannerImageView(pubkey: profile.pubkey, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
.aspectRatio(contentMode: .fill)
.frame(width: proxy.size.width, height: minY > 0 ? bannerHeight + minY : bannerHeight)
.clipped()
@@ -210,7 +210,7 @@ struct ProfileView: View {
}) {
navImage(systemImage: "ellipsis")
}
.confirmationDialog(NSLocalizedString("Actions", comment: "Title for confirmation dialog to either share, report, or block a profile."), isPresented: $action_sheet_presented) {
.confirmationDialog(NSLocalizedString("Actions", comment: "Title for confirmation dialog to either share, report, or mute a profile."), isPresented: $action_sheet_presented) {
Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) {
show_share_sheet = true
}
@@ -257,7 +257,7 @@ struct ProfileView: View {
.profile_button_style(scheme: colorScheme)
.contextMenu {
if profile.reactions == false {
Text("OnlyZaps Enabled")
Text("OnlyZaps Enabled", comment: "Non-tappable text in context menu that shows up when the zap button on profile is long pressed to indicate that the user has enabled OnlyZaps, meaning that they would like to be only zapped and not accept reactions to their notes.")
}
if let addr = profile.lud16 {
@@ -278,7 +278,7 @@ struct ProfileView: View {
}
.cornerRadius(24)
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: lnurl)
SelectWalletView(default_wallet: damus_state.settings.default_wallet, showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: lnurl)
}
}
@@ -332,7 +332,7 @@ struct ProfileView: View {
func nameSection(profile_data: Profile?) -> some View {
return Group {
HStack(alignment: .center) {
ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles)
ProfilePicView(pubkey: profile.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
.padding(.top, -(pfp_size / 2.0))
.offset(y: pfpOffset())
.scaleEffect(pfpScale())
@@ -340,7 +340,8 @@ struct ProfileView: View {
is_zoomed.toggle()
}
.fullScreenCover(isPresented: $is_zoomed) {
ProfilePicImageView(pubkey: profile.pubkey, profiles: damus_state.profiles) }
ProfilePicImageView(pubkey: profile.pubkey, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
}
Spacer()
+1 -1
View File
@@ -44,7 +44,7 @@ struct QRCodeView: View {
let profile = damus_state.profiles.lookup(id: pubkey)
if (damus_state.profiles.lookup(id: pubkey)?.picture) != nil {
ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 4.0), profiles: damus_state.profiles)
ProfilePicView(pubkey: pubkey, size: 90.0, highlight: .custom(DamusColors.white, 4.0), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
.padding(.top, 50)
} else {
Image(systemName: "person.fill")
+2 -2
View File
@@ -31,8 +31,8 @@ struct RelayFilterView: View {
.padding(.top, 20)
.padding(.bottom, 0)
List(Array(relays), id: \.url) { relay in
RelayToggle(state: state, timeline: timeline, relay_id: relay.url.absoluteString)
List(Array(relays), id: \.url.id) { relay in
RelayToggle(state: state, timeline: timeline, relay_id: relay.url.url.absoluteString)
}
}
}
+4 -4
View File
@@ -23,7 +23,7 @@ struct RelayConfigView: View {
var recommended: [RelayDescriptor] {
let rs: [RelayDescriptor] = []
return BOOTSTRAP_RELAYS.reduce(into: rs) { xs, x in
if state.pool.get_relay(x) == nil, let url = URL(string: x) {
if state.pool.get_relay(x) == nil, let url = RelayURL(x) {
xs.append(RelayDescriptor(url: url, info: .rw))
}
}
@@ -75,7 +75,7 @@ struct RelayConfigView: View {
new_relay.removeLast();
}
guard let url = URL(string: new_relay) else {
guard let url = RelayURL(new_relay) else {
return
}
@@ -120,7 +120,7 @@ struct RelayConfigView: View {
Section {
List(Array(relays), id: \.url) { relay in
RelayView(state: state, relay: relay.url.absoluteString, showActionButtons: $showActionButtons)
RelayView(state: state, relay: relay.url.id, showActionButtons: $showActionButtons)
}
} header: {
HStack {
@@ -133,7 +133,7 @@ struct RelayConfigView: View {
if recommended.count > 0 {
Section {
List(recommended, id: \.url) { r in
RecommendedRelayView(damus: state, relay: r.url.absoluteString, showActionButtons: $showActionButtons)
RecommendedRelayView(damus: state, relay: r.url.id, showActionButtons: $showActionButtons)
}
} header: {
Text(NSLocalizedString("Recommended Relays", comment: "Section title for recommend relay servers that could be added as part of configuration"))
+1 -1
View File
@@ -24,7 +24,7 @@ struct RelayStatus: View {
if c.isConnected {
conn_image = "network"
conn_color = .green
} else if c.isConnecting || c.isReconnecting {
} else if c.isConnecting {
connecting = true
} else {
conn_image = "exclamationmark.circle.fill"
+6 -1
View File
@@ -6,6 +6,7 @@
//
import SwiftUI
import Security
struct SaveKeysView: View {
let account: CreateAccountModel
@@ -15,6 +16,8 @@ struct SaveKeysView: View {
@State var priv_copied: Bool = false
@State var loading: Bool = false
@State var error: String? = nil
@State private var credential_handler = CredentialHandler()
@FocusState var pubkey_focused: Bool
@FocusState var privkey_focused: Bool
@@ -97,6 +100,8 @@ struct SaveKeysView: View {
self.pool.register_handler(sub_id: "signup", handler: handle_event)
credential_handler.save_credential(pubkey: account.pubkey_bech32, privkey: account.privkey_bech32)
self.loading = true
self.pool.connect()
@@ -127,7 +132,7 @@ struct SaveKeysView: View {
case .error(let err):
self.loading = false
self.error = "\(err.debugDescription)"
self.error = String(describing: err)
default:
break
}
+5 -4
View File
@@ -49,8 +49,8 @@ struct SearchHomeView: View {
loading: $model.loading,
damus: damus_state,
show_friend_icon: true,
filter: {
if damus_state.muted_threads.isMutedThread($0, privkey: self.damus_state.keypair.privkey) {
filter: { ev in
if damus_state.muted_threads.isMutedThread(ev, privkey: self.damus_state.keypair.privkey) {
return false
}
@@ -59,11 +59,12 @@ struct SearchHomeView: View {
}
// If we can't determine the note's language with 50%+ confidence, lean on the side of caution and show it anyway.
guard let noteLanguage = $0.note_language(damus_state.keypair.privkey) else {
let note_lang = damus_state.events.get_cache_data(ev.id).translations_model.note_language
guard let note_lang else {
return true
}
return preferredLanguages.contains(noteLanguage)
return preferredLanguages.contains(note_lang)
}
)
.refreshable {
+121 -58
View File
@@ -7,82 +7,132 @@
import SwiftUI
enum Search {
struct MultiSearch {
let hashtag: String
let profiles: [SearchedUser]
}
enum Search: Identifiable {
case profiles([SearchedUser])
case hashtag(String)
case profile(String)
case note(String)
case nip05(String)
case hex(String)
case multi(MultiSearch)
var id: String {
switch self {
case .profiles: return "profiles"
case .hashtag: return "hashtag"
case .profile: return "profile"
case .note: return "note"
case .nip05: return "nip05"
case .hex: return "hex"
case .multi: return "multi"
}
}
}
struct SearchResultsView: View {
struct AnySearchResultsView: View {
let damus_state: DamusState
@Binding var search: String
let searches: [Search]
@State var result: Search? = nil
var body: some View {
VStack {
ForEach(searches) { r in
InnerSearchResults(damus_state: damus_state, search: r)
}
}
}
}
struct InnerSearchResults: View {
let damus_state: DamusState
let search: Search?
func ProfileSearchResult(pk: String) -> some View {
FollowUserView(target: .pubkey(pk), damus_state: damus_state)
}
var MainContent: some View {
ScrollView {
Group {
switch result {
case .profiles(let results):
LazyVStack {
ForEach(results) { prof in
ProfileSearchResult(pk: prof.pubkey)
}
}
case .hashtag(let ht):
let search_model = SearchModel(contacts: damus_state.contacts, pool: damus_state.pool, search: .filter_hashtag([ht]))
let dst = SearchView(appstate: damus_state, search: search_model)
NavigationLink(destination: dst) {
Text("Search hashtag: #\(ht)", comment: "Navigation link to search hashtag.")
}
case .nip05(let addr):
SearchingEventView(state: damus_state, evid: addr, search_type: .nip05)
case .profile(let prof):
let decoded = try? bech32_decode(prof)
let hex = hex_encode(decoded!.data)
SearchingEventView(state: damus_state, evid: hex, search_type: .profile)
case .hex(let h):
//let prof_view = ProfileView(damus_state: damus_state, pubkey: h)
//let ev_view = ThreadView(damus: damus_state, event_id: h)
VStack(spacing: 10) {
SearchingEventView(state: damus_state, evid: h, search_type: .event)
SearchingEventView(state: damus_state, evid: h, search_type: .profile)
}
case .note(let nid):
let decoded = try? bech32_decode(nid)
let hex = hex_encode(decoded!.data)
SearchingEventView(state: damus_state, evid: hex, search_type: .event)
case .none:
Text("none", comment: "No search results.")
}
func HashtagSearch(_ ht: String) -> some View {
let search_model = SearchModel(state: damus_state, search: .filter_hashtag([ht]))
let dst = SearchView(appstate: damus_state, search: search_model)
return NavigationLink(destination: dst) {
Text("Search hashtag: #\(ht)", comment: "Navigation link to search hashtag.")
}
}
func ProfilesSearch(_ results: [SearchedUser]) -> some View {
return LazyVStack {
ForEach(results) { prof in
ProfileSearchResult(pk: prof.pubkey)
}
.padding()
}
}
var body: some View {
MainContent
.frame(maxHeight: .infinity)
.onAppear {
self.result = search_for_string(profiles: damus_state.profiles, search)
}
.onChange(of: search) { new in
self.result = search_for_string(profiles: damus_state.profiles, new)
Group {
switch search {
case .profiles(let results):
ProfilesSearch(results)
case .hashtag(let ht):
HashtagSearch(ht)
case .nip05(let addr):
SearchingEventView(state: damus_state, evid: addr, search_type: .nip05)
case .profile(let prof):
let decoded = try? bech32_decode(prof)
let hex = hex_encode(decoded!.data)
SearchingEventView(state: damus_state, evid: hex, search_type: .profile)
case .hex(let h):
//let prof_view = ProfileView(damus_state: damus_state, pubkey: h)
//let ev_view = ThreadView(damus: damus_state, event_id: h)
VStack(spacing: 10) {
SearchingEventView(state: damus_state, evid: h, search_type: .event)
SearchingEventView(state: damus_state, evid: h, search_type: .profile)
}
case .note(let nid):
let decoded = try? bech32_decode(nid)
let hex = hex_encode(decoded!.data)
SearchingEventView(state: damus_state, evid: hex, search_type: .event)
case .multi(let multi):
VStack {
HashtagSearch(multi.hashtag)
ProfilesSearch(multi.profiles)
}
case .none:
Text("none", comment: "No search results.")
}
}
}
}
struct SearchResultsView: View {
let damus_state: DamusState
@Binding var search: String
@State var result: Search? = nil
var body: some View {
ScrollView {
InnerSearchResults(damus_state: damus_state, search: result)
.padding()
}
.frame(maxHeight: .infinity)
.onAppear {
self.result = search_for_string(profiles: damus_state.profiles, search)
}
.onChange(of: search) { new in
self.result = search_for_string(profiles: damus_state.profiles, new)
}
}
}
@@ -107,8 +157,7 @@ func search_for_string(profiles: Profiles, _ new: String) -> Search? {
}
if new.first! == "#" {
let ht = String(new.dropFirst().filter{$0 != " "})
return .hashtag(ht)
return .hashtag(make_hashtagable(new))
}
if hex_decode(new) != nil, new.count == 64 {
@@ -127,7 +176,21 @@ func search_for_string(profiles: Profiles, _ new: String) -> Search? {
}
}
return .profiles(search_profiles(profiles: profiles, search: new))
let multisearch = MultiSearch(hashtag: make_hashtagable(new), profiles: search_profiles(profiles: profiles, search: new))
return .multi(multisearch)
}
func make_hashtagable(_ str: String) -> String {
var new = str
guard str.utf8.count > 0 else {
return str
}
if new.hasPrefix("#") {
new = String(new.dropFirst())
}
return String(new.filter{$0 != " "})
}
func search_profiles(profiles: Profiles, search: String) -> [SearchedUser] {
+1 -2
View File
@@ -43,9 +43,8 @@ struct SearchView_Previews: PreviewProvider {
static var previews: some View {
let test_state = test_damus_state()
let filter = NostrFilter.filter_hashtag(["bitcoin"])
let pool = test_state.pool
let model = SearchModel(contacts: test_state.contacts, pool: pool, search: filter)
let model = SearchModel(state: test_state, search: filter)
SearchView(appstate: test_state, search: model)
}
+3 -3
View File
@@ -8,6 +8,7 @@
import SwiftUI
struct SelectWalletView: View {
let default_wallet: Wallet
@Binding var showingSelectWallet: Bool
let our_pubkey: String
let invoice: String
@@ -38,8 +39,7 @@ struct SelectWalletView: View {
Section(NSLocalizedString("Select a Lightning wallet", comment: "Title of section for selecting a Lightning wallet to pay a Lightning invoice.")) {
List{
Button() {
let wallet_model = get_default_wallet(our_pubkey).model
open_with_wallet(wallet: wallet_model, invoice: invoice)
open_with_wallet(wallet: default_wallet.model, invoice: invoice)
} label: {
HStack {
Text("Default Wallet", comment: "Button to pay a Lightning invoice with the user's default Lightning wallet.").font(.body).foregroundColor(.blue)
@@ -73,6 +73,6 @@ struct SelectWalletView_Previews: PreviewProvider {
@State static var invoice: String = ""
static var previews: some View {
SelectWalletView(showingSelectWallet: $show, our_pubkey: "", invoice: "")
SelectWalletView(default_wallet: .lnlink, showingSelectWallet: $show, our_pubkey: "", invoice: "")
}
}
@@ -27,15 +27,15 @@ struct AppearanceSettingsView: View {
}
Section(NSLocalizedString("Images", comment: "Section title for images configuration.")) {
Toggle(NSLocalizedString("Disable animations", comment: "Button to disable image animation"), isOn: $settings.disable_animation)
Toggle(NSLocalizedString("Animations", comment: "Toggle to enable or disable image animation"), isOn: $settings.enable_animation)
.toggleStyle(.switch)
.onChange(of: settings.disable_animation) { _ in
.onChange(of: settings.enable_animation) { _ in
clear_kingfisher_cache()
}
Toggle(NSLocalizedString("Always show images", comment: "Setting to always show and never blur images"), isOn: $settings.always_show_images)
.toggleStyle(.switch)
Picker(NSLocalizedString("Select image uploader", comment: "Prompt selection of user's image uploader"),
Picker(NSLocalizedString("Image uploader", comment: "Prompt selection of user's image uploader"),
selection: $settings.default_media_uploader) {
ForEach(MediaUploader.allCases, id: \.self) { uploader in
Text(uploader.model.displayName)
@@ -65,6 +65,17 @@ struct TranslationSettingsView: View {
}
}
if settings.translation_service == .nokyctranslate {
SecureField(NSLocalizedString("API Key (required)", comment: "Prompt for required entry of API Key to use translation server."), text: $settings.nokyctranslate_api_key)
.disableAutocorrection(true)
.disabled(settings.translation_service != .nokyctranslate)
.autocapitalization(UITextAutocapitalizationType.none)
if settings.nokyctranslate_api_key == "" {
Link(NSLocalizedString("Get API Key with BTC/Lightning", comment: "Button to navigate to nokyctranslate website to get a translation API key."), destination: URL(string: "https://nokyctranslate.com")!)
}
}
if settings.translation_service != .none {
Toggle(NSLocalizedString("Automatically translate notes", comment: "Toggle to automatically translate notes."), isOn: $settings.auto_translate)
.toggleStyle(.switch)
+5 -6
View File
@@ -17,8 +17,7 @@ struct ZapSettingsView: View {
init(pubkey: String, settings: UserSettingsStore) {
self.pubkey = pubkey
let zap_amt = get_default_zap_amount(pubkey: pubkey).formatted()
_default_zap_amount = State(initialValue: zap_amt)
_default_zap_amount = State(initialValue: settings.default_zap_amount.formatted())
self._settings = ObservedObject(initialValue: settings)
}
@@ -26,10 +25,10 @@ struct ZapSettingsView: View {
Form {
Section(
header: Text(NSLocalizedString("OnlyZaps", comment: "Section header for enabling OnlyZaps mode (hide reactions)")),
footer: Text(NSLocalizedString("Hide all 🤙's", comment: "Section footer describing onlyzaps mode"))
footer: Text(NSLocalizedString("Hide all 🤙's", comment: "Section footer describing OnlyZaps mode"))
) {
Toggle(NSLocalizedString("Enable OnlyZaps mode", comment: "Setting toggle to hide reactions."), isOn: $settings.onlyzaps_mode)
Toggle(NSLocalizedString("OnlyZaps mode", comment: "Setting toggle to hide reactions."), isOn: $settings.onlyzaps_mode)
.toggleStyle(.switch)
.onChange(of: settings.onlyzaps_mode) { newVal in
notify(.onlyzaps_mode, newVal)
@@ -59,10 +58,10 @@ struct ZapSettingsView: View {
.onReceive(Just(default_zap_amount)) { newValue in
if let parsed = handle_string_amount(new_value: newValue) {
self.default_zap_amount = parsed.formatted()
set_default_zap_amount(pubkey: self.pubkey, amount: parsed)
settings.default_zap_amount = parsed
} else {
self.default_zap_amount = ""
set_default_zap_amount(pubkey: self.pubkey, amount: 0)
settings.default_zap_amount = 0
}
}
}
+1 -1
View File
@@ -58,7 +58,7 @@ struct SideMenuView: View {
NavigationLink(destination: ProfileView(damus_state: damus_state, profile: profile_model, followers: followers)) {
HStack {
ProfilePicView(pubkey: damus_state.pubkey, size: 60, highlight: .none, profiles: damus_state.profiles)
ProfilePicView(pubkey: damus_state.pubkey, size: 60, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
VStack(alignment: .leading) {
if let display_name = profile?.display_name {
+25 -8
View File
@@ -10,7 +10,7 @@ import SwiftUI
struct InnerTimelineView: View {
@ObservedObject var events: EventHolder
let damus: DamusState
let state: DamusState
let show_friend_icon: Bool
let filter: (NostrEvent) -> Bool
@State var nav_target: NostrEvent
@@ -18,7 +18,7 @@ struct InnerTimelineView: View {
init(events: EventHolder, damus: DamusState, show_friend_icon: Bool, filter: @escaping (NostrEvent) -> Bool) {
self.events = events
self.damus = damus
self.state = damus
self.show_friend_icon = show_friend_icon
self.filter = filter
// dummy event to avoid MaybeThreadView
@@ -26,7 +26,7 @@ struct InnerTimelineView: View {
}
var event_options: EventViewOptions {
if self.damus.settings.truncate_timeline_text {
if self.state.settings.truncate_timeline_text {
return [.wide, .truncate_content]
}
@@ -34,8 +34,8 @@ struct InnerTimelineView: View {
}
var body: some View {
let thread = ThreadModel(event: nav_target, damus_state: damus)
let dest = ThreadView(state: damus, thread: thread)
let thread = ThreadModel(event: nav_target, damus_state: state)
let dest = ThreadView(state: state, thread: thread)
NavigationLink(destination: dest, isActive: $navigating) {
EmptyView()
}
@@ -44,13 +44,28 @@ struct InnerTimelineView: View {
if events.isEmpty {
EmptyTimelineView()
} else {
ForEach(events.filter(filter), id: \.id) { (ev: NostrEvent) in
EventView(damus: damus, event: ev, options: event_options)
let evs = events.filter(filter)
let indexed = Array(zip(evs, 0...))
ForEach(indexed, id: \.0.id) { tup in
let ev = tup.0
let ind = tup.1
EventView(damus: state, event: ev, options: event_options)
.onTapGesture {
nav_target = ev.inner_event ?? ev
nav_target = ev.get_inner_event(cache: state.events) ?? ev
navigating = true
}
.padding(.top, 7)
.onAppear {
let to_preload =
Array([indexed[safe: ind+1]?.0,
indexed[safe: ind+2]?.0,
indexed[safe: ind+3]?.0,
indexed[safe: ind+4]?.0,
indexed[safe: ind+5]?.0
].compactMap({ $0 }))
preload_events(event_cache: state.events, events: to_preload, profiles: state.profiles, our_keypair: state.keypair, settings: state.settings)
}
ThiccDivider()
.padding([.top], 7)
@@ -58,6 +73,7 @@ struct InnerTimelineView: View {
}
}
//.padding(.horizontal)
}
}
@@ -69,3 +85,4 @@ struct InnerTimelineView_Previews: PreviewProvider {
.border(Color.red)
}
}
+193 -113
View File
@@ -8,13 +8,6 @@
import SwiftUI
import Combine
enum ZapType {
case pub
case anon
case priv
case non_zap
}
struct ZapAmountItem: Identifiable, Hashable {
let amount: Int
let icon: String
@@ -24,15 +17,15 @@ struct ZapAmountItem: Identifiable, Hashable {
}
}
func get_default_zap_amount_item(_ pubkey: String) -> ZapAmountItem {
let def = get_default_zap_amount(pubkey: pubkey)
func get_default_zap_amount_item(_ def: Int) -> ZapAmountItem {
return ZapAmountItem(amount: def, icon: "🤙")
}
func get_zap_amount_items(pubkey: String) -> [ZapAmountItem] {
let def_item = get_default_zap_amount_item(pubkey)
func get_zap_amount_items(_ default_zap_amt: Int) -> [ZapAmountItem] {
let def_item = get_default_zap_amount_item(default_zap_amt)
var entries = [
ZapAmountItem(amount: 500, icon: "🙂"),
ZapAmountItem(amount: 69, icon: "😘"),
ZapAmountItem(amount: 420, icon: "🌿"),
ZapAmountItem(amount: 5000, icon: "💜"),
ZapAmountItem(amount: 10_000, icon: "😍"),
ZapAmountItem(amount: 20_000, icon: "🤩"),
@@ -53,74 +46,156 @@ struct CustomizeZapView: View {
@State var comment: String
@State var custom_amount: String
@State var custom_amount_sats: Int?
@State var selected_amount: ZapAmountItem
@State var zap_type: ZapType
@State var invoice: String
@State var error: String?
@State var showing_wallet_selector: Bool
@State var zapping: Bool
@State var show_zap_types: Bool = false
let zap_amounts: [ZapAmountItem]
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
func fillColor() -> Color {
colorScheme == .light ? DamusColors.white : DamusColors.black
}
func fontColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white
}
init(state: DamusState, event: NostrEvent, lnurl: String) {
self._comment = State(initialValue: "")
self.event = event
self.zap_amounts = get_zap_amount_items(pubkey: state.pubkey)
self.zap_amounts = get_zap_amount_items(state.settings.default_zap_amount)
self._error = State(initialValue: nil)
self._invoice = State(initialValue: "")
self._showing_wallet_selector = State(initialValue: false)
self._custom_amount = State(initialValue: "")
self._zap_type = State(initialValue: .pub)
let selected = get_default_zap_amount_item(state.pubkey)
self._selected_amount = State(initialValue: selected)
self._zap_type = State(initialValue: state.settings.default_zap_type)
self._custom_amount = State(initialValue: String(state.settings.default_zap_amount))
self._custom_amount_sats = State(initialValue: nil)
self._zapping = State(initialValue: false)
self.lnurl = lnurl
self.state = state
}
var zap_type_desc: String {
switch zap_type {
case .pub:
return NSLocalizedString("Everyone on can see that you zapped", comment: "Description of public zap type where the zap is sent publicly and identifies the user who sent it.")
case .anon:
return NSLocalizedString("No one can see that you zapped", comment: "Description of anonymous zap type where the zap is sent anonymously and does not identify the user who sent it.")
case .priv:
let pk = event.pubkey
let prof = state.profiles.lookup(id: pk)
let name = Profile.displayName(profile: prof, pubkey: pk).username
return String.localizedStringWithFormat(NSLocalizedString("private_zap_description", value: "Only '%@' can see that you zapped them", comment: "Description of private zap type where the zap is sent privately and does not identify the user to the public."), name)
case .non_zap:
return NSLocalizedString("No zaps are sent, only a lightning payment.", comment: "Description of non-zap type where sats are sent to the user's wallet as a regular Lightning payment, not as a zap.")
func amount_parts(_ n: Int) -> [ZapAmountItem] {
var i: Int = -1
let start = n * 3
let end = start + 3
return zap_amounts.filter { _ in
i += 1
return i >= start && i < end
}
}
var ZapTypePicker: some View {
Picker(NSLocalizedString("Zap Type", comment: "Header text to indicate that the picker below it is to choose the type of zap to send."), selection: $zap_type) {
Text("Public", comment: "Picker option to indicate that a zap should be sent publicly and identify the user as who sent it.").tag(ZapType.pub)
Text("Private", comment: "Picker option to indicate that a zap should be sent privately and not identify the user to the public.").tag(ZapType.priv)
Text("Anonymous", comment: "Picker option to indicate that a zap should be sent anonymously and not identify the user as who sent it.").tag(ZapType.anon)
Text(verbatim: NSLocalizedString("none_zap_type", value: "None", comment: "Picker option to indicate that sats should be sent to the user's wallet as a regular Lightning payment, not as a zap.")).tag(ZapType.non_zap)
func AmountsPart(n: Int) -> some View {
HStack(alignment: .center, spacing: 15) {
ForEach(amount_parts(n)) { entry in
ZapAmountButton(zapAmountItem: entry, action: {custom_amount_sats = entry.amount; custom_amount = String(entry.amount)})
}
}
.pickerStyle(.menu)
}
var AmountPicker: some View {
Picker(NSLocalizedString("Zap Amount", comment: "Title of picker that allows selection of predefined amounts to zap."), selection: $selected_amount) {
ForEach(zap_amounts) { entry in
let fmt = format_msats_abbrev(Int64(entry.amount) * 1000)
HStack(alignment: .firstTextBaseline) {
Text("\(entry.icon)")
.frame(width: 30)
Text("\(fmt)")
.frame(width: 50)
}
.tag(entry)
VStack {
AmountsPart(n: 0)
AmountsPart(n: 1)
AmountsPart(n: 2)
}
.padding(10)
}
func ZapAmountButton(zapAmountItem: ZapAmountItem, action: @escaping () -> ()) -> some View {
Button(action: action) {
let fmt = format_msats_abbrev(Int64(zapAmountItem.amount) * 1000)
Text(verbatim: "\(zapAmountItem.icon)\n\(fmt)")
.contentShape(Rectangle())
.font(.headline)
.frame(width: 70, height: 70)
.foregroundColor(fontColor())
.background(custom_amount_sats == zapAmountItem.amount ? fillColor() : DamusColors.adaptableGrey)
.cornerRadius(15)
.overlay(RoundedRectangle(cornerRadius: 15)
.stroke(DamusColors.purple.opacity(custom_amount_sats == zapAmountItem.amount ? 1.0 : 0.0), lineWidth: 2))
}
}
var CustomZapTextField: some View {
VStack(alignment: .center, spacing: 0) {
TextField("", text: $custom_amount)
.placeholder(when: custom_amount.isEmpty, alignment: .center) {
Text(String("0"))
}
.accentColor(.clear)
.font(.system(size: 72, weight: .heavy))
.minimumScaleFactor(0.01)
.keyboardType(.numberPad)
.multilineTextAlignment(.center)
.onReceive(Just(custom_amount)) { newValue in
if let parsed = handle_string_amount(new_value: newValue) {
self.custom_amount = parsed.formatted()
self.custom_amount_sats = parsed
} else {
self.custom_amount = ""
self.custom_amount_sats = nil
}
}
Text(custom_amount_sats == 1 ? "sat" : "sats", comment: "Shortened form of satoshi, display unit of measure where 1,000,000,000 satoshis is 1 Bitcoin. Used to indicate how many sats will be zapped to a note, configured through the custom zap view.")
.font(.system(size: 18, weight: .heavy))
}
}
var ZapReply: some View {
HStack {
if #available(iOS 16.0, *) {
TextField(NSLocalizedString("Send a reply with your zap...", comment: "Placeholder text for a comment to send as part of a zap to the user."), text: $comment, axis: .vertical)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
.lineLimit(5)
} else {
TextField(NSLocalizedString("Send a reply with your zap...", comment: "Placeholder text for a comment to send as part of a zap to the user."), text: $comment)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
}
}
.frame(minHeight: 30)
.padding(10)
.background(.secondary.opacity(0.2))
.cornerRadius(10)
.padding(.horizontal, 10)
}
var ZapButton: some View {
VStack {
if zapping {
Text("Zapping...", comment: "Text to indicate that the app is in the process of sending a zap.")
} else {
Button(NSLocalizedString("Zap", comment: "Button to send a zap.")) {
let amount = custom_amount_sats
send_zap(damus_state: state, event: event, lnurl: lnurl, is_custom: true, comment: comment, amount_sats: amount, zap_type: zap_type)
self.zapping = true
}
.disabled(custom_amount_sats == 0 || custom_amount.isEmpty)
.font(.system(size: 28, weight: .bold))
.frame(width: 130, height: 50)
.foregroundColor(.white)
.background(LINEAR_GRADIENT)
.opacity(custom_amount_sats == 0 || custom_amount.isEmpty ? 0.5 : 1.0)
.clipShape(Capsule())
}
if let error {
Text(error)
.foregroundColor(.red)
}
}
.pickerStyle(.wheel)
}
func receive_zap(notif: Notification) {
@@ -144,98 +219,103 @@ struct CustomizeZapView: View {
}
break
case .got_zap_invoice(let inv):
if should_show_wallet_selector(state.pubkey) {
if state.settings.show_wallet_selector {
self.invoice = inv
self.showing_wallet_selector = true
} else {
end_editing()
open_with_wallet(wallet: get_default_wallet(state.pubkey).model, invoice: inv)
let wallet = state.settings.default_wallet.model
open_with_wallet(wallet: wallet, invoice: inv)
self.showing_wallet_selector = false
dismiss()
}
}
}
var body: some View {
MainContent
.sheet(isPresented: $showing_wallet_selector) {
SelectWalletView(showingSelectWallet: $showing_wallet_selector, our_pubkey: state.pubkey, invoice: invoice)
SelectWalletView(default_wallet: state.settings.default_wallet, showingSelectWallet: $showing_wallet_selector, our_pubkey: state.pubkey, invoice: invoice)
}
.onReceive(handle_notify(.zapping)) { notif in
receive_zap(notif: notif)
}
.ignoresSafeArea()
.background(fillColor().edgesIgnoringSafeArea(.all))
.onTapGesture {
hideKeyboard()
}
}
var TheForm: some View {
Form {
Group {
Section(content: {
AmountPicker
.frame(height: 120)
}, header: {
Text("Zap Amount in sats", comment: "Header text to indicate that the picker below it is to choose a pre-defined amount of sats to zap.")
})
Section(content: {
// Use the selected sats amount as the placeholder text so that the UI is less confusing.
// User can type in their custom amount, which hides the placeholder.
TextField(selected_amount.amount.formatted(), text: $custom_amount)
.keyboardType(.numberPad)
.onReceive(Just(custom_amount)) { newValue in
if let parsed = handle_string_amount(new_value: newValue) {
self.custom_amount = parsed.formatted()
self.custom_amount_sats = parsed
} else {
self.custom_amount = ""
self.custom_amount_sats = nil
}
}
}, header: {
Text("Custom Zap Amount", comment: "Header text to indicate that the text field below it is to enter a custom zap amount.")
})
Section(content: {
TextField(NSLocalizedString("Awesome post!", comment: "Placeholder text for a comment to send as part of a zap to the user."), text: $comment)
}, header: {
Text("Comment", comment: "Header text to indicate that the text field below it is a comment that will be used to send as part of a zap to the user.")
})
func ZapTypeButton() -> some View {
Button(action: {
show_zap_types = true
}) {
switch zap_type {
case .pub:
Image(systemName: "person.2")
Text("Public", comment: "Button text to indicate that the zap type is a public zap.")
case .anon:
Image(systemName: "person.fill.questionmark")
Text("Anonymous", comment: "Button text to indicate that the zap type is a anonymous zap.")
case .priv:
Image(systemName: "lock")
Text("Private", comment: "Button text to indicate that the zap type is a private zap.")
case .non_zap:
Image(systemName: "bolt")
Text("None", comment: "Button text to indicate that the zap type is a private zap.")
}
.dismissKeyboardOnTap()
Section(content: {
ZapTypePicker
}, header: {
Text("Zap Type", comment: "Header text to indicate that the picker below it is to choose the type of zap to send.")
}, footer: {
Text(zap_type_desc)
})
}
.font(.headline)
.foregroundColor(fontColor())
.padding(EdgeInsets(top: 5, leading: 15, bottom: 5, trailing: 15))
.background(DamusColors.adaptableGrey)
.cornerRadius(15)
}
var CustomZap: some View {
VStack(alignment: .center, spacing: 20) {
ZapTypeButton()
.padding(.top, 50)
if zapping {
Text("Zapping...", comment: "Text to indicate that the app is in the process of sending a zap.")
Spacer()
CustomZapTextField
AmountPicker
ZapReply
ZapButton
Spacer()
Spacer()
}
.sheet(isPresented: $show_zap_types) {
if #available(iOS 16.0, *) {
ZapPicker
.presentationDetents([.medium])
.presentationDragIndicator(.visible)
} else {
Button(NSLocalizedString("Zap", comment: "Button to send a zap.")) {
let amount = custom_amount_sats ?? selected_amount.amount
send_zap(damus_state: state, event: event, lnurl: lnurl, is_custom: true, comment: comment, amount_sats: amount, zap_type: zap_type)
self.zapping = true
}
.zIndex(16)
ZapPicker
}
if let error {
Text(error)
.foregroundColor(.red)
}
}
}
var ZapPicker: some View {
ZapTypePicker(zap_type: $zap_type, settings: state.settings, profiles: state.profiles, pubkey: event.pubkey)
}
var MainContent: some View {
TheForm
CustomZap
}
}
extension View {
func hideKeyboard() {
let resign = #selector(UIResponder.resignFirstResponder)
UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil)
}
}
+122
View File
@@ -0,0 +1,122 @@
//
// ZapTypePicker.swift
// damus
//
// Created by William Casarin on 2023-04-23.
//
import SwiftUI
enum ZapType: String, StringCodable {
case pub
case anon
case priv
case non_zap
init?(from string: String) {
guard let v = ZapType(rawValue: string) else {
return nil
}
self = v
}
func to_string() -> String {
return self.rawValue
}
}
struct ZapTypePicker: View {
@Binding var zap_type: ZapType
@ObservedObject var settings: UserSettingsStore
let profiles: Profiles
let pubkey: String
@Environment(\.colorScheme) var colorScheme
func fillColor() -> Color {
colorScheme == .light ? DamusColors.white : DamusColors.black
}
func fontColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white
}
var is_default: Bool {
zap_type == settings.default_zap_type
}
var body: some View {
VStack(spacing: 20) {
HStack {
Text("Zap type", comment: "Text to indicate that the buttons below it is for choosing the type of zap to send.")
.font(.system(size: 25, weight: .heavy))
Spacer()
if !is_default {
Button(action: {
settings.default_zap_type = zap_type
}) {
Label(NSLocalizedString("Make Default", comment: "Button label to indicate that tapping it will make the selected zap type be the default for future zaps."), image: "checkmark.circle.fill")
}
}
}
ZapTypeSelection(text: "Public", comment: "Picker option to indicate that a zap should be sent publicly and identify the user as who sent it.", img: "person.2.circle.fill", action: {zap_type = ZapType.pub}, type: ZapType.pub)
ZapTypeSelection(text: "Private", comment: "Picker option to indicate that a zap should be sent privately and not identify the user to the public.", img: "lock.circle.fill", action: {zap_type = ZapType.priv}, type: ZapType.priv)
ZapTypeSelection(text: "Anonymous", comment: "Picker option to indicate that a zap should be sent anonymously and not identify the user as who sent it.", img: "person.crop.circle.fill.badge.questionmark", action: {zap_type = ZapType.anon}, type: ZapType.anon)
ZapTypeSelection(text: "None", comment: "Picker option to indicate that sats should be sent to the user's wallet as a regular Lightning payment, not as a zap.", img: "bolt.circle.fill", action: {zap_type = ZapType.non_zap}, type: ZapType.non_zap)
}
.padding(.horizontal)
}
func ZapTypeSelection(text: LocalizedStringKey, comment: StaticString, img: String, action: @escaping () -> (), type: ZapType) -> some View {
Button(action: action) {
VStack(alignment: .leading, spacing: 5) {
HStack {
Image(systemName: img)
.foregroundColor(.gray)
.font(.system(size: 24))
Text(text, comment: comment)
.font(.system(size: 20, weight: .semibold))
Spacer()
}
.padding(.horizontal)
Text(zap_type_desc(type: type, profiles: profiles, pubkey: pubkey))
.padding(.horizontal)
.foregroundColor(.gray)
.font(.system(size: 16))
}
}
.frame(minWidth: 400, maxWidth: .infinity, minHeight: 50, maxHeight: 70)
.foregroundColor(fontColor())
.background(zap_type == type ? fillColor() : DamusColors.adaptableGrey)
.cornerRadius(15)
.overlay(RoundedRectangle(cornerRadius: 15)
.stroke(DamusColors.purple.opacity(zap_type == type ? 1.0 : 0.0), lineWidth: 2))
}
}
struct ZapTypePicker_Previews: PreviewProvider {
@State static var zap_type: ZapType = .pub
@State static var default_type: ZapType = .pub
static var previews: some View {
let ds = test_damus_state()
ZapTypePicker(zap_type: $zap_type, settings: ds.settings, profiles: ds.profiles, pubkey: "bob")
}
}
func zap_type_desc(type: ZapType, profiles: Profiles, pubkey: String) -> String {
switch type {
case .pub:
return NSLocalizedString("Everyone will see that you zapped", comment: "Description of public zap type where the zap is sent publicly and identifies the user who sent it.")
case .anon:
return NSLocalizedString("No one will see that you zapped", comment: "Description of anonymous zap type where the zap is sent anonymously and does not identify the user who sent it.")
case .priv:
let prof = profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: prof, pubkey: pubkey).username
return String.localizedStringWithFormat(NSLocalizedString("private_zap_description", value: "Only '%@' will see that you zapped them", comment: "Description of private zap type where the zap is sent privately and does not identify the user to the public."), name)
case .non_zap:
return NSLocalizedString("No zaps will be sent, only a lightning payment.", comment: "Description of non-zap type where sats are sent to the user's wallet as a regular Lightning payment, not as a zap.")
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

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