Compare commits

..

182 Commits

Author SHA1 Message Date
tyiu 94504a116d 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
2023-04-22 11:45:06 +02:00
William Casarin 00aa897f05 build 8 2023-04-21 17:48:34 -07:00
William Casarin 71a1a6f0a3 Add some default height to images 2023-04-21 17:45:58 -07:00
William Casarin 9d22f40a53 Remove some cruft 2023-04-21 17:15:23 -07:00
William Casarin 0bd40c0018 Add Friends filter to DMs
Changelog-Added: Add friends filter to DMs
2023-04-21 16:55:59 -07:00
William Casarin ec75769a0f Refactor notification state filter saving and loading 2023-04-21 16:39:12 -07:00
William Casarin 47e349558c rename FineNotificationFilter to FriendFilter 2023-04-21 16:25:18 -07:00
William Casarin aa559b2916 Refactor and Scope user settings to pubkey 2023-04-21 16:21:01 -07:00
William Casarin 9bf8349db6 Friends filter for notifications
Changelog-Added: Friends filter for notifications
2023-04-21 14:17:37 -07:00
William Casarin 4c44de9276 Merge remote-tracking branch 'github/translations' 2023-04-21 11:27:35 -07:00
William Casarin aa5f8d19f7 build 7 2023-04-21 11:26:49 -07:00
William Casarin 040e452132 Fix having to set onlyzaps mode every time on restart
Changelog-Fixed: Fix having to set onlyzaps mode every time on restart
2023-04-21 10:01:32 -07:00
William Casarin ff41bb1b35 Update OnlyZaps wording: others can still pointlessly send you likes 2023-04-21 09:54:29 -07:00
transifex-integration[bot] efe231c122 Apply translations in nl [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'nl' language.
2023-04-21 16:52:37 +00:00
transifex-integration[bot] 0305712f65 Apply translations in es_419 [Manual Sync]
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'es_419' language.
2023-04-21 16:46:29 +00:00
William Casarin ba844aec97 Enable like button on OnlyZaps profiles for people who don't have OnlyZaps mode on
It seems like people still want to like posts even if the receiver isn't
going to see it.

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

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

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

* Apply translations in sv_SE

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

* Apply translations in ja

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

* Apply translations in ja

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

* Apply translations in el_GR

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

* Apply translations in el_GR

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

* Apply translations in el_GR

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

* Apply translations in el_GR

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

* Apply translations in el_GR

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

* Apply translations in el_GR

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

* Apply translations in el_GR

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

* Apply translations in el_GR

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

* Apply translations in el_GR

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

* Apply translations in el_GR

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

* Apply translations in hu_HU

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

* Apply translations in es_419

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

* Export strings for translation

* Apply translations in nl

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

* Apply translations in sv_SE

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

* Apply translations in ja

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

* Apply translations in hu_HU

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

* Apply translations in pl_PL

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

* Apply translations in zh_CN

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

* Apply translations in zh_CN

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

* Apply translations in zh_CN

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

* Apply translations in zh_CN

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

* Apply translations in zh_CN

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

* Apply translations in zh_CN

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

* Apply translations in zh_CN

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

* Apply translations in zh_CN

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

* Apply translations in zh_CN

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

* Apply translations in zh_CN

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

* Apply translations in zh_CN

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

* Apply translations in zh_CN

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

* Apply translations in zh_HK

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

* Apply translations in zh_TW

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

* Apply translations in zh_TW

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

* Apply translations in zh_TW

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

* Apply translations in zh_TW

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

* Apply translations in zh_TW

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

* Apply translations in zh_TW

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

* Apply translations in zh_TW

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

* Apply translations in zh_TW

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

---------

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

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

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

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

Changelog-Added: Add deep links for local notifications
Co-authored-by: William Casarin <jb55@jb55.com>
Closes: #880
2023-04-15 12:41:49 -07:00
William Casarin b5a3697d78 Refactor direct messages model
We can track the pubkey in the DirectMessageModel instead of having a
janky tuple.
2023-04-15 12:41:00 -07:00
William Casarin 247270f3d3 Introduce LocalNotification
This will be used for local notification data
2023-04-15 12:40:00 -07:00
William Casarin 9327068264 ContentView: open thread helper 2023-04-15 12:38:05 -07:00
transifex-integration[bot] c277c14bcd Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-15 18:48:00 +00:00
transifex-integration[bot] e688a691fc Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-15 18:47:38 +00:00
transifex-integration[bot] b470af8f1d Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-15 18:47:29 +00:00
transifex-integration[bot] efd1168217 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-15 18:46:02 +00:00
transifex-integration[bot] f7a3f9ab76 Apply translations in hu_HU
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'hu_HU' language.
2023-04-15 17:55:34 +00:00
Joel Klabo 95041600dc Changelog-Added: Banner Image Upload 2023-04-15 10:28:17 -07:00
William Casarin 8a8d2ebbc3 PostView: change wording on upload confirmation 2023-04-15 10:26:36 -07:00
William Casarin fad0a6b783 PostView: remove ! unwrap for mediaToUpload 2023-04-15 10:22:05 -07:00
Swift f5d7465368 Ask permission before uploading media
Changelog-Changed: Ask permission before uploading media
Closes: #886
2023-04-15 10:19:53 -07:00
OlegAba 8a785559c6 Fix tap area when mentioning users
Changelog-Fixed: Fix tap area when mentioning users
Closes: #895
2023-04-15 10:16:40 -07:00
William Casarin d4c8c15cc3 Revert "Revert "Fix tap area when mentioning users""
This reverts commit 735376b00f.
2023-04-15 09:59:55 -07:00
Ryan Calder 41a462871c Enable Mac Catalyst
Closes: #926
2023-04-15 09:47:21 -07:00
William Casarin 76a669acc2 Show DM message in local notification
Changelog-Changed: Show DM message in local notification
2023-04-15 09:46:13 -07:00
William Casarin 39236dc094 remove verbose logs 2023-04-15 09:44:17 -07:00
transifex-integration[bot] 5860125802 Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.
2023-04-15 10:29:15 +00:00
transifex-integration[bot] ae96c3b707 Apply translations in el_GR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.
2023-04-15 09:50:43 +00:00
transifex-integration[bot] 136f6f37e8 Apply translations in el_GR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.
2023-04-15 09:49:12 +00:00
transifex-integration[bot] 21f84f722b Apply translations in el_GR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.
2023-04-15 09:49:04 +00:00
transifex-integration[bot] 47747379ee Apply translations in nl
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-04-15 09:48:02 +00:00
transifex-integration[bot] a1b95d40e6 Apply translations in el_GR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.
2023-04-15 09:43:47 +00:00
transifex-integration[bot] 0fc69d862a Apply translations in ja
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ja' language.
2023-04-15 09:36:45 +00:00
tyiu fb0330476d Export strings for translation 2023-04-15 01:32:07 +02:00
transifex-integration[bot] 4b978594fa Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-15 01:31:27 +02:00
William Casarin f0bbba7a33 Fix invalid DM author notifications
Changelog-Fixed: Fix invalid DM author notifications
2023-04-14 11:46:55 -07:00
William Casarin b5faae9d1c Make reposts use postbox 2023-04-14 11:46:55 -07:00
William Casarin a4d4954abd Fix relay signal indicator, properly show how many relays you are connected to
Changelog-Fixed: Fix relay signal indicator, properly show how many relays you are connected to
2023-04-14 11:46:55 -07:00
William Casarin 735376b00f Revert "Fix tap area when mentioning users"
This reverts commit a2cd51b6e7.
2023-04-14 11:36:45 -07:00
William Casarin 042e02d2e4 regression: unbreak dms 2023-04-14 10:33:50 -07:00
William Casarin 40468b1603 refactor: dms view init logic 2023-04-14 10:33:20 -07:00
William Casarin 8c19ec1532 small refactor to include_event 2023-04-14 09:54:54 -07:00
tyiu 1ac9620242 Add thread muting
Changelog-Added: Add thread muting
Closes: #893
2023-04-14 09:43:36 -07:00
Swift d5ecc9bce4 Preview media uploads when posting
Changelog-Added: Preview media uploads when posting
Closes: #894
2023-04-14 09:32:27 -07:00
William Casarin d82b69aac5 Merge remote-tracking branch 'github/translations' 2023-04-14 09:24:59 -07:00
William Casarin bad6ba3643 qrcode: don't default pubkey to "" 2023-04-14 09:23:14 -07:00
ericholguin 5c131e62d7 Add QR Code in profiles
Changelog-Added: Add QR Code in profiles
Closes: #918
2023-04-14 09:20:10 -07:00
William Casarin 29ab48287f start v1.4.3-1 build train 2023-04-14 09:20:10 -07:00
transifex-integration[bot] 5c854519db Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.
2023-04-14 14:15:46 +00:00
transifex-integration[bot] 2cc04e24a3 Apply translations in el_GR
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'el_GR' language.
2023-04-14 11:51:53 +00:00
transifex-integration[bot] d24bea366d Apply translations in ar
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ar' language.
2023-04-14 03:20:41 +00:00
transifex-integration[bot] 85ce8cb93c Apply translations in nl
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-04-13 18:44:01 +00:00
tyiu 32bb8c365d Export strings for translation 2023-04-13 18:37:58 +02:00
transifex-integration[bot] d9285ab3ca Apply translations in nl
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'nl' language.
2023-04-13 18:33:59 +02:00
tyiu 3cba771655 Export strings for translation 2023-04-13 18:33:58 +02:00
tyiu f6f2517fda Merge French variant translations into general French translations 2023-04-13 18:33:58 +02:00
transifex-integration[bot] 047325e6b2 Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'cs' language.
2023-04-13 18:33:58 +02:00
transifex-integration[bot] ba2108d659 Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'cs' language.
2023-04-13 18:33:58 +02:00
transifex-integration[bot] 863c7baa8b Apply translations in cs
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'cs' language.
2023-04-13 18:33:58 +02:00
transifex-integration[bot] 8a5e95e47a Apply translations in fr
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'fr' language.
2023-04-13 18:33:58 +02:00
transifex-integration[bot] de0997216d Apply translations in fr
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'fr' language.
2023-04-13 18:33:58 +02:00
transifex-integration[bot] cc64c82ec4 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] e2ca02399b Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] 5418f55cee Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] eb65d473cd Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] dd337c4805 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] 2f6ed72f6d Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] d71bb33408 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] 72cfb2b071 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:57 +02:00
transifex-integration[bot] a67cb2df90 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'sv_SE' language.
2023-04-13 18:33:56 +02:00
William Casarin 23e9ce1455 v1.4.2 changelog 2023-04-13 09:27:57 -07:00
William Casarin 5f1132cbc8 v1.4.2-2 2023-04-12 20:11:08 -07:00
William Casarin 806c6257df A few more performance tweaks 2023-04-12 20:09:51 -07:00
William Casarin 18aafb086e Revert "Make tabs easier to click"
This reverts commit 2a2af056eb.
2023-04-12 19:49:52 -07:00
William Casarin fc534ea42d Avoid slow string byte counting functions 2023-04-12 19:26:28 -07:00
William Casarin 54c8958250 Fix a few more hitches 2023-04-12 18:22:16 -07:00
123 changed files with 1914 additions and 1356 deletions
+52
View File
@@ -1,3 +1,54 @@
## [1.4.3-2] - 2023-04-17
### Added
- Add deep links for local notifications (Swift)
- Add thread muting (Terry Yiu)
- Preview media uploads when posting (Swift)
- Add QR Code in profiles (ericholguin)
### Changed
- Always check signatures of profile events (William Casarin)
- Ask permission before uploading media (Swift)
- Show DM message in local notification (William Casarin)
### Fixed
- Fixed repost turning green too early and not reposting sometimes (Swift)
- Fix shuffling when choosing users to reply to (Joshua Jiang)
- Do not translate own notes if logged in with private key (Terry Yiu)
- Load missing profiles from boosts on home view (Gísli Kristjánsson)
- Load missing profiles from boosts on profile view (Gísli Kristjánsson)
- Fix tap area when mentioning users (OlegAba)
- Fix invalid DM author notifications (William Casarin)
- Fix relay signal indicator, properly show how many relays you are connected to (William Casarin)
[1.4.3-2]: https://github.com/damus-io/damus/releases/tag/v1.4.3-2
## [1.4.2-2] - 2023-04-12
### Added
- Include #btc in custom #bitcoin hashtag (William Casarin)
- Make notification dots configurable (William Casarin)
### Changed
- Display follows in most recent to oldest (Luis Cabrera)
### Fixed
- Fix hitches caused by syncronous loading of cached images (William Casarin)
- Fix tabs sometimes not switching (William Casarin)
[1.4.2-2]: https://github.com/damus-io/damus/releases/tag/v1.4.2-2
## [1.4.1-8] - 2023-04-10 ## [1.4.1-8] - 2023-04-10
### Added ### Added
@@ -987,3 +1038,4 @@
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2 [0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2
+4 -5
View File
@@ -10,7 +10,6 @@
#include <ctype.h> #include <ctype.h>
#include <string.h> #include <string.h>
#include "bech32.h"
typedef unsigned char u8; typedef unsigned char u8;
@@ -32,8 +31,8 @@ static inline int is_invalid_url_ending(char c) {
return c == '!' || c == '?' || c == ')' || c == '.' || c == ',' || c == ';'; return c == '!' || c == '?' || c == ')' || c == '.' || c == ',' || c == ';';
} }
static inline int is_bech32_character(char c) { static inline int is_alphanumeric(char c) {
return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || bech32_charset_rev[c] != -1; return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
} }
static inline void make_cursor(struct cursor *c, const u8 *content, size_t len) static inline void make_cursor(struct cursor *c, const u8 *content, size_t len)
@@ -75,14 +74,14 @@ static inline int consume_until_whitespace(struct cursor *cur, int or_end) {
return or_end; return or_end;
} }
static inline int consume_until_non_bech32_character(struct cursor *cur, int or_end) { static inline int consume_until_non_alphanumeric(struct cursor *cur, int or_end) {
char c; char c;
int consumedAtLeastOne = 0; int consumedAtLeastOne = 0;
while (cur->p < cur->end) { while (cur->p < cur->end) {
c = *cur->p; c = *cur->p;
if (!is_bech32_character(c)) if (!is_alphanumeric(c))
return consumedAtLeastOne; return consumedAtLeastOne;
cur->p++; cur->p++;
+1 -1
View File
@@ -222,7 +222,7 @@ int parse_nostr_bech32(struct cursor *cur, struct nostr_bech32 *obj) {
start = cur->p; start = cur->p;
if (!consume_until_non_bech32_character(cur, 1)) { if (!consume_until_non_alphanumeric(cur, 1)) {
cur->p = start; cur->p = start;
return 0; return 0;
} }
+50 -31
View File
@@ -142,8 +142,9 @@
4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */; }; 4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */; };
4C8D00CF29E38B950036AF10 /* nostr_bech32.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00CE29E38B950036AF10 /* nostr_bech32.c */; }; 4C8D00CF29E38B950036AF10 /* nostr_bech32.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00CE29E38B950036AF10 /* nostr_bech32.c */; };
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */; }; 4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */; };
4C8D1A6C29F1DFC200ACDF75 /* FriendIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D1A6B29F1DFC200ACDF75 /* FriendIcon.swift */; };
4C8D1A6F29F31E5000ACDF75 /* FriendsButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */; };
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */; }; 4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */; };
4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD152839DB54008EE7EF /* NostrMetadata.swift */; };
4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD17283A9EE5008EE7EF /* LoginView.swift */; }; 4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD17283A9EE5008EE7EF /* LoginView.swift */; };
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD19283AA67F008EE7EF /* Bech32.swift */; }; 4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD19283AA67F008EE7EF /* Bech32.swift */; };
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */; }; 4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */; };
@@ -154,6 +155,7 @@
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */; }; 4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */; };
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */; }; 4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */; };
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; }; 4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; };
4CA5588329F33F5B00DC6A45 /* StringCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA5588229F33F5B00DC6A45 /* StringCodable.swift */; };
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AC298851D000060CEA /* AccountDeletion.swift */; }; 4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AC298851D000060CEA /* AccountDeletion.swift */; };
4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */; }; 4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */; };
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */; }; 4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */; };
@@ -182,7 +184,6 @@
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */; }; 4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */; };
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEC297F0B9E00430951 /* Highlight.swift */; }; 4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEC297F0B9E00430951 /* Highlight.swift */; };
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEF297F11C700430951 /* SelectedEventView.swift */; }; 4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEF297F11C700430951 /* SelectedEventView.swift */; };
4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF1297F129C00430951 /* EmbeddedEventView.swift */; };
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF3297F18B400430951 /* ReplyDescription.swift */; }; 4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF3297F18B400430951 /* ReplyDescription.swift */; };
4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF5297F1A6A00430951 /* EventBody.swift */; }; 4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF5297F1A6A00430951 /* EventBody.swift */; };
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */; }; 4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */; };
@@ -192,6 +193,8 @@
4CCEB7B029B5415A0078AA28 /* SearchingProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */; }; 4CCEB7B029B5415A0078AA28 /* SearchingProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */; };
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; }; 4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; };
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; }; 4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128929E9D10C0006FA5A /* SignalView.swift */; };
4CDA128C29EB19C40006FA5A /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128B29EB19C40006FA5A /* LocalNotification.swift */; };
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */; }; 4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */; };
4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */; }; 4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */; };
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */; }; 4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */; };
@@ -276,7 +279,7 @@
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */; }; F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */; };
F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */; }; F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */; };
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */; }; F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */; };
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParicipantsView.swift */; }; F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParticipantsView.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -330,9 +333,6 @@
3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; }; 3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; 3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutedThreadsManager.swift; sourceTree = "<group>"; }; 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutedThreadsManager.swift; sourceTree = "<group>"; };
3A4F3320297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A4F3321297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A4F3322297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-FR"; path = "fr-FR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A5C4575296A879E0032D398 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-419"; path = "es-419.lproj/Localizable.stringsdict"; sourceTree = "<group>"; }; 3A5C4575296A879E0032D398 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-419"; path = "es-419.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; }; 3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A5CAE1E298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/Localizable.strings"; sourceTree = "<group>"; }; 3A5CAE1E298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/Localizable.strings"; sourceTree = "<group>"; };
@@ -340,6 +340,9 @@
3A66D927299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 3A66D927299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A66D928299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; }; 3A66D928299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
3A66D929299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; 3A66D929299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A821C3E29E819D500B4BCA7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
3A821C3F29E819D500B4BCA7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A821C4029E819D500B4BCA7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A827A18299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 3A827A18299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A827A19299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; }; 3A827A19299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
3A827A1A299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; 3A827A1A299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@@ -386,9 +389,6 @@
3AD14EB829C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "sv-SE"; path = "sv-SE.lproj/Localizable.stringsdict"; sourceTree = "<group>"; }; 3AD14EB829C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "sv-SE"; path = "sv-SE.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3AD14EB929C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sv-SE"; path = "sv-SE.lproj/InfoPlist.strings"; sourceTree = "<group>"; }; 3AD14EB929C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sv-SE"; path = "sv-SE.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AD14EBA29C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sv-SE"; path = "sv-SE.lproj/Localizable.strings"; sourceTree = "<group>"; }; 3AD14EBA29C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sv-SE"; path = "sv-SE.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AD14EBB29C40F47009D2D9C /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AD14EBC29C40F47009D2D9C /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-CA"; path = "fr-CA.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3AD14EBD29C40F47009D2D9C /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AD5662B29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/InfoPlist.strings; sourceTree = "<group>"; }; 3AD5662B29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3AD5662C29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fa; path = fa.lproj/Localizable.stringsdict; sourceTree = "<group>"; }; 3AD5662C29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fa; path = fa.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3AD5662D29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = "<group>"; }; 3AD5662D29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = "<group>"; };
@@ -551,8 +551,9 @@
4C8D00D129E397AD0036AF10 /* block.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = block.h; sourceTree = "<group>"; }; 4C8D00D129E397AD0036AF10 /* block.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = block.h; sourceTree = "<group>"; };
4C8D00D229E3C19F0036AF10 /* str_block.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = str_block.h; sourceTree = "<group>"; }; 4C8D00D229E3C19F0036AF10 /* str_block.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = str_block.h; sourceTree = "<group>"; };
4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP19Tests.swift; sourceTree = "<group>"; }; 4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP19Tests.swift; sourceTree = "<group>"; };
4C8D1A6B29F1DFC200ACDF75 /* FriendIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FriendIcon.swift; sourceTree = "<group>"; };
4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FriendsButton.swift; sourceTree = "<group>"; };
4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusColors.swift; sourceTree = "<group>"; }; 4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusColors.swift; sourceTree = "<group>"; };
4C90BD152839DB54008EE7EF /* NostrMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrMetadata.swift; sourceTree = "<group>"; };
4C90BD17283A9EE5008EE7EF /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; }; 4C90BD17283A9EE5008EE7EF /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
4C90BD19283AA67F008EE7EF /* Bech32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32.swift; sourceTree = "<group>"; }; 4C90BD19283AA67F008EE7EF /* Bech32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32.swift; sourceTree = "<group>"; };
4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Tests.swift; sourceTree = "<group>"; }; 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Tests.swift; sourceTree = "<group>"; };
@@ -563,6 +564,7 @@
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.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>"; }; 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>"; }; 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConfigView.swift; sourceTree = "<group>"; };
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyView.swift; sourceTree = "<group>"; }; 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyView.swift; sourceTree = "<group>"; };
@@ -591,7 +593,6 @@
4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuilderEventView.swift; sourceTree = "<group>"; }; 4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuilderEventView.swift; sourceTree = "<group>"; };
4CC7AAEC297F0B9E00430951 /* Highlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Highlight.swift; sourceTree = "<group>"; }; 4CC7AAEC297F0B9E00430951 /* Highlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Highlight.swift; sourceTree = "<group>"; };
4CC7AAEF297F11C700430951 /* SelectedEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedEventView.swift; sourceTree = "<group>"; }; 4CC7AAEF297F11C700430951 /* SelectedEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedEventView.swift; sourceTree = "<group>"; };
4CC7AAF1297F129C00430951 /* EmbeddedEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedEventView.swift; sourceTree = "<group>"; };
4CC7AAF3297F18B400430951 /* ReplyDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyDescription.swift; sourceTree = "<group>"; }; 4CC7AAF3297F18B400430951 /* ReplyDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyDescription.swift; sourceTree = "<group>"; };
4CC7AAF5297F1A6A00430951 /* EventBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBody.swift; sourceTree = "<group>"; }; 4CC7AAF5297F1A6A00430951 /* EventBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBody.swift; sourceTree = "<group>"; };
4CC7AAF7297F1CEE00430951 /* EventProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfile.swift; sourceTree = "<group>"; }; 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfile.swift; sourceTree = "<group>"; };
@@ -601,6 +602,8 @@
4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingProfileView.swift; sourceTree = "<group>"; }; 4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingProfileView.swift; sourceTree = "<group>"; };
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploadModel.swift; sourceTree = "<group>"; }; 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploadModel.swift; sourceTree = "<group>"; };
4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; }; 4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; };
4CDA128929E9D10C0006FA5A /* SignalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalView.swift; sourceTree = "<group>"; };
4CDA128B29EB19C40006FA5A /* LocalNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotification.swift; sourceTree = "<group>"; };
4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventHolder.swift; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InnerTimelineView.swift; sourceTree = "<group>"; };
@@ -687,7 +690,7 @@
F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIPURLBuilder.swift; sourceTree = "<group>"; }; F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIPURLBuilder.swift; sourceTree = "<group>"; };
F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfilePictureControl.swift; sourceTree = "<group>"; }; F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfilePictureControl.swift; sourceTree = "<group>"; };
F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToDismiss.swift; sourceTree = "<group>"; }; F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToDismiss.swift; sourceTree = "<group>"; };
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParicipantsView.swift; sourceTree = "<group>"; }; F7F0BA262978E54D009531F3 /* ParticipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantsView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -882,6 +885,7 @@
4C75EFA227FA576C0006080F /* Views */ = { 4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
4C8D1A6D29F31E4100ACDF75 /* Buttons */,
4C1A9A1B29DDCF8B00516EAC /* Settings */, 4C1A9A1B29DDCF8B00516EAC /* Settings */,
4CFF8F6129CC9A80008DB934 /* Images */, 4CFF8F6129CC9A80008DB934 /* Images */,
4CCEB7AC29B53D180078AA28 /* Search */, 4CCEB7AC29B53D180078AA28 /* Search */,
@@ -907,7 +911,6 @@
4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */, 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */,
4C216F31286E388800040376 /* DMChatView.swift */, 4C216F31286E388800040376 /* DMChatView.swift */,
4C216F33286F5ACD00040376 /* DMView.swift */, 4C216F33286F5ACD00040376 /* DMView.swift */,
E990020E2955F837003BBC5A /* EditMetadataView.swift */,
3169CAE4294E699400EE4006 /* Empty Views */, 3169CAE4294E699400EE4006 /* Empty Views */,
4C75EFB82804A2740006080F /* EventView.swift */, 4C75EFB82804A2740006080F /* EventView.swift */,
4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */, 4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */,
@@ -925,7 +928,7 @@
4C3AC7A42836987600E1F516 /* MainTabView.swift */, 4C3AC7A42836987600E1F516 /* MainTabView.swift */,
4C363A8B28236B92006E126D /* PubkeyView.swift */, 4C363A8B28236B92006E126D /* PubkeyView.swift */,
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */, 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */,
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */, F7F0BA262978E54D009531F3 /* ParticipantsView.swift */,
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */, 4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */,
4C3AC7A628369BA200E1F516 /* SearchHomeView.swift */, 4C3AC7A628369BA200E1F516 /* SearchHomeView.swift */,
4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */, 4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */,
@@ -962,7 +965,6 @@
4CACA9DB280C38C000D9BBE8 /* Profiles.swift */, 4CACA9DB280C38C000D9BBE8 /* Profiles.swift */,
4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */, 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */,
4C363A8F28247A1D006E126D /* NostrLink.swift */, 4C363A8F28247A1D006E126D /* NostrLink.swift */,
4C90BD152839DB54008EE7EF /* NostrMetadata.swift */,
); );
path = Nostr; path = Nostr;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1007,10 +1009,20 @@
4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */, 4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */,
4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */, 4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */,
4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */, 4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */,
4CDA128B29EB19C40006FA5A /* LocalNotification.swift */,
4CA5588229F33F5B00DC6A45 /* StringCodable.swift */,
); );
path = Util; path = Util;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
4C8D1A6D29F31E4100ACDF75 /* Buttons */ = {
isa = PBXGroup;
children = (
4C8D1A6E29F31E5000ACDF75 /* FriendsButton.swift */,
);
path = Buttons;
sourceTree = "<group>";
};
4CAAD8AE29888A9B00060CEA /* Relays */ = { 4CAAD8AE29888A9B00060CEA /* Relays */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -1022,6 +1034,7 @@
4CE8794D2996B16A00F758CC /* RelayToggle.swift */, 4CE8794D2996B16A00F758CC /* RelayToggle.swift */,
4CE8794F2996B2BD00F758CC /* RelayStatus.swift */, 4CE8794F2996B2BD00F758CC /* RelayStatus.swift */,
4CE879512996B68900F758CC /* RelayType.swift */, 4CE879512996B68900F758CC /* RelayType.swift */,
4CDA128929E9D10C0006FA5A /* SignalView.swift */,
); );
path = Relays; path = Relays;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1050,12 +1063,14 @@
4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */, 4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */,
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */, 4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */,
F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */, F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */,
E990020E2955F837003BBC5A /* EditMetadataView.swift */,
4CEE2AF2280B25C500AB5EEF /* ProfilePicView.swift */, 4CEE2AF2280B25C500AB5EEF /* ProfilePicView.swift */,
4C8682862814DE470026224F /* ProfileView.swift */, 4C8682862814DE470026224F /* ProfileView.swift */,
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */, 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */,
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */, 4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */,
4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */, 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */,
4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */, 4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */,
4C8D1A6B29F1DFC200ACDF75 /* FriendIcon.swift */,
); );
path = Profile; path = Profile;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1064,7 +1079,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
4CC7AAEF297F11C700430951 /* SelectedEventView.swift */, 4CC7AAEF297F11C700430951 /* SelectedEventView.swift */,
4CC7AAF1297F129C00430951 /* EmbeddedEventView.swift */,
4CC7AAF3297F18B400430951 /* ReplyDescription.swift */, 4CC7AAF3297F18B400430951 /* ReplyDescription.swift */,
4CC7AAF5297F1A6A00430951 /* EventBody.swift */, 4CC7AAF5297F1A6A00430951 /* EventBody.swift */,
4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */, 4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */,
@@ -1406,8 +1420,7 @@
"es-419", "es-419",
"es-ES", "es-ES",
fa, fa,
"fr-CA", fr,
"fr-FR",
"hu-HU", "hu-HU",
id, id,
"it-IT", "it-IT",
@@ -1482,6 +1495,7 @@
4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */, 4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */,
5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */, 5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */,
4C363A8A28236B57006E126D /* MentionView.swift in Sources */, 4C363A8A28236B57006E126D /* MentionView.swift in Sources */,
4C8D1A6C29F1DFC200ACDF75 /* FriendIcon.swift in Sources */,
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */, 4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */,
4C30AC7829A577AB00E2BD5A /* EventCache.swift in Sources */, 4C30AC7829A577AB00E2BD5A /* EventCache.swift in Sources */,
4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */, 4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */,
@@ -1495,6 +1509,7 @@
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */, 7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */,
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */, 4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */,
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */, 4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */,
4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */,
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */, 4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */, 4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */,
4C75EFB728049D990006080F /* RelayPool.swift in Sources */, 4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
@@ -1512,6 +1527,7 @@
4CC6193A29DC777C006A86D1 /* RelayBootstrap.swift in Sources */, 4CC6193A29DC777C006A86D1 /* RelayBootstrap.swift in Sources */,
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */, 4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */, 4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */,
4CA5588329F33F5B00DC6A45 /* StringCodable.swift in Sources */,
4C75EFB92804A2740006080F /* EventView.swift in Sources */, 4C75EFB92804A2740006080F /* EventView.swift in Sources */,
4C8D00C829DF791C0036AF10 /* CompatibleAttribute.swift in Sources */, 4C8D00C829DF791C0036AF10 /* CompatibleAttribute.swift in Sources */,
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */, 3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */,
@@ -1526,7 +1542,7 @@
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */, 4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */,
4C3D52B6298DB4E6001C5831 /* ZapEvent.swift in Sources */, 4C3D52B6298DB4E6001C5831 /* ZapEvent.swift in Sources */,
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */, 647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */,
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */, F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */,
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */, 4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */, 4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */,
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */, 4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */,
@@ -1534,6 +1550,7 @@
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */, 4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */,
4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */, 4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */,
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */, 4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
4C8D1A6F29F31E5000ACDF75 /* FriendsButton.swift in Sources */,
4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */, 4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */,
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */, 4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */, 4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */,
@@ -1579,7 +1596,6 @@
4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */, 4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */,
4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */, 4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */,
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */, 4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */,
4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */, 4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */,
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */, 4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */,
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */, 4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
@@ -1594,7 +1610,6 @@
4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */, 4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */,
4C3EA64928FF597700C48A62 /* bech32.c in Sources */, 4C3EA64928FF597700C48A62 /* bech32.c in Sources */,
4CE879522996B68900F758CC /* RelayType.swift in Sources */, 4CE879522996B68900F758CC /* RelayType.swift in Sources */,
4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */,
4CE8795B2996C47A00F758CC /* ZapsModel.swift in Sources */, 4CE8795B2996C47A00F758CC /* ZapsModel.swift in Sources */,
4C3A1D3729637E0500558C0F /* PreviewCache.swift in Sources */, 4C3A1D3729637E0500558C0F /* PreviewCache.swift in Sources */,
4C3EA67528FF7A5A00C48A62 /* take.c in Sources */, 4C3EA67528FF7A5A00C48A62 /* take.c in Sources */,
@@ -1650,6 +1665,7 @@
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */, 7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
6439E014296790CF0020672B /* ProfilePicImageView.swift in Sources */, 6439E014296790CF0020672B /* ProfilePicImageView.swift in Sources */,
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */, 4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */,
4CDA128C29EB19C40006FA5A /* LocalNotification.swift in Sources */,
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */, 4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */,
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */, 4C363AA428296DEE006E126D /* SearchModel.swift in Sources */,
4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */, 4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */,
@@ -1779,7 +1795,6 @@
3A5C4575296A879E0032D398 /* es-419 */, 3A5C4575296A879E0032D398 /* es-419 */,
3A2B8B0A296A8982009CC16D /* en-US */, 3A2B8B0A296A8982009CC16D /* en-US */,
3AEB8005297CCEA900713A25 /* tr-TR */, 3AEB8005297CCEA900713A25 /* tr-TR */,
3A4F3322297CCFEE004B5F72 /* fr-FR */,
3A185A06297F2C3800F4BDC0 /* lv-LV */, 3A185A06297F2C3800F4BDC0 /* lv-LV */,
3A929C22297F2CF80090925E /* it-IT */, 3A929C22297F2CF80090925E /* it-IT */,
3AB5B86C2986D8A3006599D2 /* de */, 3AB5B86C2986D8A3006599D2 /* de */,
@@ -1801,10 +1816,10 @@
3AD5663229C0DA4B00BF77C5 /* ko */, 3AD5663229C0DA4B00BF77C5 /* ko */,
3AD14EB529C40F38009D2D9C /* hu-HU */, 3AD14EB529C40F38009D2D9C /* hu-HU */,
3AD14EB829C40F3F009D2D9C /* sv-SE */, 3AD14EB829C40F3F009D2D9C /* sv-SE */,
3AD14EBC29C40F47009D2D9C /* fr-CA */,
3A325AC629C9E0B8002BE7ED /* vi */, 3A325AC629C9E0B8002BE7ED /* vi */,
3A325AC929C9E0CF002BE7ED /* es-ES */, 3A325AC929C9E0CF002BE7ED /* es-ES */,
3AC59CA929CDDB78007E04A6 /* pt-BR */, 3AC59CA929CDDB78007E04A6 /* pt-BR */,
3A821C4029E819D500B4BCA7 /* fr */,
); );
name = Localizable.stringsdict; name = Localizable.stringsdict;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1814,7 +1829,6 @@
children = ( children = (
3ACB685B297633BC00C46468 /* es-419 */, 3ACB685B297633BC00C46468 /* es-419 */,
3AEB8003297CCEA800713A25 /* tr-TR */, 3AEB8003297CCEA800713A25 /* tr-TR */,
3A4F3320297CCFEE004B5F72 /* fr-FR */,
3A185A04297F2C3800F4BDC0 /* lv-LV */, 3A185A04297F2C3800F4BDC0 /* lv-LV */,
3A929C20297F2CF80090925E /* it-IT */, 3A929C20297F2CF80090925E /* it-IT */,
3AB5B86A2986D8A3006599D2 /* de */, 3AB5B86A2986D8A3006599D2 /* de */,
@@ -1836,10 +1850,10 @@
3AD5663329C0DA4B00BF77C5 /* ko */, 3AD5663329C0DA4B00BF77C5 /* ko */,
3AD14EB629C40F38009D2D9C /* hu-HU */, 3AD14EB629C40F38009D2D9C /* hu-HU */,
3AD14EB929C40F3F009D2D9C /* sv-SE */, 3AD14EB929C40F3F009D2D9C /* sv-SE */,
3AD14EBB29C40F47009D2D9C /* fr-CA */,
3A325AC529C9E0B8002BE7ED /* vi */, 3A325AC529C9E0B8002BE7ED /* vi */,
3A325AC829C9E0CF002BE7ED /* es-ES */, 3A325AC829C9E0CF002BE7ED /* es-ES */,
3AC59CA829CDDB78007E04A6 /* pt-BR */, 3AC59CA829CDDB78007E04A6 /* pt-BR */,
3A821C3F29E819D500B4BCA7 /* fr */,
); );
name = InfoPlist.strings; name = InfoPlist.strings;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -1849,7 +1863,6 @@
children = ( children = (
3ACB685E297633BC00C46468 /* es-419 */, 3ACB685E297633BC00C46468 /* es-419 */,
3AEB8004297CCEA800713A25 /* tr-TR */, 3AEB8004297CCEA800713A25 /* tr-TR */,
3A4F3321297CCFEE004B5F72 /* fr-FR */,
3A185A05297F2C3800F4BDC0 /* lv-LV */, 3A185A05297F2C3800F4BDC0 /* lv-LV */,
3A929C21297F2CF80090925E /* it-IT */, 3A929C21297F2CF80090925E /* it-IT */,
3AB5B86B2986D8A3006599D2 /* de */, 3AB5B86B2986D8A3006599D2 /* de */,
@@ -1872,10 +1885,10 @@
3AD5663129C0DA4B00BF77C5 /* ko */, 3AD5663129C0DA4B00BF77C5 /* ko */,
3AD14EB729C40F38009D2D9C /* hu-HU */, 3AD14EB729C40F38009D2D9C /* hu-HU */,
3AD14EBA29C40F3F009D2D9C /* sv-SE */, 3AD14EBA29C40F3F009D2D9C /* sv-SE */,
3AD14EBD29C40F47009D2D9C /* fr-CA */,
3A325AC429C9E0B8002BE7ED /* vi */, 3A325AC429C9E0B8002BE7ED /* vi */,
3A325AC729C9E0CF002BE7ED /* es-ES */, 3A325AC729C9E0CF002BE7ED /* es-ES */,
3AC59CA729CDDB78007E04A6 /* pt-BR */, 3AC59CA729CDDB78007E04A6 /* pt-BR */,
3A821C3E29E819D500B4BCA7 /* fr */,
); );
name = Localizable.strings; name = Localizable.strings;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -2011,7 +2024,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 8;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D; DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -2036,9 +2049,12 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)", "$(PROJECT_DIR)",
); );
MARKETING_VERSION = 1.4.2; MARKETING_VERSION = 1.4.3;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2; PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "damus-c/damus-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "damus-c/damus-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -2055,7 +2071,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 8;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D; DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -2080,9 +2096,12 @@
"$(inherited)", "$(inherited)",
"$(PROJECT_DIR)", "$(PROJECT_DIR)",
); );
MARKETING_VERSION = 1.4.2; MARKETING_VERSION = 1.4.3;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2; PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = YES;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "damus-c/damus-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "damus-c/damus-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
+1 -2
View File
@@ -37,8 +37,6 @@ struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
text text
.padding(EdgeInsets(top: 15, leading: 0, bottom: 10, trailing: 0)) .padding(EdgeInsets(top: 15, leading: 0, bottom: 10, trailing: 0))
.font(.system(size: 14, weight: .heavy)) .font(.system(size: 14, weight: .heavy))
.contentShape(Rectangle())
.frame(maxWidth: .infinity)
} }
.background( .background(
Group { Group {
@@ -50,6 +48,7 @@ struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
}, },
alignment: .bottom alignment: .bottom
) )
.frame(maxWidth: .infinity)
.accentColor(tag == selection ? textColor() : .gray) .accentColor(tag == selection ? textColor() : .gray)
} }
} }
+2 -5
View File
@@ -66,7 +66,7 @@ struct ImageCarousel: View {
} }
var height: CGFloat { var height: CGFloat {
image_fill?.height ?? 0 image_fill?.height ?? 100
} }
var body: some View { var body: some View {
@@ -118,10 +118,7 @@ extension KFOptionSetter {
let modifier = AnyImageModifier { image -> KFCrossPlatformImage in let modifier = AnyImageModifier { image -> KFCrossPlatformImage in
let img_size = image.size let img_size = image.size
let geo_size = size let geo_size = size
let fill = ImageFill.calculate_image_fill(geo_size: geo_size, let fill = ImageFill.calculate_image_fill(geo_size: geo_size, img_size: img_size, maxHeight: max, fillHeight: fill)
img_size: img_size,
maxHeight: max,
fillHeight: fill)
DispatchQueue.main.async { [block, fill] in DispatchQueue.main.async { [block, fill] in
try? block(fill) try? block(fill)
} }
+2 -2
View File
@@ -35,10 +35,10 @@ struct NIP05Badge: View {
.mask(Image(systemName: "checkmark.seal.fill") .mask(Image(systemName: "checkmark.seal.fill")
.resizable() .resizable()
).frame(width: 14, height: 14) ).frame(width: 14, height: 14)
} else { } else if show_domain {
Image(systemName: "checkmark.seal.fill") Image(systemName: "checkmark.seal.fill")
.font(.footnote) .font(.footnote)
.foregroundColor(.gray) .nip05_colorized(gradient: nip05_color)
} }
} }
} }
+8 -2
View File
@@ -40,8 +40,14 @@ struct TranslateView: View {
} else { } else {
self.currentLanguage = Locale.current.languageCode ?? "en" self.currentLanguage = Locale.current.languageCode ?? "en"
} }
if let cached = damus_state.events.lookup_translated_artifacts(evid: event.id) { 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) self._translated = State(initialValue: cached)
} else { } else {
let initval: TranslateStatus = self.damus_state.settings.auto_translate ? .trying : .havent_tried let initval: TranslateStatus = self.damus_state.settings.auto_translate ? .trying : .havent_tried
+23 -1
View File
@@ -7,11 +7,34 @@
import SwiftUI import SwiftUI
struct UserViewRow: View {
let damus_state: DamusState
let pubkey: String
@State var navigating: Bool = false
var body: some View {
let dest = ProfileView(damus_state: damus_state, pubkey: pubkey)
UserView(damus_state: damus_state, pubkey: pubkey)
.contentShape(Rectangle())
.background(
NavigationLink(destination: dest, isActive: $navigating) {
EmptyView()
}
)
.onTapGesture {
navigating = true
}
}
}
struct UserView: View { struct UserView: View {
let damus_state: DamusState let damus_state: DamusState
let pubkey: String let pubkey: String
var body: some View { var body: some View {
VStack { VStack {
HStack { 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)
@@ -28,7 +51,6 @@ struct UserView: View {
Spacer() Spacer()
} }
Spacer()
} }
} }
} }
+1 -1
View File
@@ -173,7 +173,7 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
damus_state.lnurls.endpoints[target.pubkey] = payreq damus_state.lnurls.endpoints[target.pubkey] = payreq
} }
let zap_amount = amount_sats ?? get_default_zap_amount(pubkey: damus_state.pubkey) ?? 1000 let zap_amount = amount_sats ?? get_default_zap_amount(pubkey: damus_state.pubkey)
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount, zap_type: zap_type, comment: comment) else { guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount, zap_type: zap_type, comment: comment) else {
DispatchQueue.main.async { DispatchQueue.main.async {
+111 -47
View File
@@ -15,17 +15,15 @@ struct TimestampedProfile {
} }
enum Sheets: Identifiable { enum Sheets: Identifiable {
case post case post(PostAction)
case report(ReportTarget) case report(ReportTarget)
case reply(NostrEvent)
case event(NostrEvent) case event(NostrEvent)
case filter case filter
var id: String { var id: String {
switch self { switch self {
case .report: return "report" case .report: return "report"
case .post: return "post" case .post(let action): return "post-" + (action.ev?.id ?? "")
case .reply(let ev): return "reply-" + ev.id
case .event(let ev): return "event-" + ev.id case .event(let ev): return "event-" + ev.id
case .filter: return "filter" case .filter: return "filter"
} }
@@ -66,7 +64,6 @@ struct ContentView: View {
@State var active_sheet: Sheets? = nil @State var active_sheet: Sheets? = nil
@State var damus_state: DamusState? = nil @State var damus_state: DamusState? = nil
@State var selected_timeline: Timeline? = .home @State var selected_timeline: Timeline? = .home
@State var is_thread_open: Bool = false
@State var is_deleted_account: Bool = false @State var is_deleted_account: Bool = false
@State var is_profile_open: Bool = false @State var is_profile_open: Bool = false
@State var event: NostrEvent? = nil @State var event: NostrEvent? = nil
@@ -84,6 +81,7 @@ struct ContentView: View {
@State var filter_state : FilterState = .posts_and_replies @State var filter_state : FilterState = .posts_and_replies
@State private var isSideBarOpened = false @State private var isSideBarOpened = false
@StateObject var home: HomeModel = HomeModel() @StateObject var home: HomeModel = HomeModel()
@State var shouldShowBoostAlert = false
// connect retry timer // connect retry timer
let timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect() let timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect()
@@ -91,14 +89,19 @@ struct ContentView: View {
let sub_id = UUID().description let sub_id = UUID().description
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
var mystery: some View {
Text("Are you lost?", comment: "Text asking the user if they are lost in the app.")
.id("what")
}
var PostingTimelineView: some View { var PostingTimelineView: some View {
VStack { VStack {
ZStack { ZStack {
TabView(selection: $filter_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. // This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why.
Text("") mystery
.id("what")
contentTimelineView(filter: FilterState.posts.filter) contentTimelineView(filter: FilterState.posts.filter)
.tag(FilterState.posts) .tag(FilterState.posts)
.id(FilterState.posts) .id(FilterState.posts)
@@ -110,7 +113,7 @@ struct ContentView: View {
if privkey != nil { if privkey != nil {
PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) { PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) {
self.active_sheet = .post self.active_sheet = .post(.posting)
} }
} }
} }
@@ -179,8 +182,7 @@ struct ContentView: View {
NotificationsView(state: damus, notifications: home.notifications) NotificationsView(state: damus, notifications: home.notifications)
case .dms: case .dms:
DirectMessagesView(damus_state: damus_state!) DirectMessagesView(damus_state: damus_state!, model: damus_state!.dms, settings: damus_state!.settings)
.environmentObject(home.dms)
case .none: case .none:
EmptyView() EmptyView()
@@ -243,6 +245,11 @@ struct ContentView: View {
} }
} }
func open_event(ev: NostrEvent) {
self.active_event = ev
self.thread_open = true
}
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
if let damus = self.damus_state { if let damus = self.damus_state {
@@ -263,13 +270,7 @@ struct ContentView: View {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
HStack(alignment: .center) { HStack(alignment: .center) {
if home.signal.signal != home.signal.max_signal { SignalView(state: damus_state!, signal: home.signal)
NavigationLink(destination: RelayConfigView(state: damus_state!)) {
Text("\(home.signal.signal)/\(home.signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
.font(.callout)
.foregroundColor(.gray)
}
}
// maybe expand this to other timelines in the future // maybe expand this to other timelines in the future
if selected_timeline == .search { if selected_timeline == .search {
@@ -308,10 +309,8 @@ struct ContentView: View {
switch item { switch item {
case .report(let target): case .report(let target):
MaybeReportView(target: target) MaybeReportView(target: target)
case .post: case .post(let action):
PostView(replying_to: nil, damus_state: damus_state!) PostView(action: action, damus_state: damus_state!)
case .reply(let event):
PostView(replying_to: event, damus_state: damus_state!)
case .event: case .event:
EventDetailView() EventDetailView()
case .filter: case .filter:
@@ -338,10 +337,9 @@ struct ContentView: View {
} else if ref.key == "e" { } else if ref.key == "e" {
find_event(state: damus_state!, evid: ref.ref_id, search_type: .event, find_from: nil) { ev in find_event(state: damus_state!, evid: ref.ref_id, search_type: .event, find_from: nil) { ev in
if let ev { if let ev {
active_event = ev open_event(ev: ev)
} }
} }
thread_open = true
} }
case .filter(let filt): case .filter(let filt):
active_search = filt active_search = filt
@@ -352,16 +350,16 @@ struct ContentView: View {
} }
.onReceive(handle_notify(.boost)) { notif in .onReceive(handle_notify(.boost)) { notif in
current_boost = (notif.object as? NostrEvent) guard let ev = notif.object as? NostrEvent else {
} return
.onReceive(handle_notify(.open_thread)) { obj in }
//let ev = obj.object as! NostrEvent
//thread.set_active_event(ev) current_boost = ev
//is_thread_open = true shouldShowBoostAlert = true
} }
.onReceive(handle_notify(.reply)) { notif in .onReceive(handle_notify(.reply)) { notif in
let ev = notif.object as! NostrEvent let ev = notif.object as! NostrEvent
self.active_sheet = .reply(ev) self.active_sheet = .post(.replying_to(ev))
} }
.onReceive(handle_notify(.like)) { like in .onReceive(handle_notify(.like)) { like in
} }
@@ -469,13 +467,56 @@ struct ContentView: View {
self.damus_state?.pool.connect_to_disconnected() self.damus_state?.pool.connect_to_disconnected()
} }
.onReceive(handle_notify(.new_mutes)) { notif in .onReceive(handle_notify(.new_mutes)) { notif in
home.filter_muted() home.filter_events()
} }
.onReceive(handle_notify(.mute_thread)) { notif in .onReceive(handle_notify(.mute_thread)) { notif in
home.filter_muted() home.filter_events()
} }
.onReceive(handle_notify(.unmute_thread)) { notif in .onReceive(handle_notify(.unmute_thread)) { notif in
home.filter_muted() home.filter_events()
}
.onReceive(handle_notify(.local_notification)) { notif in
guard let local = notif.object as? LossyLocalNotification,
let damus_state else {
return
}
guard let target = damus_state.events.lookup(local.event_id) else {
return
}
switch local.type {
case .dm:
selected_timeline = .dms
damus_state.dms.open_dm_by_pk(target.pubkey)
case .like: fallthrough
case .zap: fallthrough
case .mention: fallthrough
case .repost:
open_event(ev: target)
}
}
.onReceive(handle_notify(.onlyzaps_mode)) { notif in
let hide = notif.object as! Bool
home.filter_events()
guard let damus_state else {
return
}
guard let profile = damus_state.profiles.lookup(id: damus_state.pubkey) else {
return
}
profile.reactions = !hide
guard let profile_ev = make_metadata_event(keypair: damus_state.keypair, metadata: profile) else {
return
}
damus_state.postbox.send(profile_ev)
} }
.alert(NSLocalizedString("Deleted Account", comment: "Alert message to indicate this is a deleted account"), isPresented: $is_deleted_account) { .alert(NSLocalizedString("Deleted Account", comment: "Alert message to indicate this is a deleted account"), isPresented: $is_deleted_account) {
Button(NSLocalizedString("Logout", comment: "Button to close the alert that informs that the current account has been deleted.")) { Button(NSLocalizedString("Logout", comment: "Button to close the alert that informs that the current account has been deleted.")) {
@@ -564,17 +605,35 @@ struct ContentView: View {
Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.") Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.")
} }
}) })
.alert(NSLocalizedString("Repost", comment: "Title of alert for confirming to repost a post."), isPresented: $current_boost.mappedToBool()) { .confirmationDialog("Repost", isPresented: $shouldShowBoostAlert) {
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of reposting a post.")) { Button(NSLocalizedString("Repost", comment: "Title of alert for confirming to repost a post.")) {
current_boost = nil guard let current_boost else {
} return
Button(NSLocalizedString("Repost", comment: "Button to confirm reposting a post.")) {
if let current_boost {
self.damus_state?.pool.send(.event(current_boost))
} }
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
} }
} message: {
Text("Are you sure you want to repost this?", comment: "Alert message to ask if user wants to repost a post.")
} }
} }
@@ -619,7 +678,12 @@ struct ContentView: View {
} }
pool.register_handler(sub_id: sub_id, handler: home.handle_event) pool.register_handler(sub_id: sub_id, handler: home.handle_event)
// dumb stuff needed for property wrappers
UserSettingsStore.pubkey = pubkey
let settings = UserSettingsStore()
UserSettingsStore.shared = settings
self.damus_state = DamusState(pool: pool, self.damus_state = DamusState(pool: pool,
keypair: keypair, keypair: keypair,
likes: EventCounter(our_pubkey: pubkey), likes: EventCounter(our_pubkey: pubkey),
@@ -631,7 +695,7 @@ struct ContentView: View {
previews: PreviewCache(), previews: PreviewCache(),
zaps: Zaps(our_pubkey: pubkey), zaps: Zaps(our_pubkey: pubkey),
lnurls: LNUrls(), lnurls: LNUrls(),
settings: UserSettingsStore(), settings: settings,
relay_filters: relay_filters, relay_filters: relay_filters,
relay_metadata: metadatas, relay_metadata: metadatas,
drafts: Drafts(), drafts: Drafts(),
@@ -640,7 +704,7 @@ struct ContentView: View {
postbox: PostBox(pool: pool), postbox: PostBox(pool: pool),
bootstrap_relays: bootstrap_relays, bootstrap_relays: bootstrap_relays,
replies: ReplyCounter(our_pubkey: pubkey), replies: ReplyCounter(our_pubkey: pubkey),
muted_threads: MutedThreadsManager(pubkey: pubkey) muted_threads: MutedThreadsManager(keypair: keypair)
) )
home.damus_state = self.damus_state! home.damus_state = self.damus_state!
@@ -802,7 +866,7 @@ func find_event(state: DamusState, evid: String, search_type: SearchType, find_f
var filter = search_type == .event ? NostrFilter.filter_ids([ evid ]) : NostrFilter.filter_authors([ evid ]) var filter = search_type == .event ? NostrFilter.filter_ids([ evid ]) : NostrFilter.filter_authors([ evid ])
if search_type == .profile { if search_type == .profile {
filter.kinds = [0] filter.kinds = [NostrKind.metadata.rawValue]
} }
filter.limit = 1 filter.limit = 1
+3 -2
View File
@@ -39,7 +39,8 @@ struct DamusState {
keypair.privkey != nil keypair.privkey != nil
} }
static var settings_pubkey: String? = nil
static var empty: DamusState { static var empty: DamusState {
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(pubkey: "")) return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil))) }
}
} }
+13 -1
View File
@@ -7,7 +7,19 @@
import Foundation import Foundation
enum DeepLPlan: String, CaseIterable, Identifiable { enum DeepLPlan: String, CaseIterable, Identifiable, StringCodable {
init?(from string: String) {
guard let dl = DeepLPlan(rawValue: string) else {
return nil
}
self = dl
}
func to_string() -> String {
return self.rawValue
}
var id: String { self.rawValue } var id: String { self.rawValue }
struct Model: Identifiable, Hashable { struct Model: Identifiable, Hashable {
+6 -2
View File
@@ -16,6 +16,8 @@ class DirectMessageModel: ObservableObject {
@Published var draft: String @Published var draft: String
let pubkey: String
var is_request: Bool var is_request: Bool
var our_pubkey: String var our_pubkey: String
@@ -29,17 +31,19 @@ class DirectMessageModel: ObservableObject {
return true return true
} }
init(events: [NostrEvent], our_pubkey: String) { init(events: [NostrEvent], our_pubkey: String, pubkey: String) {
self.events = events self.events = events
self.is_request = false self.is_request = false
self.our_pubkey = our_pubkey self.our_pubkey = our_pubkey
self.draft = "" self.draft = ""
self.pubkey = pubkey
} }
init(our_pubkey: String) { init(our_pubkey: String, pubkey: String) {
self.events = [] self.events = []
self.is_request = false self.is_request = false
self.our_pubkey = our_pubkey self.our_pubkey = our_pubkey
self.draft = "" self.draft = ""
self.pubkey = pubkey
} }
} }
+32 -9
View File
@@ -8,20 +8,43 @@
import Foundation import Foundation
class DirectMessagesModel: ObservableObject { class DirectMessagesModel: ObservableObject {
@Published var dms: [(String, DirectMessageModel)] = [] @Published var dms: [DirectMessageModel] = []
@Published var loading: Bool = false @Published var loading: Bool = false
@Published var open_dm: Bool = false
@Published private(set) var active_model: DirectMessageModel = DirectMessageModel(our_pubkey: "", pubkey: "")
let our_pubkey: String let our_pubkey: String
init(our_pubkey: String) { init(our_pubkey: String) {
self.our_pubkey = our_pubkey self.our_pubkey = our_pubkey
} }
var message_requests: [(String, DirectMessageModel)] { var message_requests: [DirectMessageModel] {
return dms.filter { dm in dm.1.is_request } return dms.filter { dm in dm.is_request }
} }
var friend_dms: [(String, DirectMessageModel)] { var friend_dms: [DirectMessageModel] {
return dms.filter { dm in !dm.1.is_request } return dms.filter { dm in !dm.is_request }
}
func set_active_dm_model(_ model: DirectMessageModel) {
self.active_model = model
}
func open_dm_by_pk(_ pubkey: String) {
self.set_active_dm(pubkey)
self.open_dm = true
}
func open_dm_by_model(_ model: DirectMessageModel) {
self.set_active_dm_model(model)
self.open_dm = true
}
func set_active_dm(_ pubkey: String) {
for model in self.dms where model.pubkey == pubkey {
self.set_active_dm_model(model)
break
}
} }
func lookup_or_create(_ pubkey: String) -> DirectMessageModel { func lookup_or_create(_ pubkey: String) -> DirectMessageModel {
@@ -29,15 +52,15 @@ class DirectMessagesModel: ObservableObject {
return dm return dm
} }
let new = DirectMessageModel(our_pubkey: our_pubkey) let new = DirectMessageModel(our_pubkey: our_pubkey, pubkey: pubkey)
dms.append((pubkey, new)) dms.append(new)
return new return new
} }
func lookup(_ pubkey: String) -> DirectMessageModel? { func lookup(_ pubkey: String) -> DirectMessageModel? {
for dm in dms { for dm in dms {
if pubkey == dm.0 { if pubkey == dm.pubkey {
return dm.1 return dm
} }
} }
+19 -3
View File
@@ -7,7 +7,23 @@
import Foundation import Foundation
class Drafts: ObservableObject { class DraftArtifacts {
@Published var post: NSMutableAttributedString = NSMutableAttributedString(string: "") var content: NSMutableAttributedString
@Published var replies: [NostrEvent: NSMutableAttributedString] = [:] var media: [UploadedMedia]
init() {
self.content = NSMutableAttributedString(string: "")
self.media = []
}
init(content: NSMutableAttributedString, media: [UploadedMedia]) {
self.content = content
self.media = media
}
}
class Drafts: ObservableObject {
@Published var post: DraftArtifacts? = nil
@Published var replies: [NostrEvent: DraftArtifacts] = [:]
@Published var quotes: [NostrEvent: DraftArtifacts] = [:]
} }
+1 -1
View File
@@ -82,7 +82,7 @@ class FollowersModel: ObservableObject {
if ev.known_kind == .contacts { if ev.known_kind == .contacts {
handle_contact_event(ev) handle_contact_event(ev)
} else if ev.known_kind == .metadata { } else if ev.known_kind == .metadata {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
} }
case .notice(let msg): case .notice(let msg):
+2 -2
View File
@@ -22,7 +22,7 @@ class FollowingModel {
} }
func get_filter() -> NostrFilter { func get_filter() -> NostrFilter {
var f = NostrFilter.filter_kinds([0]) var f = NostrFilter.filter_kinds([NostrKind.metadata.rawValue])
f.authors = self.contacts.reduce(into: Array<String>()) { acc, pk in f.authors = self.contacts.reduce(into: Array<String>()) { acc, pk in
// don't fetch profiles we already have // don't fetch profiles we already have
if damus_state.profiles.lookup(id: pk) != nil { if damus_state.profiles.lookup(id: pk) != nil {
@@ -62,7 +62,7 @@ class FollowingModel {
break break
case .event(_, let ev): case .event(_, let ev):
if ev.kind == 0 { if ev.kind == 0 {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
} }
case .notice(let msg): case .notice(let msg):
print("followingmodel notice: \(msg)") print("followingmodel notice: \(msg)")
+155 -103
View File
@@ -41,36 +41,27 @@ class HomeModel: ObservableObject {
let dms_subid = UUID().description let dms_subid = UUID().description
let init_subid = UUID().description let init_subid = UUID().description
let profiles_subid = UUID().description let profiles_subid = UUID().description
var loading: Bool = false
var signal = SignalModel()
@Published var new_events: NewEventsBits = NewEventsBits() @Published var new_events: NewEventsBits = NewEventsBits()
@Published var notifications = NotificationsModel() @Published var notifications = NotificationsModel()
@Published var dms: DirectMessagesModel
@Published var events = EventHolder() @Published var events = EventHolder()
@Published var loading: Bool = false
@Published var signal: SignalModel = SignalModel()
init() { init() {
self.damus_state = DamusState.empty self.damus_state = DamusState.empty
self.dms = DirectMessagesModel(our_pubkey: "") filter_events()
filter_muted() self.setup_debouncer()
} }
init(damus_state: DamusState) {
self.damus_state = damus_state
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
self.setup_debouncer()
filter_muted()
}
var pool: RelayPool { var pool: RelayPool {
return damus_state.pool return damus_state.pool
} }
func setup_debouncer() { var dms: DirectMessagesModel {
// turn off debouncer after initial load return damus_state.dms
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.should_debounce_dms = false
}
} }
func has_sub_id_event(sub_id: String, ev_id: String) -> Bool { func has_sub_id_event(sub_id: String, ev_id: String) -> Bool {
@@ -81,6 +72,13 @@ class HomeModel: ObservableObject {
return has_event[sub_id]!.contains(ev_id) return has_event[sub_id]!.contains(ev_id)
} }
func setup_debouncer() {
// turn off debouncer after initial load
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.should_debounce_dms = false
}
}
func process_event(sub_id: String, relay_id: String, ev: NostrEvent) { func process_event(sub_id: String, relay_id: String, ev: NostrEvent) {
if has_sub_id_event(sub_id: sub_id, ev_id: ev.id) { if has_sub_id_event(sub_id: sub_id, ev_id: ev.id) {
@@ -147,7 +145,7 @@ class HomeModel: ObservableObject {
} }
if damus_state.settings.zap_notification { if damus_state.settings.zap_notification {
// Create in-app local notification for zap received. // Create in-app local notification for zap received.
create_in_app_zap_notification(profiles: profiles, zap: zap) create_in_app_zap_notification(profiles: profiles, zap: zap, evId: ev.referenced_ids.first?.id ?? "")
} }
} }
@@ -188,27 +186,31 @@ class HomeModel: ObservableObject {
} }
func handle_channel_create(_ ev: NostrEvent) { func handle_channel_create(_ ev: NostrEvent) {
guard ev.is_valid else {
return
}
self.channels[ev.id] = ev self.channels[ev.id] = ev
} }
func handle_channel_meta(_ ev: NostrEvent) { func handle_channel_meta(_ ev: NostrEvent) {
} }
func filter_muted() { func filter_events() {
events.filter { !damus_state.contacts.is_muted($0.pubkey) && !damus_state.muted_threads.isMutedThread($0) } events.filter { ev in
self.dms.dms = dms.dms.filter { !damus_state.contacts.is_muted($0.0) } !damus_state.contacts.is_muted(ev.pubkey)
notifications.filter_and_build_notifications(damus_state) }
self.dms.dms = dms.dms.filter { ev in
!damus_state.contacts.is_muted(ev.pubkey)
}
notifications.filter { ev in
if damus_state.settings.onlyzaps_mode && ev.known_kind == NostrKind.like {
return false
}
return !damus_state.contacts.is_muted(ev.pubkey) && !damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey)
}
} }
func handle_delete_event(_ ev: NostrEvent) { func handle_delete_event(_ ev: NostrEvent) {
guard ev.is_valid else {
return
}
self.deleted_events.insert(ev.id) self.deleted_events.insert(ev.id)
} }
@@ -230,7 +232,7 @@ class HomeModel: ObservableObject {
if let inner_ev = ev.inner_event { if let inner_ev = ev.inner_event {
boost_ev_id = inner_ev.id boost_ev_id = inner_ev.id
guard inner_ev.is_valid else { guard validate_event(ev: inner_ev) == .ok else {
return return
} }
@@ -259,6 +261,10 @@ class HomeModel: ObservableObject {
return return
} }
if damus_state.settings.onlyzaps_mode {
return
}
switch damus_state.likes.add_event(ev, target: e.ref_id) { switch damus_state.likes.add_event(ev, target: e.ref_id) {
case .already_counted: case .already_counted:
break break
@@ -299,10 +305,7 @@ class HomeModel: ObservableObject {
break break
} }
update_signal_from_pool(signal: signal, pool: damus_state.pool) update_signal_from_pool(signal: self.signal, pool: damus_state.pool)
print("ws_event \(ev)")
case .nostr_event(let ev): case .nostr_event(let ev):
switch ev { switch ev {
case .event(let sub_id, let ev): case .event(let sub_id, let ev):
@@ -321,11 +324,13 @@ class HomeModel: ObservableObject {
case .eose(let sub_id): case .eose(let sub_id):
if sub_id == dms_subid { if sub_id == dms_subid {
var dms = dms.dms.flatMap { $0.1.events } var dms = dms.dms.flatMap { $0.events }
dms.append(contentsOf: incoming_dms) dms.append(contentsOf: incoming_dms)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(dms), damus_state: damus_state) load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(dms), damus_state: damus_state)
} else if sub_id == notifications_subid { } else if sub_id == notifications_subid {
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_keys(notifications.uniq_pubkeys()), damus_state: damus_state) load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_keys(notifications.uniq_pubkeys()), damus_state: damus_state)
} else if sub_id == home_subid {
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus_state)
} }
self.loading = false self.loading = false
@@ -355,13 +360,13 @@ class HomeModel: ObservableObject {
var friends = damus_state.contacts.get_friend_list() var friends = damus_state.contacts.get_friend_list()
friends.append(damus_state.pubkey) friends.append(damus_state.pubkey)
var contacts_filter = NostrFilter.filter_kinds([0]) var contacts_filter = NostrFilter.filter_kinds([NostrKind.metadata.rawValue])
contacts_filter.authors = friends contacts_filter.authors = friends
var our_contacts_filter = NostrFilter.filter_kinds([3, 0]) var our_contacts_filter = NostrFilter.filter_kinds([NostrKind.contacts.rawValue, NostrKind.metadata.rawValue])
our_contacts_filter.authors = [damus_state.pubkey] our_contacts_filter.authors = [damus_state.pubkey]
var our_blocklist_filter = NostrFilter.filter_kinds([30000]) var our_blocklist_filter = NostrFilter.filter_kinds([NostrKind.list.rawValue])
our_blocklist_filter.parameter = ["mute"] our_blocklist_filter.parameter = ["mute"]
our_blocklist_filter.authors = [damus_state.pubkey] our_blocklist_filter.authors = [damus_state.pubkey]
@@ -380,21 +385,27 @@ class HomeModel: ObservableObject {
our_dms_filter.authors = [ damus_state.pubkey ] our_dms_filter.authors = [ damus_state.pubkey ]
// TODO: separate likes? // TODO: separate likes?
var home_filter = NostrFilter.filter_kinds([ var home_filter_kinds = [
NostrKind.text.rawValue, NostrKind.text.rawValue,
NostrKind.like.rawValue, NostrKind.boost.rawValue
NostrKind.boost.rawValue, ]
]) if !damus_state.settings.onlyzaps_mode {
home_filter_kinds.append(NostrKind.like.rawValue)
}
var home_filter = NostrFilter.filter_kinds(home_filter_kinds)
// include our pubkey as well even if we're not technically a friend // include our pubkey as well even if we're not technically a friend
home_filter.authors = friends home_filter.authors = friends
home_filter.limit = 500 home_filter.limit = 500
var notifications_filter = NostrFilter.filter_kinds([ var notifications_filter_kinds = [
NostrKind.text.rawValue, NostrKind.text.rawValue,
NostrKind.like.rawValue,
NostrKind.boost.rawValue, NostrKind.boost.rawValue,
NostrKind.zap.rawValue, NostrKind.zap.rawValue,
]) ]
if !damus_state.settings.onlyzaps_mode {
notifications_filter_kinds.append(NostrKind.like.rawValue)
}
var notifications_filter = NostrFilter.filter_kinds(notifications_filter_kinds)
notifications_filter.pubkeys = [damus_state.pubkey] notifications_filter.pubkeys = [damus_state.pubkey]
notifications_filter.limit = 500 notifications_filter.limit = 500
@@ -449,7 +460,7 @@ class HomeModel: ObservableObject {
} }
func handle_metadata_event(_ ev: NostrEvent) { func handle_metadata_event(_ ev: NostrEvent) {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
} }
func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? { func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? {
@@ -471,11 +482,12 @@ class HomeModel: ObservableObject {
return return
} }
guard should_show_event(contacts: damus_state.contacts, ev: ev) else { guard should_show_event(contacts: damus_state.contacts, ev: ev) && !damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey) else {
return return
} }
damus_state.events.insert(ev) damus_state.events.insert(ev)
if let inner_ev = ev.inner_event { if let inner_ev = ev.inner_event {
damus_state.events.insert(inner_ev) damus_state.events.insert(inner_ev)
} }
@@ -522,15 +534,26 @@ class HomeModel: ObservableObject {
} }
} }
func got_new_dm(notifs: NewEventsBits, ev: NostrEvent) {
self.new_events = notifs
if damus_state.settings.dm_notification {
let convo = ev.decrypted(privkey: self.damus_state.keypair.privkey) ?? NSLocalizedString("New encrypted direct message", comment: "Notification that the user has received a new direct message")
let notify = LocalNotification(type: .dm, event: ev, target: ev, content: convo)
create_local_notification(profiles: damus_state.profiles, notify: notify)
}
}
func handle_dm(_ ev: NostrEvent) { func handle_dm(_ ev: NostrEvent) {
guard should_show_event(contacts: damus_state.contacts, ev: ev) else { guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
return return
} }
damus_state.events.insert(ev)
if !should_debounce_dms { if !should_debounce_dms {
self.incoming_dms.append(ev) self.incoming_dms.append(ev)
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) { if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
self.new_events = notifs got_new_dm(notifs: notifs, ev: ev)
} }
self.incoming_dms = [] self.incoming_dms = []
return return
@@ -540,11 +563,7 @@ class HomeModel: ObservableObject {
dm_debouncer.debounce { [self] in dm_debouncer.debounce { [self] in
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) { if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
self.new_events = notifs got_new_dm(notifs: notifs, ev: ev)
if damus_state.settings.dm_notification,
let displayName = damus_state.profiles.lookup(id: self.incoming_dms.last!.pubkey)?.display_name {
create_local_notification(displayName: displayName, conversation: "You have received a direct message", type: .dm)
}
} }
self.incoming_dms = [] self.incoming_dms = []
} }
@@ -557,8 +576,8 @@ func update_signal_from_pool(signal: SignalModel, pool: RelayPool) {
signal.max_signal = pool.relays.count signal.max_signal = pool.relays.count
} }
if signal.signal != pool.num_connecting { if signal.signal != pool.num_connected {
signal.signal = signal.max_signal - pool.num_connecting signal.signal = pool.num_connected
} }
} }
@@ -652,15 +671,9 @@ func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) {
print("-----") print("-----")
} }
func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEvent) { func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: Profile, ev: NostrEvent) {
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
return
}
if our_pubkey == ev.pubkey && (profile.deleted ?? false) { if our_pubkey == ev.pubkey && (profile.deleted ?? false) {
DispatchQueue.main.async { notify(.deleted_account, ())
notify(.deleted_account, ())
}
return return
} }
@@ -694,21 +707,57 @@ func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEve
// load pfps asap // load pfps asap
let picture = tprof.profile.picture ?? robohash(ev.pubkey) let picture = tprof.profile.picture ?? robohash(ev.pubkey)
if URL(string: picture) != nil { if URL(string: picture) != nil {
DispatchQueue.main.async { notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
}
} }
let banner = tprof.profile.banner ?? "" let banner = tprof.profile.banner ?? ""
if URL(string: banner) != nil { if URL(string: banner) != nil {
DispatchQueue.main.async { notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
}
} }
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile)) notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
} }
func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping () -> Void) {
let validated = events.is_event_valid(ev.id)
switch validated {
case .unknown:
Task {
let result = validate_event(ev: ev)
DispatchQueue.main.async {
events.validation[ev.id] = result
guard result == .ok else {
return
}
callback()
}
}
case .ok:
callback()
case .bad_id: fallthrough
case .bad_sig:
break
}
}
func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Profiles, ev: NostrEvent) {
guard_valid_event(events: events, ev: ev) {
DispatchQueue.global(qos: .background).async {
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
return
}
DispatchQueue.main.async {
process_metadata_profile(our_pubkey: our_pubkey, profiles: profiles, profile: profile, ev: ev)
}
}
}
}
func robohash(_ pk: String) -> String { func robohash(_ pk: String) -> String {
return "https://robohash.org/" + pk return "https://robohash.org/" + pk
} }
@@ -847,10 +896,10 @@ func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesM
} }
} }
for (pk, _) in dms.dms { for model in dms.dms {
if pk == the_pk { if model.pubkey == the_pk {
found = true found = true
inserted = insert_uniq_sorted_event(events: &(dms.dms[i].1.events), new_ev: ev) { inserted = insert_uniq_sorted_event(events: &(dms.dms[i].events), new_ev: ev) {
$0.created_at < $1.created_at $0.created_at < $1.created_at
} }
@@ -860,8 +909,8 @@ func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesM
} }
if !found { if !found {
let model = DirectMessageModel(events: [ev], our_pubkey: our_pubkey) let model = DirectMessageModel(events: [ev], our_pubkey: our_pubkey, pubkey: the_pk)
dms.dms.append((the_pk, model)) dms.dms.append(model)
inserted = true inserted = true
} }
@@ -888,8 +937,8 @@ func handle_incoming_dms(prev_events: NewEventsBits, dms: DirectMessagesModel, o
} }
if inserted { if inserted {
dms.dms = dms.dms.filter({ $0.1.events.count > 0 }).sorted { a, b in dms.dms = dms.dms.filter({ $0.events.count > 0 }).sorted { a, b in
return a.1.events.last!.created_at > b.1.events.last!.created_at return a.events.last!.created_at > b.events.last!.created_at
} }
} }
@@ -1009,12 +1058,13 @@ func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale
} }
} }
func create_in_app_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) { func create_in_app_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: String) {
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
content.title = zap_notification_title(zap) content.title = zap_notification_title(zap)
content.body = zap_notification_body(profiles: profiles, zap: zap, locale: locale) content.body = zap_notification_body(profiles: profiles, zap: zap, locale: locale)
content.sound = UNNotificationSound.default content.sound = UNNotificationSound.default
content.userInfo = LossyLocalNotification(type: .zap, event_id: evId).to_user_info()
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
@@ -1042,57 +1092,59 @@ func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
} }
// Don't show notifications from muted threads. // Don't show notifications from muted threads.
if damus_state.muted_threads.isMutedThread(ev) { if damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey) {
return return
} }
if type == .text && damus_state.settings.mention_notification { if type == .text && damus_state.settings.mention_notification {
for block in ev.blocks(damus_state.keypair.privkey) { let blocks = ev.blocks(damus_state.keypair.privkey)
if case .mention(let mention) = block, mention.ref.ref_id == damus_state.keypair.pubkey, for case .mention(let mention) in blocks where mention.ref.ref_id == damus_state.keypair.pubkey {
let displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name { let content = NSAttributedString(render_note_content(ev: ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content.attributed).string
let justContent = NSAttributedString(render_note_content(ev: ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content.attributed).string
create_local_notification(displayName: displayName, conversation: justContent, type: type) 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 displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name {
if let inner_ev = ev.inner_event {
create_local_notification(displayName: displayName, conversation: inner_ev.content, type: type)
} }
} else if type == .boost && damus_state.settings.repost_notification, let inner_ev = ev.inner_event {
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, } else if type == .like && damus_state.settings.like_notification,
let displayName = damus_state.profiles.lookup(id: ev.pubkey)?.display_name, let evid = ev.referenced_ids.first?.ref_id,
let e_ref = ev.referenced_ids.first?.ref_id, let liked_event = damus_state.events.lookup(evid)
let content = damus_state.events.lookup(e_ref)?.content { {
let notify = LocalNotification(type: .like, event: ev, target: liked_event, content: liked_event.content)
create_local_notification(displayName: displayName, conversation: content, type: type) create_local_notification(profiles: damus_state.profiles, notify: notify)
} }
} }
func create_local_notification(displayName: String, conversation: String, type: NostrKind) { func create_local_notification(profiles: Profiles, notify: LocalNotification) {
let content = UNMutableNotificationContent() let content = UNMutableNotificationContent()
var title = "" var title = ""
var identifier = "" var identifier = ""
switch type {
case .text: let displayName = event_author_name(profiles: profiles, pubkey: notify.event.pubkey)
switch notify.type {
case .mention:
title = String(format: NSLocalizedString("Mentioned by %@", comment: "Mentioned by heading in local notification"), displayName) title = String(format: NSLocalizedString("Mentioned by %@", comment: "Mentioned by heading in local notification"), displayName)
identifier = "myMentionNotification" identifier = "myMentionNotification"
case .boost: case .repost:
title = String(format: NSLocalizedString("Reposted by %@", comment: "Reposted by heading in local notification"), displayName) title = String(format: NSLocalizedString("Reposted by %@", comment: "Reposted by heading in local notification"), displayName)
identifier = "myBoostNotification" identifier = "myBoostNotification"
case .like: case .like:
title = String(format: NSLocalizedString("Liked by %@", comment: "Liked by heading in local notification"), displayName) title = String(format: NSLocalizedString("Liked by %@", comment: "Liked by heading in local notification"), displayName)
identifier = "myLikeNotification" identifier = "myLikeNotification"
case .dm: case .dm:
title = String(format: NSLocalizedString("DM by %@", comment: "DM by heading in local notification"), displayName) title = String(format: NSLocalizedString("%@", comment: "DM by heading in local notification"), displayName)
identifier = "myDMNotification" identifier = "myDMNotification"
default: case .zap:
// not handled here
break break
} }
content.title = title content.title = title
content.body = conversation content.body = notify.content
content.sound = UNNotificationSound.default content.sound = UNNotificationSound.default
content.userInfo = notify.to_lossy().to_user_info()
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
+9
View File
@@ -25,6 +25,15 @@ enum MediaUpload {
return url.pathExtension return url.pathExtension
} }
} }
var localURL: URL {
switch self {
case .image(let url):
return url
case .video(let url):
return url
}
}
var is_image: Bool { var is_image: Bool {
if case .image = self { if case .image = self {
+12 -4
View File
@@ -7,7 +7,7 @@
import Foundation import Foundation
enum LibreTranslateServer: String, CaseIterable, Identifiable { enum LibreTranslateServer: String, CaseIterable, Identifiable, StringCodable {
var id: String { self.rawValue } var id: String { self.rawValue }
struct Model: Identifiable, Hashable { struct Model: Identifiable, Hashable {
@@ -17,9 +17,19 @@ enum LibreTranslateServer: String, CaseIterable, Identifiable {
var url: String? var url: String?
} }
func to_string() -> String {
return rawValue
}
init?(from string: String) {
guard let libreTranslateServer = LibreTranslateServer(rawValue: string) else {
return nil
}
self = libreTranslateServer
}
case argosopentech case argosopentech
case terraprint case terraprint
case vern
case custom case custom
var model: Model { var model: Model {
@@ -28,8 +38,6 @@ enum LibreTranslateServer: String, CaseIterable, Identifiable {
return .init(tag: self.rawValue, displayName: "translate.argosopentech.com", url: "https://translate.argosopentech.com") return .init(tag: self.rawValue, displayName: "translate.argosopentech.com", url: "https://translate.argosopentech.com")
case .terraprint: case .terraprint:
return .init(tag: self.rawValue, displayName: "translate.terraprint.co", url: "https://translate.terraprint.co") return .init(tag: self.rawValue, displayName: "translate.terraprint.co", url: "https://translate.terraprint.co")
case .vern:
return .init(tag: self.rawValue, displayName: "lt.vern.cc", url: "https://lt.vern.cc")
case .custom: case .custom:
return .init(tag: self.rawValue, displayName: NSLocalizedString("Custom", comment: "Dropdown option for selecting a custom translation server."), url: nil) return .init(tag: self.rawValue, displayName: NSLocalizedString("Custom", comment: "Dropdown option for selecting a custom translation server."), url: nil)
} }
+8 -8
View File
@@ -30,7 +30,7 @@ func saveMutedThreads(pubkey: String, currentValue: [String], value: [String]) -
class MutedThreadsManager: ObservableObject { class MutedThreadsManager: ObservableObject {
private let userDefaults = UserDefaults.standard private let userDefaults = UserDefaults.standard
private let pubkey: String private let keypair: Keypair
private var _mutedThreadsSet: Set<String> private var _mutedThreadsSet: Set<String>
private var _mutedThreads: [String] private var _mutedThreads: [String]
@@ -39,26 +39,26 @@ class MutedThreadsManager: ObservableObject {
return _mutedThreads return _mutedThreads
} }
set { set {
if saveMutedThreads(pubkey: pubkey, currentValue: _mutedThreads, value: newValue) { if saveMutedThreads(pubkey: keypair.pubkey, currentValue: _mutedThreads, value: newValue) {
self._mutedThreads = newValue self._mutedThreads = newValue
self.objectWillChange.send() self.objectWillChange.send()
} }
} }
} }
init(pubkey: String) { init(keypair: Keypair) {
self._mutedThreads = loadMutedThreads(pubkey: pubkey) self._mutedThreads = loadMutedThreads(pubkey: keypair.pubkey)
self._mutedThreadsSet = Set(_mutedThreads) self._mutedThreadsSet = Set(_mutedThreads)
self.pubkey = pubkey self.keypair = keypair
} }
func isMutedThread(_ ev: NostrEvent) -> Bool { func isMutedThread(_ ev: NostrEvent, privkey: String?) -> Bool {
return _mutedThreadsSet.contains(ev.thread_id(privkey: nil)) return _mutedThreadsSet.contains(ev.thread_id(privkey: privkey))
} }
func updateMutedThread(_ ev: NostrEvent) { func updateMutedThread(_ ev: NostrEvent) {
let threadId = ev.thread_id(privkey: nil) let threadId = ev.thread_id(privkey: nil)
if isMutedThread(ev) { if isMutedThread(ev, privkey: keypair.privkey) {
mutedThreads = mutedThreads.filter { $0 != threadId } mutedThreads = mutedThreads.filter { $0 != threadId }
_mutedThreadsSet.remove(threadId) _mutedThreadsSet.remove(threadId)
notify(.unmute_thread, ev) notify(.unmute_thread, ev)
@@ -29,4 +29,22 @@ class EventGroup {
func insert(_ ev: NostrEvent) -> Bool { func insert(_ ev: NostrEvent) -> Bool {
return insert_uniq_sorted_event_created(events: &events, new_ev: ev) return insert_uniq_sorted_event_created(events: &events, new_ev: ev)
} }
func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool {
for ev in events {
if !isIncluded(ev) {
return true
}
}
return false
}
func filter(_ isIncluded: (NostrEvent) -> Bool) -> EventGroup? {
let new_evs = events.filter(isIncluded)
guard new_evs.count > 0 else {
return nil
}
return EventGroup(events: new_evs)
}
} }
+21 -4
View File
@@ -30,10 +30,26 @@ class ZapGroup {
} }
} }
init(zaps: [Zap]) { func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool {
self.zaps = zaps for zap in zaps {
self.msat_total = 0 if !isIncluded(zap.request_ev) {
self.zappers = Set() return true
}
}
return false
}
func filter(_ isIncluded: (NostrEvent) -> Bool) -> ZapGroup? {
let new_zaps = zaps.filter { isIncluded($0.request_ev) }
guard new_zaps.count > 0 else {
return nil
}
let grp = ZapGroup()
for zap in new_zaps {
grp.insert(zap)
}
return grp
} }
init() { init() {
@@ -42,6 +58,7 @@ class ZapGroup {
self.zappers = Set() self.zappers = Set()
} }
@discardableResult
func insert(_ zap: Zap) -> Bool { func insert(_ zap: Zap) -> Bool {
if !insert_uniq_sorted_zap_by_created(zaps: &zaps, new_zap: zap) { if !insert_uniq_sorted_zap_by_created(zaps: &zaps, new_zap: zap) {
return false return false
+41 -14
View File
@@ -65,6 +65,37 @@ enum NotificationItem {
return reply.created_at return reply.created_at
} }
} }
func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool {
switch self {
case .repost(_, let evgrp):
return evgrp.would_filter(isIncluded)
case .reaction(_, let evgrp):
return evgrp.would_filter(isIncluded)
case .profile_zap(let zapgrp):
return zapgrp.would_filter(isIncluded)
case .event_zap(_, let zapgrp):
return zapgrp.would_filter(isIncluded)
case .reply(let ev):
return !isIncluded(ev)
}
}
func filter(_ isIncluded: (NostrEvent) -> Bool) -> NotificationItem? {
switch self {
case .repost(let evid, let evgrp):
return evgrp.filter(isIncluded).map { .repost(evid, $0) }
case .reaction(let evid, let evgrp):
return evgrp.filter(isIncluded).map { .reaction(evid, $0) }
case .profile_zap(let zapgrp):
return zapgrp.filter(isIncluded).map { .profile_zap($0) }
case .event_zap(let evid, let zapgrp):
return zapgrp.filter(isIncluded).map { .event_zap(evid, $0) }
case .reply(let ev):
if isIncluded(ev) { return .reply(ev) }
return nil
}
}
} }
class NotificationsModel: ObservableObject, ScrollQueue { class NotificationsModel: ObservableObject, ScrollQueue {
@@ -239,7 +270,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
} }
if insert_event_immediate(ev) { if insert_event_immediate(ev) {
filter_and_build_notifications(damus_state) self.notifications = build_notifications()
return true return true
} }
@@ -252,47 +283,47 @@ class NotificationsModel: ObservableObject, ScrollQueue {
} }
if insert_zap_immediate(zap) { if insert_zap_immediate(zap) {
filter_and_build_notifications(damus_state) self.notifications = build_notifications()
return true return true
} }
return false return false
} }
func filter_and_build_notifications(_ damus_state: DamusState) { func filter(_ isIncluded: (NostrEvent) -> Bool) {
var changed = false var changed = false
var count = 0 var count = 0
count = incoming_events.count count = incoming_events.count
incoming_events = incoming_events.filter { include_event($0, damus_state: damus_state) } incoming_events = incoming_events.filter(isIncluded)
changed = changed || incoming_events.count != count changed = changed || incoming_events.count != count
count = profile_zaps.zaps.count count = profile_zaps.zaps.count
profile_zaps.zaps = profile_zaps.zaps.filter { zap in include_event(zap.request.ev, damus_state: damus_state) } profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request.ev) }
changed = changed || profile_zaps.zaps.count != count changed = changed || profile_zaps.zaps.count != count
for el in reactions { for el in reactions {
count = el.value.events.count count = el.value.events.count
el.value.events = el.value.events.filter { include_event($0, damus_state: damus_state) } el.value.events = el.value.events.filter(isIncluded)
changed = changed || el.value.events.count != count changed = changed || el.value.events.count != count
} }
for el in reposts { for el in reposts {
count = el.value.events.count count = el.value.events.count
el.value.events = el.value.events.filter { include_event($0, damus_state: damus_state) } el.value.events = el.value.events.filter(isIncluded)
changed = changed || el.value.events.count != count changed = changed || el.value.events.count != count
} }
for el in zaps { for el in zaps {
count = el.value.zaps.count count = el.value.zaps.count
el.value.zaps = el.value.zaps.filter { el.value.zaps = el.value.zaps.filter {
include_event($0.request.ev, damus_state: damus_state) isIncluded($0.request.ev)
} }
changed = changed || el.value.zaps.count != count changed = changed || el.value.zaps.count != count
} }
count = replies.count count = replies.count
replies = replies.filter { include_event($0, damus_state: damus_state) } replies = replies.filter(isIncluded)
changed = changed || replies.count != count changed = changed || replies.count != count
if changed { if changed {
@@ -312,13 +343,9 @@ class NotificationsModel: ObservableObject, ScrollQueue {
} }
if inserted { if inserted {
filter_and_build_notifications(damus_state) self.notifications = build_notifications()
} }
return inserted return inserted
} }
func include_event(_ event: NostrEvent, damus_state: DamusState) -> Bool {
return !damus_state.contacts.is_muted(event.pubkey) && !damus_state.muted_threads.isMutedThread(event)
}
} }
+4 -1
View File
@@ -119,7 +119,7 @@ class ProfileModel: ObservableObject, Equatable {
} else if ev.known_kind == .contacts { } else if ev.known_kind == .contacts {
handle_profile_contact_event(ev) handle_profile_contact_event(ev)
} else if ev.known_kind == .metadata { } else if ev.known_kind == .metadata {
process_metadata_event(our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev) process_metadata_event(events: damus.events, our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev)
} }
seen_event.insert(ev.id) seen_event.insert(ev.id)
} }
@@ -140,6 +140,9 @@ class ProfileModel: ObservableObject, Equatable {
case .notice(let notice): case .notice(let notice):
notify(.notice, notice) notify(.notice, notice)
case .eose: case .eose:
if resp.subid == sub_id {
load_profiles(profiles_subid: prof_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus)
}
progress += 1 progress += 1
break break
} }
+8 -5
View File
@@ -24,7 +24,7 @@ class SearchHomeModel: ObservableObject {
} }
func get_base_filter() -> NostrFilter { func get_base_filter() -> NostrFilter {
var filter = NostrFilter.filter_kinds([1, 42]) var filter = NostrFilter.filter_kinds([NostrKind.text.rawValue, NostrKind.chat.rawValue])
filter.limit = self.limit filter.limit = self.limit
filter.until = Int64(Date.now.timeIntervalSince1970) filter.until = Int64(Date.now.timeIntervalSince1970)
return filter return filter
@@ -128,11 +128,14 @@ func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent]
var pubkeys = Set<String>() var pubkeys = Set<String>()
for ev in events { for ev in events {
if profiles.lookup(id: ev.pubkey) != nil { // lookup profiles from boosted events
continue if ev.known_kind == .boost, let bev = ev.inner_event, profiles.lookup(id: bev.pubkey) == nil {
pubkeys.insert(bev.pubkey)
} }
pubkeys.insert(ev.pubkey) if profiles.lookup(id: ev.pubkey) == nil {
pubkeys.insert(ev.pubkey)
}
} }
return Array(pubkeys) return Array(pubkeys)
@@ -161,7 +164,7 @@ func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad
} }
if ev.known_kind == .metadata { if ev.known_kind == .metadata {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
} }
} }
+1 -1
View File
@@ -33,7 +33,7 @@ class SearchModel: ObservableObject {
func subscribe() { func subscribe() {
// since 1 month // since 1 month
search.limit = self.limit search.limit = self.limit
search.kinds = [1,5,7] search.kinds = [NostrKind.text.rawValue, NostrKind.like.rawValue]
//likes_filter.ids = ref_events.referenced_ids! //likes_filter.ids = ref_events.referenced_ids!
+9 -4
View File
@@ -77,18 +77,23 @@ class ThreadModel: ObservableObject {
var meta_events = NostrFilter() var meta_events = NostrFilter()
var event_filter = NostrFilter() var event_filter = NostrFilter()
var ref_events = NostrFilter() var ref_events = NostrFilter()
//var likes_filter = NostrFilter.filter_kinds(7])
let thread_id = event.thread_id(privkey: nil) let thread_id = event.thread_id(privkey: nil)
ref_events.referenced_ids = [thread_id, event.id] ref_events.referenced_ids = [thread_id, event.id]
ref_events.kinds = [1] ref_events.kinds = [NostrKind.text.rawValue]
ref_events.limit = 1000 ref_events.limit = 1000
event_filter.ids = [thread_id, event.id] event_filter.ids = [thread_id, event.id]
meta_events.referenced_ids = [event.id] meta_events.referenced_ids = [event.id]
meta_events.kinds = [9735, 1, 6, 7]
var kinds = [NostrKind.zap.rawValue, NostrKind.text.rawValue, NostrKind.boost.rawValue]
if !damus_state.settings.onlyzaps_mode {
kinds.append(NostrKind.like.rawValue)
}
meta_events.kinds = kinds
meta_events.limit = 1000 meta_events.limit = 1000
/* /*
@@ -129,7 +134,7 @@ class ThreadModel: ObservableObject {
} }
if ev.known_kind == .metadata { if ev.known_kind == .metadata {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
} else if ev.is_textlike { } else if ev.is_textlike {
self.add_event(ev, privkey: self.damus_state.keypair.privkey) self.add_event(ev, privkey: self.damus_state.keypair.privkey)
} }
+13 -1
View File
@@ -7,7 +7,19 @@
import Foundation import Foundation
enum TranslationService: String, CaseIterable, Identifiable { enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
init?(from string: String) {
guard let ts = TranslationService(rawValue: string) else {
return nil
}
self = ts
}
func to_string() -> String {
return self.rawValue
}
var id: String { self.rawValue } var id: String { self.rawValue }
struct Model: Identifiable, Hashable { struct Model: Identifiable, Hashable {
+199 -272
View File
@@ -9,210 +9,141 @@ import Foundation
import Vault import Vault
import UIKit import UIKit
func should_show_wallet_selector(_ pubkey: String) -> Bool { @propertyWrapper struct Setting<T: Equatable> {
return UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true private let key: String
} private var value: T
func pk_setting_key(_ pubkey: String, key: String) -> String { init(key: String, default_value: T) {
return "\(pubkey)_\(key)" self.key = pk_setting_key(UserSettingsStore.pubkey ?? "", key: key)
} if let loaded = UserDefaults.standard.object(forKey: self.key) as? T {
self.value = loaded
func default_zap_setting_key(pubkey: String) -> String { } else if let loaded = UserDefaults.standard.object(forKey: key) as? T {
return pk_setting_key(pubkey, key: "default_zap_amount") // try to load from deprecated non-pubkey-keyed setting
} self.value = loaded
} else {
func set_default_zap_amount(pubkey: String, amount: Int) { self.value = default_value
let key = default_zap_setting_key(pubkey: pubkey) }
UserDefaults.standard.setValue(amount, forKey: key)
}
func get_default_zap_amount(pubkey: String) -> Int? {
let key = default_zap_setting_key(pubkey: pubkey)
let amt = UserDefaults.standard.integer(forKey: key)
if amt == 0 {
return nil
}
return amt
}
func should_disable_image_animation() -> Bool {
return (UserDefaults.standard.object(forKey: "disable_animation") as? Bool)
?? UIAccessibility.isReduceMotionEnabled
}
func get_default_wallet(_ pubkey: String) -> Wallet {
if let defaultWalletName = UserDefaults.standard.string(forKey: "default_wallet"),
let default_wallet = Wallet(rawValue: defaultWalletName)
{
return default_wallet
} else {
return .system_default_wallet
}
}
func get_media_uploader(_ pubkey: String) -> MediaUploader {
if let defaultMediaUploader = UserDefaults.standard.string(forKey: "default_media_uploader"),
let defaultMediaUploader = MediaUploader(rawValue: defaultMediaUploader) {
return defaultMediaUploader
} else {
return .nostrBuild
}
}
private func get_translation_service(_ pubkey: String) -> TranslationService? {
guard let translation_service = UserDefaults.standard.string(forKey: "translation_service") else {
return nil
}
return TranslationService(rawValue: translation_service)
}
private func get_deepl_plan(_ pubkey: String) -> DeepLPlan? {
guard let server_name = UserDefaults.standard.string(forKey: "deepl_plan") else {
return nil
}
return DeepLPlan(rawValue: server_name)
}
private func get_libretranslate_server(_ pubkey: String) -> LibreTranslateServer? {
guard let server_name = UserDefaults.standard.string(forKey: "libretranslate_server") else {
return nil
} }
return LibreTranslateServer(rawValue: server_name) var wrappedValue: T {
get { return value }
set {
guard self.value != newValue else {
return
}
self.value = newValue
UserDefaults.standard.set(newValue, forKey: key)
UserSettingsStore.shared!.objectWillChange.send()
}
}
} }
private func get_libretranslate_url(_ pubkey: String, server: LibreTranslateServer) -> String? { @propertyWrapper class StringSetting<T: StringCodable & Equatable> {
if let url = server.model.url { private let key: String
return url private var value: T
init(key: String, default_value: T) {
self.key = pk_setting_key(UserSettingsStore.pubkey ?? "", key: key)
if let loaded = UserDefaults.standard.string(forKey: self.key), let val = T.init(from: loaded) {
self.value = val
} else if let loaded = UserDefaults.standard.string(forKey: key), let val = T.init(from: loaded) {
// try to load from deprecated non-pubkey-keyed setting
self.value = val
} else {
self.value = default_value
}
} }
return UserDefaults.standard.object(forKey: "libretranslate_url") as? String var wrappedValue: T {
get { return value }
set {
guard self.value != newValue else {
return
}
self.value = newValue
UserDefaults.standard.set(newValue.to_string(), forKey: key)
UserSettingsStore.shared!.objectWillChange.send()
}
}
} }
class UserSettingsStore: ObservableObject { class UserSettingsStore: ObservableObject {
@Published var default_wallet: Wallet { static var pubkey: String? = nil
didSet { static var shared: UserSettingsStore? = nil
UserDefaults.standard.set(default_wallet.rawValue, forKey: "default_wallet")
}
}
@Published var default_media_uploader: MediaUploader {
didSet {
UserDefaults.standard.set(default_media_uploader.rawValue, forKey: "default_media_uploader")
}
}
@Published var show_wallet_selector: Bool { @StringSetting(key: "default_wallet", default_value: .system_default_wallet)
didSet { var default_wallet: Wallet
UserDefaults.standard.set(show_wallet_selector, forKey: "show_wallet_selector")
}
}
@Published var left_handed: Bool {
didSet {
UserDefaults.standard.set(left_handed, forKey: "left_handed")
}
}
@Published var always_show_images: Bool { @StringSetting(key: "default_media_uploader", default_value: .nostrBuild)
didSet { var default_media_uploader: MediaUploader
UserDefaults.standard.set(always_show_images, forKey: "always_show_images")
}
}
@Published var zap_vibration: Bool {
didSet {
UserDefaults.standard.set(zap_vibration, forKey: "zap_vibration")
}
}
@Published var zap_notification: Bool {
didSet {
UserDefaults.standard.set(zap_notification, forKey: "zap_notification")
}
}
@Published var mention_notification: Bool {
didSet {
UserDefaults.standard.set(mention_notification, forKey: "mention_notification")
}
}
@Published var repost_notification: Bool {
didSet {
UserDefaults.standard.set(repost_notification, forKey: "repost_notification")
}
}
@Published var dm_notification: Bool {
didSet {
UserDefaults.standard.set(dm_notification, forKey: "dm_notification")
}
}
@Published var like_notification: Bool {
didSet {
UserDefaults.standard.set(like_notification, forKey: "like_notification")
}
}
@Published var notification_only_from_following: Bool {
didSet {
UserDefaults.standard.set(notification_only_from_following, forKey: "notification_only_from_following")
}
}
@Published var translate_dms: Bool { @Setting(key: "show_wallet_selector", default_value: true)
didSet { var show_wallet_selector: Bool
UserDefaults.standard.set(translate_dms, forKey: "translate_dms")
}
}
@Published var truncate_timeline_text: Bool {
didSet {
UserDefaults.standard.set(truncate_timeline_text, forKey: "truncate_timeline_text")
}
}
@Published var notification_indicators: Int { @Setting(key: "left_handed", default_value: false)
didSet { var left_handed: Bool
UserDefaults.standard.set(notification_indicators, forKey: "notification_indicators")
}
}
@Published var truncate_mention_text: Bool { @Setting(key: "always_show_images", default_value: false)
didSet { var always_show_images: Bool
UserDefaults.standard.set(truncate_mention_text, forKey: "truncate_mention_text")
}
}
@Published var auto_translate: Bool { @Setting(key: "zap_vibration", default_value: true)
didSet { var zap_vibration: Bool
UserDefaults.standard.set(auto_translate, forKey: "auto_translate")
} @Setting(key: "zap_notification", default_value: true)
} var zap_notification: Bool
@Setting(key: "mention_notification", default_value: true)
var mention_notification: Bool
@Published var show_only_preferred_languages: Bool { @Setting(key: "repost_notification", default_value: true)
didSet { var repost_notification: Bool
UserDefaults.standard.set(show_only_preferred_languages, forKey: "show_only_preferred_languages")
} @Setting(key: "dm_notification", default_value: true)
} var dm_notification: Bool
@Setting(key: "like_notification", default_value: true)
var like_notification: Bool
@Setting(key: "notification_only_from_following", default_value: false)
var notification_only_from_following: Bool
@Setting(key: "translate_dms", default_value: false)
var translate_dms: Bool
@Setting(key: "truncate_timeline_text", default_value: false)
var truncate_timeline_text: Bool
@Setting(key: "truncate_mention_text", default_value: true)
var truncate_mention_text: Bool
@Setting(key: "notification_indicators", default_value: NewEventsBits.all.rawValue)
var notification_indicators: Int
@Setting(key: "auto_translate", default_value: true)
var auto_translate: Bool
@Published var translation_service: TranslationService { @Setting(key: "show_only_preferred_languages", default_value: false)
didSet { var show_only_preferred_languages: Bool
UserDefaults.standard.set(translation_service.rawValue, forKey: "translation_service")
}
}
@Published var deepl_plan: DeepLPlan { @Setting(key: "onlyzaps_mode", default_value: false)
didSet { var onlyzaps_mode: Bool
UserDefaults.standard.set(deepl_plan.rawValue, forKey: "deepl_plan")
} @Setting(key: "disable_animation", default_value: UIAccessibility.isReduceMotionEnabled)
} var disable_animation: Bool
@StringSetting(key: "friend_filter", default_value: .all)
var friend_filter: FriendFilter
@StringSetting(key: "notification_state", default_value: .all)
var notification_state: NotificationFilterState
@Published var deepl_api_key: String { @StringSetting(key: "translation_service", default_value: .none)
var translation_service: TranslationService
@StringSetting(key: "deepl_plan", default_value: .free)
var deepl_plan: DeepLPlan
var deepl_api_key: String {
didSet { didSet {
do { do {
if deepl_api_key == "" { if deepl_api_key == "" {
@@ -226,31 +157,14 @@ class UserSettingsStore: ObservableObject {
} }
} }
@Published var libretranslate_server: LibreTranslateServer { @StringSetting(key: "libretranslate_server", default_value: .terraprint)
didSet { var libretranslate_server: LibreTranslateServer
if oldValue == libretranslate_server {
return @Setting(key: "libretranslate_url", default_value: "")
} var libretranslate_url: String
UserDefaults.standard.set(libretranslate_server.rawValue, forKey: "libretranslate_server") @Setting(key: "libretranslate_api_key", default_value: "")
var libretranslate_api_key: String {
libretranslate_api_key = ""
if libretranslate_server == .custom {
libretranslate_url = ""
} else {
libretranslate_url = libretranslate_server.model.url!
}
}
}
@Published var libretranslate_url: String {
didSet {
UserDefaults.standard.set(libretranslate_url, forKey: "libretranslate_url")
}
}
@Published var libretranslate_api_key: String {
didSet { didSet {
do { do {
if libretranslate_api_key == "" { if libretranslate_api_key == "" {
@@ -263,71 +177,8 @@ class UserSettingsStore: ObservableObject {
} }
} }
} }
@Published var disable_animation: Bool {
didSet {
UserDefaults.standard.set(disable_animation, forKey: "disable_animation")
}
}
init() { init() {
// TODO: pubkey-scoped settings
let pubkey = ""
self.default_wallet = get_default_wallet(pubkey)
show_wallet_selector = should_show_wallet_selector(pubkey)
always_show_images = UserDefaults.standard.object(forKey: "always_show_images") as? Bool ?? false
default_media_uploader = get_media_uploader(pubkey)
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
zap_vibration = UserDefaults.standard.object(forKey: "zap_vibration") as? Bool ?? false
zap_notification = UserDefaults.standard.object(forKey: "zap_notification") as? Bool ?? true
mention_notification = UserDefaults.standard.object(forKey: "mention_notification") as? Bool ?? true
repost_notification = UserDefaults.standard.object(forKey: "repost_notification") as? Bool ?? true
like_notification = UserDefaults.standard.object(forKey: "like_notification") as? Bool ?? true
dm_notification = UserDefaults.standard.object(forKey: "dm_notification") as? Bool ?? true
notification_indicators = UserDefaults.standard.object(forKey: "notification_indicators") as? Int ?? NewEventsBits.all.rawValue
notification_only_from_following = UserDefaults.standard.object(forKey: "notification_only_from_following") as? Bool ?? false
translate_dms = UserDefaults.standard.object(forKey: "translate_dms") as? Bool ?? false
truncate_timeline_text = UserDefaults.standard.object(forKey: "truncate_timeline_text") as? Bool ?? false
truncate_mention_text = UserDefaults.standard.object(forKey: "truncate_mention_text") as? Bool ?? false
disable_animation = should_disable_image_animation()
auto_translate = UserDefaults.standard.object(forKey: "auto_translate") as? Bool ?? true
show_only_preferred_languages = UserDefaults.standard.object(forKey: "show_only_preferred_languages") as? Bool ?? false
// Note from @tyiu:
// Default translation service is disabled by default for now until we gain some confidence that it is working well in production.
// Instead of throwing all Damus users onto feature immediately, allow for discovery of feature organically.
// Also, we are connecting to servers listed as mirrors on the official LibreTranslate GitHub README that do not require API keys.
// However, we have not asked them for permission to use, so we're trying to be good neighbors for now.
// Opportunity: spin up dedicated trusted LibreTranslate server that requires an API key for any access (or higher rate limit access).
if let translation_service = get_translation_service(pubkey) {
self.translation_service = translation_service
} else {
self.translation_service = .none
}
if let libretranslate_server = get_libretranslate_server(pubkey) {
self.libretranslate_server = libretranslate_server
self.libretranslate_url = get_libretranslate_url(pubkey, server: libretranslate_server) ?? ""
} else {
// Choose a random server to distribute load.
libretranslate_server = .allCases.filter { $0 != .custom }.randomElement()!
libretranslate_url = ""
}
do {
libretranslate_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusLibreTranslateKeychainConfiguration())
} catch {
libretranslate_api_key = ""
}
if let deepl_plan = get_deepl_plan(pubkey) {
self.deepl_plan = deepl_plan
} else {
self.deepl_plan = .free
}
do { do {
deepl_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration()) deepl_api_key = try Vault.getPrivateKey(keychainConfiguration: DamusDeepLKeychainConfiguration())
} catch { } catch {
@@ -374,3 +225,79 @@ struct DamusDeepLKeychainConfiguration: KeychainConfiguration {
var accessGroup: String? = nil var accessGroup: String? = nil
var accountName = "deepl_apikey" var accountName = "deepl_apikey"
} }
func should_show_wallet_selector(_ pubkey: String) -> Bool {
return UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
}
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)
}
+12 -1
View File
@@ -7,7 +7,7 @@
import Foundation import Foundation
enum Wallet: String, CaseIterable, Identifiable { enum Wallet: String, CaseIterable, Identifiable, StringCodable {
var id: String { self.rawValue } var id: String { self.rawValue }
struct Model: Identifiable, Hashable { struct Model: Identifiable, Hashable {
@@ -20,6 +20,17 @@ enum Wallet: String, CaseIterable, Identifiable {
var image: String var image: String
} }
func to_string() -> String {
return rawValue
}
init?(from string: String) {
guard let w = Wallet(rawValue: string) else {
return nil
}
self = w
}
// New url prefixes needed to be added to LSApplicationQueriesSchemes // New url prefixes needed to be added to LSApplicationQueriesSchemes
case system_default_wallet case system_default_wallet
case strike case strike
+1 -1
View File
@@ -22,7 +22,7 @@ class ZapsModel: ObservableObject {
} }
func subscribe() { func subscribe() {
var filter = NostrFilter.filter_kinds([9735]) var filter = NostrFilter.filter_kinds([NostrKind.zap.rawValue])
switch target { switch target {
case .profile(let profile_id): case .profile(let profile_id):
filter.pubkeys = [profile_id] filter.pubkeys = [profile_id]
+5
View File
@@ -52,6 +52,11 @@ class Profile: Codable {
set_val(key, val) set_val(key, val)
} }
var reactions: Bool? {
get { return get_val("reactions"); }
set(s) { set_val("reactions", s) }
}
var deleted: Bool? { var deleted: Bool? {
get { return get_val("deleted"); } get { return get_val("deleted"); }
set(s) { set_val("deleted", s) } set(s) { set_val("deleted", s) }
+31 -12
View File
@@ -13,11 +13,15 @@ import CryptoKit
import NaturalLanguage import NaturalLanguage
enum ValidationResult: Decodable { enum ValidationResult: Decodable {
case unknown
case ok case ok
case bad_id case bad_id
case bad_sig case bad_sig
var is_bad: Bool {
return self == .bad_id || self == .bad_sig
}
} }
struct OtherEvent { struct OtherEvent {
@@ -82,7 +86,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
} }
var too_big: Bool { var too_big: Bool {
return self.content.count > 16000 return self.content.utf8.count > 16000
} }
var should_show_event: Bool { var should_show_event: Bool {
@@ -93,14 +97,6 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
return calculate_event_id(ev: self) == self.id return calculate_event_id(ev: self) == self.id
} }
var is_valid: Bool {
return validity == .ok
}
lazy var validity: ValidationResult = {
return .ok //validate_event(ev: self)
}()
private var _blocks: [Block]? = nil private var _blocks: [Block]? = nil
func blocks(_ privkey: String?) -> [Block] { func blocks(_ privkey: String?) -> [Block] {
if let bs = _blocks { if let bs = _blocks {
@@ -564,7 +560,7 @@ func make_first_contact_event(keypair: Keypair) -> NostrEvent? {
return ev return ev
} }
func make_metadata_event(keypair: Keypair, metadata: NostrMetadata) -> NostrEvent? { func make_metadata_event(keypair: Keypair, metadata: Profile) -> NostrEvent? {
guard let privkey = keypair.privkey else { guard let privkey = keypair.privkey else {
return nil return nil
} }
@@ -724,11 +720,34 @@ func make_zap_request_event(keypair: FullKeypair, content: String, relays: [Rela
return ev return ev
} }
func uniq<T: Hashable>(_ xs: [T]) -> [T] {
var s = Set<T>()
var ys: [T] = []
for x in xs {
if s.contains(x) {
continue
}
s.insert(x)
ys.append(x)
}
return ys
}
func gather_reply_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] { func gather_reply_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
var ids = get_referenced_ids(tags: from.tags, key: "e").first.map { [$0] } ?? [] 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(ReferencedId(ref_id: from.id, relay_id: nil, key: "e"))
ids.append(contentsOf: from.referenced_pubkeys.filter { $0.ref_id != our_pubkey }) ids.append(contentsOf: uniq(from.referenced_pubkeys.filter { $0.ref_id != our_pubkey }))
if from.pubkey != our_pubkey {
ids.append(ReferencedId(ref_id: from.pubkey, relay_id: nil, key: "p"))
}
return ids
}
func gather_quote_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
var ids: [ReferencedId] = []
if from.pubkey != our_pubkey { if from.pubkey != our_pubkey {
ids.append(ReferencedId(ref_id: from.pubkey, relay_id: nil, key: "p")) ids.append(ReferencedId(ref_id: from.pubkey, relay_id: nil, key: "p"))
} }
+3 -3
View File
@@ -41,7 +41,7 @@ struct NostrFilter: Codable, Equatable {
} }
public static var filter_text: NostrFilter { public static var filter_text: NostrFilter {
return filter_kinds([1]) return filter_kinds([NostrKind.text.rawValue])
} }
public static func filter_ids(_ ids: [String]) -> NostrFilter { public static func filter_ids(_ ids: [String]) -> NostrFilter {
@@ -49,11 +49,11 @@ struct NostrFilter: Codable, Equatable {
} }
public static var filter_profiles: NostrFilter { public static var filter_profiles: NostrFilter {
return filter_kinds([0]) return filter_kinds([NostrKind.metadata.rawValue])
} }
public static var filter_contacts: NostrFilter { public static var filter_contacts: NostrFilter {
return filter_kinds([3]) return filter_kinds([NostrKind.contacts.rawValue])
} }
public static func filter_authors(_ authors: [String]) -> NostrFilter { public static func filter_authors(_ authors: [String]) -> NostrFilter {
-25
View File
@@ -1,25 +0,0 @@
//
// NostrMetadata.swift
// damus
//
// Created by William Casarin on 2022-05-21.
//
import Foundation
struct NostrMetadata: Codable {
let display_name: String?
let name: String?
let about: String?
let website: String?
let nip05: String?
let picture: String?
let banner: String?
let lud06: String?
let lud16: String?
}
func create_account_to_metadata(_ model: CreateAccountModel) -> NostrMetadata {
return NostrMetadata(display_name: model.real_name, name: model.nick_name, about: model.about, website: nil, nip05: nil, picture: model.profile_image, banner: nil, lud06: nil, lud16: nil)
}
+1 -3
View File
@@ -89,7 +89,7 @@ final class RelayConnection: WebSocketDelegate {
self.isConnected = false self.isConnected = false
case .text(let txt): case .text(let txt):
if txt.count > 2000 { if txt.utf8.count > 2000 {
DispatchQueue.global(qos: .default).async { DispatchQueue.global(qos: .default).async {
if let ev = decode_nostr_event(txt: txt) { if let ev = decode_nostr_event(txt: txt) {
DispatchQueue.main.async { DispatchQueue.main.async {
@@ -105,8 +105,6 @@ final class RelayConnection: WebSocketDelegate {
} }
} }
print("decode failed for \(txt)")
// TODO: trigger event error
default: default:
break break
+4
View File
@@ -52,6 +52,10 @@ class RelayPool {
var num_connecting: Int { var num_connecting: Int {
return relays.reduce(0) { n, r in n + (r.connection.isConnecting ? 1 : 0) } return relays.reduce(0) { n, r in n + (r.connection.isConnecting ? 1 : 0) }
} }
var num_connected: Int {
return relays.reduce(0) { n, r in n + (r.connection.isConnected ? 1 : 0) }
}
func remove_handler(sub_id: String) { func remove_handler(sub_id: String) {
self.handlers = handlers.filter { $0.sub_id != sub_id } self.handlers = handlers.filter { $0.sub_id != sub_id }
+9
View File
@@ -15,6 +15,7 @@ class EventCache {
private var cancellable: AnyCancellable? private var cancellable: AnyCancellable?
private var translations: [String: TranslateStatus] = [:] private var translations: [String: TranslateStatus] = [:]
private var artifacts: [String: NoteArtifacts] = [:] private var artifacts: [String: NoteArtifacts] = [:]
var validation: [String: ValidationResult] = [:]
//private var thread_latest: [String: Int64] //private var thread_latest: [String: Int64]
@@ -26,6 +27,14 @@ class EventCache {
} }
} }
func is_event_valid(_ evid: String) -> ValidationResult {
guard let result = validation[evid] else {
return .unknown
}
return result
}
func store_translation_artifacts(evid: String, translated: TranslateStatus) { func store_translation_artifacts(evid: String, translated: TranslateStatus) {
self.translations[evid] = translated self.translations[evid] = translated
} }
+2
View File
@@ -35,7 +35,9 @@ let custom_hashtags: [String: CustomHashtag] = [
"coffeechain": CustomHashtag.coffee, "coffeechain": CustomHashtag.coffee,
"plebchain": CustomHashtag.plebchain, "plebchain": CustomHashtag.plebchain,
"zap": CustomHashtag.zap, "zap": CustomHashtag.zap,
"zaps": CustomHashtag.zap,
"zapathon": CustomHashtag.zap, "zapathon": CustomHashtag.zap,
"onlyzaps": CustomHashtag.zap,
] ]
func hashtag_str(_ htag: String) -> CompatibleText { func hashtag_str(_ htag: String) -> CompatibleText {
+47
View File
@@ -0,0 +1,47 @@
//
// LocalNotification.swift
// damus
//
// Created by William Casarin on 2023-04-15.
//
import Foundation
struct LossyLocalNotification {
let type: LocalNotificationType
let event_id: String
func to_user_info() -> [AnyHashable: Any] {
return [
"type": self.type.rawValue,
"evid": self.event_id
]
}
static func from_user_info(user_info: [AnyHashable: Any]) -> LossyLocalNotification {
let target_id = user_info["evid"] as! String
let typestr = user_info["type"] as! String
let type = LocalNotificationType(rawValue: typestr)!
return LossyLocalNotification(type: type, event_id: target_id)
}
}
struct LocalNotification {
let type: LocalNotificationType
let event: NostrEvent
let target: NostrEvent
let content: String
func to_lossy() -> LossyLocalNotification {
return LossyLocalNotification(type: self.type, event_id: self.target.id)
}
}
enum LocalNotificationType: String {
case dm
case like
case mention
case repost
case zap
}
+6
View File
@@ -110,6 +110,12 @@ extension Notification.Name {
static var unmute_thread: Notification.Name { static var unmute_thread: Notification.Name {
return Notification.Name("unmute_thread") return Notification.Name("unmute_thread")
} }
static var local_notification: Notification.Name {
return Notification.Name("local_notification")
}
static var onlyzaps_mode: Notification.Name {
return Notification.Name("hide_reactions")
}
} }
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher { func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {
+13
View File
@@ -0,0 +1,13 @@
//
// StringCodable.swift
// damus
//
// Created by William Casarin on 2023-04-21.
//
import Foundation
protocol StringCodable {
init?(from string: String)
func to_string() -> String
}
+4
View File
@@ -52,6 +52,10 @@ struct Zap {
public let is_anon: Bool public let is_anon: Bool
public let private_request: NostrEvent? public let private_request: NostrEvent?
var request_ev: NostrEvent {
return private_request ?? self.request.ev
}
public static func from_zap_event(zap_ev: NostrEvent, zapper: String, our_privkey: String?) -> Zap? { public static func from_zap_event(zap_ev: NostrEvent, zapper: String, our_privkey: String?) -> Zap? {
/// Make sure that we only create a zap event if it is authorized by the profile or event /// Make sure that we only create a zap event if it is authorized by the profile or event
guard zapper == zap_ev.pubkey else { guard zapper == zap_ev.pubkey else {
+27 -22
View File
@@ -30,6 +30,7 @@ struct EventActionBar: View {
@State var show_share_action: Bool = false @State var show_share_action: Bool = false
@ObservedObject var bar: ActionBarModel @ObservedObject var bar: ActionBarModel
@ObservedObject var settings: UserSettingsStore
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@@ -38,12 +39,21 @@ struct EventActionBar: View {
self.event = event self.event = event
self.test_lnurl = test_lnurl self.test_lnurl = test_lnurl
_bar = ObservedObject(wrappedValue: bar ?? make_actionbar_model(ev: event.id, damus: damus_state)) _bar = ObservedObject(wrappedValue: bar ?? make_actionbar_model(ev: event.id, damus: damus_state))
_settings = ObservedObject(wrappedValue: damus_state.settings)
} }
var lnurl: String? { var lnurl: String? {
test_lnurl ?? damus_state.profiles.lookup(id: event.pubkey)?.lnurl test_lnurl ?? damus_state.profiles.lookup(id: event.pubkey)?.lnurl
} }
var show_like: Bool {
if settings.onlyzaps_mode {
return false
}
return true
}
var body: some View { var body: some View {
HStack { HStack {
if damus_state.keypair.privkey != nil { if damus_state.keypair.privkey != nil {
@@ -72,22 +82,25 @@ struct EventActionBar: View {
.font(.footnote.weight(.medium)) .font(.footnote.weight(.medium))
.foregroundColor(bar.boosted ? Color.green : Color.gray) .foregroundColor(bar.boosted ? Color.green : Color.gray)
} }
Spacer()
if show_like {
HStack(spacing: 4) { Spacer()
LikeButton(liked: bar.liked) {
if bar.liked { HStack(spacing: 4) {
notify(.delete, bar.our_like) LikeButton(liked: bar.liked) {
} else { if bar.liked {
send_like() notify(.delete, bar.our_like)
} else {
send_like()
}
} }
Text(verbatim: "\(bar.likes > 0 ? "\(bar.likes)" : "")")
.font(.footnote.weight(.medium))
.nip05_colorized(gradient: bar.liked)
} }
Text(verbatim: "\(bar.likes > 0 ? "\(bar.likes)" : "")")
.font(.footnote.weight(.medium))
.nip05_colorized(gradient: bar.liked)
} }
if let lnurl = self.lnurl { if let lnurl = self.lnurl {
Spacer() Spacer()
ZapButton(damus_state: damus_state, event: event, lnurl: lnurl, bar: bar) ZapButton(damus_state: damus_state, event: event, lnurl: lnurl, bar: bar)
@@ -137,15 +150,7 @@ struct EventActionBar: View {
} }
func send_boost() { func send_boost() {
guard let privkey = self.damus_state.keypair.privkey else { notify(.boost, self.event)
return
}
let boost = make_boost_event(pubkey: damus_state.keypair.pubkey, privkey: privkey, boosted: self.event)
self.bar.our_boost = boost
notify(.boost, boost)
} }
func send_like() { func send_like() {
+1 -1
View File
@@ -32,7 +32,7 @@ struct EventDetailBar: View {
.buttonStyle(PlainButtonStyle()) .buttonStyle(PlainButtonStyle())
} }
if bar.likes > 0 { if bar.likes > 0 && !state.settings.onlyzaps_mode {
NavigationLink(destination: ReactionsView(damus_state: state, model: ReactionsModel(state: state, target: target))) { NavigationLink(destination: ReactionsView(damus_state: state, model: ReactionsModel(state: state, target: target))) {
let noun = Text(verbatim: "\(reactionsCountString(bar.likes))").foregroundColor(.gray) let noun = Text(verbatim: "\(reactionsCountString(bar.likes))").foregroundColor(.gray)
Text("\(Text("\(bar.likes)").font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.") Text("\(Text("\(bar.likes)").font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.")
+13 -1
View File
@@ -89,10 +89,22 @@ extension NSMutableData {
} }
} }
enum MediaUploader: String, CaseIterable, Identifiable { enum MediaUploader: String, CaseIterable, Identifiable, StringCodable {
var id: String { self.rawValue } var id: String { self.rawValue }
case nostrBuild case nostrBuild
case nostrImg case nostrImg
init?(from string: String) {
guard let mu = MediaUploader(rawValue: string) else {
return nil
}
self = mu
}
func to_string() -> String {
return rawValue
}
var nameParam: String { var nameParam: String {
switch self { switch self {
+44
View File
@@ -0,0 +1,44 @@
//
// FriendsButton.swift
// damus
//
// Created by William Casarin on 2023-04-21.
//
import SwiftUI
struct FriendsButton: View {
@Binding var filter: FriendFilter
var body: some View {
Button(action: {
switch self.filter {
case .all:
self.filter = .friends
case .friends:
self.filter = .all
}
}) {
if filter == .friends {
LINEAR_GRADIENT
.mask(Image(systemName: "person.2.fill")
.resizable()
).frame(width: 30, height: 20)
} else {
Image(systemName: "person.2.fill")
.resizable()
.frame(width: 30, height: 20)
.foregroundColor(DamusColors.adaptableGrey)
}
}
.buttonStyle(.plain)
}
}
struct FriendsButton_Previews: PreviewProvider {
@State static var enabled: FriendFilter = .all
static var previews: some View {
FriendsButton(filter: $enabled)
}
}
+5 -4
View File
@@ -45,7 +45,7 @@ struct ConfigView: View {
} }
NavigationLink(destination: NotificationSettingsView(settings: settings)) { NavigationLink(destination: NotificationSettingsView(settings: settings)) {
IconLabel(NSLocalizedString("Local Notifications", comment: "Section header for damus local notifications user configuration"), img_name: "bell.fill", color: .blue) IconLabel(NSLocalizedString("Notifications", comment: "Section header for Damus notifications"), img_name: "bell.fill", color: .blue)
} }
NavigationLink(destination: ZapSettingsView(pubkey: state.pubkey, settings: settings)) { NavigationLink(destination: ZapSettingsView(pubkey: state.pubkey, settings: settings)) {
@@ -144,14 +144,15 @@ struct ConfigView_Previews: PreviewProvider {
func handle_string_amount(new_value: String) -> Int? { func handle_string_amount(new_value: String) -> Int? {
let digits = Set("0123456789") let filtered = new_value.filter {
let filtered = new_value.filter { digits.contains($0) } $0.isNumber
}
if filtered == "" { if filtered == "" {
return nil return nil
} }
guard let amt = Int(filtered) else { guard let amt = NumberFormatter().number(from: filtered) as? Int else {
return nil return nil
} }
+8 -6
View File
@@ -9,10 +9,13 @@ import SwiftUI
struct DMChatView: View { struct DMChatView: View {
let damus_state: DamusState let damus_state: DamusState
let pubkey: String @ObservedObject var dms: DirectMessageModel
@EnvironmentObject var dms: DirectMessageModel
@State var showPrivateKeyWarning: Bool = false @State var showPrivateKeyWarning: Bool = false
var pubkey: String {
dms.pubkey
}
var Messages: some View { var Messages: some View {
ScrollViewReader { scroller in ScrollViewReader { scroller in
ScrollView { ScrollView {
@@ -177,10 +180,9 @@ struct DMChatView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let ev = NostrEvent(content: "hi", pubkey: "pubkey", kind: 1, tags: []) let ev = NostrEvent(content: "hi", pubkey: "pubkey", kind: 1, tags: [])
let model = DirectMessageModel(events: [ev], our_pubkey: "pubkey") let model = DirectMessageModel(events: [ev], our_pubkey: "pubkey", pubkey: "the_pk")
DMChatView(damus_state: test_damus_state(), pubkey: "pubkey") DMChatView(damus_state: test_damus_state(), dms: model)
.environmentObject(model)
} }
} }
+33 -26
View File
@@ -16,21 +16,13 @@ struct DirectMessagesView: View {
let damus_state: DamusState let damus_state: DamusState
@State var dm_type: DMType = .friend @State var dm_type: DMType = .friend
@State var open_dm: Bool = false @ObservedObject var model: DirectMessagesModel
@State var pubkey: String = "" @ObservedObject var settings: UserSettingsStore
@EnvironmentObject var model: DirectMessagesModel
@State var active_model: DirectMessageModel
init(damus_state: DamusState) {
self.damus_state = damus_state
self._active_model = State(initialValue: DirectMessageModel(our_pubkey: damus_state.pubkey))
}
func MainContent(requests: Bool) -> some View { func MainContent(requests: Bool) -> some View {
ScrollView { ScrollView {
let chat = DMChatView(damus_state: damus_state, pubkey: pubkey) let chat = DMChatView(damus_state: damus_state, dms: model.active_model)
.environmentObject(active_model) NavigationLink(destination: chat, isActive: $model.open_dm) {
NavigationLink(destination: chat, isActive: $open_dm) {
EmptyView() EmptyView()
} }
LazyVStack(spacing: 0) { LazyVStack(spacing: 0) {
@@ -38,12 +30,9 @@ struct DirectMessagesView: View {
EmptyTimelineView() EmptyTimelineView()
} else { } else {
let dms = requests ? model.message_requests : model.friend_dms let dms = requests ? model.message_requests : model.friend_dms
ForEach(dms, id: \.0) { tup in ForEach(dms, id: \.pubkey) { dm in
MaybeEvent(tup) MaybeEvent(dm)
.padding(.top, 10) .padding(.top, 10)
Divider()
.padding([.top], 10)
} }
} }
} }
@@ -59,15 +48,17 @@ struct DirectMessagesView: View {
return [.truncate_content, .no_action_bar, .no_translate] return [.truncate_content, .no_action_bar, .no_translate]
} }
func MaybeEvent(_ tup: (String, DirectMessageModel)) -> some View { func MaybeEvent(_ model: DirectMessageModel) -> some View {
Group { Group {
if let ev = tup.1.events.last { let ok = damus_state.settings.friend_filter.filter(contacts: damus_state.contacts, pubkey: model.pubkey)
EventView(damus: damus_state, event: ev, pubkey: tup.0, options: options) if ok, let ev = model.events.last {
EventView(damus: damus_state, event: ev, pubkey: model.pubkey, options: options)
.onTapGesture { .onTapGesture {
pubkey = tup.0 self.model.open_dm_by_model(model)
active_model = tup.1
open_dm = true
} }
Divider()
.padding([.top], 10)
} else { } else {
EmptyView() EmptyView()
} }
@@ -95,10 +86,28 @@ struct DirectMessagesView: View {
} }
.tabViewStyle(.page(indexDisplayMode: .never)) .tabViewStyle(.page(indexDisplayMode: .never))
} }
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
if would_filter_non_friends_from_dms(contacts: damus_state.contacts, dms: self.model.dms) {
FriendsButton(filter: $settings.friend_filter)
}
}
}
.navigationTitle(NSLocalizedString("DMs", comment: "Navigation title for view of DMs, where DM is an English abbreviation for Direct Message.")) .navigationTitle(NSLocalizedString("DMs", comment: "Navigation title for view of DMs, where DM is an English abbreviation for Direct Message."))
} }
} }
func would_filter_non_friends_from_dms(contacts: Contacts, dms: [DirectMessageModel]) -> Bool {
for dm in dms {
if !FriendFilter.friends.filter(contacts: contacts, pubkey: dm.pubkey) {
return true
}
}
return false
}
struct DirectMessagesView_Previews: PreviewProvider { struct DirectMessagesView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let ev = NostrEvent(content: "encrypted stuff", let ev = NostrEvent(content: "encrypted stuff",
@@ -106,8 +115,6 @@ struct DirectMessagesView_Previews: PreviewProvider {
kind: 4, kind: 4,
tags: []) tags: [])
let ds = test_damus_state() let ds = test_damus_state()
let model = DirectMessageModel(events: [ev], our_pubkey: ds.pubkey) DirectMessagesView(damus_state: ds, model: ds.dms, settings: ds.settings)
DirectMessagesView(damus_state: ds)
.environmentObject(model)
} }
} }
-13
View File
@@ -69,19 +69,6 @@ func should_show_images(settings: UserSettingsStore, contacts: Contacts, ev: Nos
return false return false
} }
func event_validity_color(_ validation: ValidationResult) -> some View {
Group {
switch validation {
case .ok:
EmptyView()
case .bad_id:
Color.orange.opacity(0.4)
case .bad_sig:
Color.red.opacity(0.4)
}
}
}
extension View { extension View {
func pubkey_context_menu(bech32_pubkey: String) -> some View { func pubkey_context_menu(bech32_pubkey: String) -> some View {
return self.contextMenu { return self.contextMenu {
+2 -6
View File
@@ -48,10 +48,6 @@ struct BuilderEventView: View {
return return
} }
guard nostr_event.known_kind == .text else {
return
}
if event != nil { if event != nil {
return return
} }
@@ -78,8 +74,8 @@ struct BuilderEventView: View {
let thread = ThreadModel(event: ev, damus_state: damus) let thread = ThreadModel(event: ev, damus_state: damus)
let dest = ThreadView(state: damus, thread: thread) let dest = ThreadView(state: damus, thread: thread)
NavigationLink(destination: dest) { NavigationLink(destination: dest) {
EmbeddedEventView(damus_state: damus, event: event) EventView(damus: damus, event: event, options: .embedded)
.padding(8) .padding([.top, .bottom], 8)
}.buttonStyle(.plain) }.buttonStyle(.plain)
} else { } else {
ProgressView().padding() ProgressView().padding()
@@ -1,47 +0,0 @@
//
// EmbeddedEventView.swift
// damus
//
// Created by William Casarin on 2023-01-23.
//
import SwiftUI
struct EmbeddedEventView: View {
let damus_state: DamusState
let event: NostrEvent
var pubkey: String {
event.pubkey
}
var body: some View {
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey)
HStack {
EventProfile(damus_state: damus_state, pubkey: pubkey, profile: profile, size: .small)
Spacer()
EventMenuContext(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads)
.padding([.bottom], 4)
}
.minimumScaleFactor(0.75)
.lineLimit(1)
if event_is_reply(event, privkey: damus_state.keypair.privkey) {
ReplyDescription(event: event, profiles: damus_state.profiles)
}
EventBody(damus_state: damus_state, event: event, size: .small, options: [.truncate_content])
}
}
}
struct EmbeddedEventView_Previews: PreviewProvider {
static var previews: some View {
EmbeddedEventView(damus_state: test_damus_state(), event: test_event)
.padding()
}
}
+2 -2
View File
@@ -46,7 +46,7 @@ struct MenuItems: View {
let bookmarked = bookmarks.isBookmarked(event) let bookmarked = bookmarks.isBookmarked(event)
self._isBookmarked = State(initialValue: bookmarked) self._isBookmarked = State(initialValue: bookmarked)
let muted_thread = muted_threads.isMutedThread(event) let muted_thread = muted_threads.isMutedThread(event, privkey: keypair.privkey)
self._isMutedThread = State(initialValue: muted_thread) self._isMutedThread = State(initialValue: muted_thread)
self.bookmarks = bookmarks self.bookmarks = bookmarks
@@ -96,7 +96,7 @@ struct MenuItems: View {
if event.known_kind != .dm { if event.known_kind != .dm {
Button { Button {
self.muted_threads.updateMutedThread(event) self.muted_threads.updateMutedThread(event)
let muted = self.muted_threads.isMutedThread(event) let muted = self.muted_threads.isMutedThread(event, privkey: self.keypair.privkey)
isMutedThread = muted isMutedThread = muted
} label: { } label: {
let imageName = isMutedThread ? "speaker" : "speaker.slash" let imageName = isMutedThread ? "speaker" : "speaker.slash"
+2 -4
View File
@@ -10,15 +10,13 @@ import SwiftUI
struct MutedEventView: View { struct MutedEventView: View {
let damus_state: DamusState let damus_state: DamusState
let event: NostrEvent let event: NostrEvent
let scroller: ScrollViewProxy?
let selected: Bool let selected: Bool
@State var shown: Bool @State var shown: Bool
init(damus_state: DamusState, event: NostrEvent, scroller: ScrollViewProxy?, selected: Bool) { init(damus_state: DamusState, event: NostrEvent, selected: Bool) {
self.damus_state = damus_state self.damus_state = damus_state
self.event = event self.event = event
self.scroller = scroller
self.selected = selected self.selected = selected
self._shown = State(initialValue: should_show_event(contacts: damus_state.contacts, ev: event)) self._shown = State(initialValue: should_show_event(contacts: damus_state.contacts, ev: event))
} }
@@ -89,7 +87,7 @@ struct MutedEventView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
MutedEventView(damus_state: test_damus_state(), event: test_event, scroller: nil, selected: false) MutedEventView(damus_state: test_damus_state(), event: test_event, selected: false)
.frame(width: .infinity, height: 50) .frame(width: .infinity, height: 50)
} }
} }
+17 -5
View File
@@ -8,7 +8,8 @@
import SwiftUI import SwiftUI
struct EventViewOptions: OptionSet { struct EventViewOptions: OptionSet {
let rawValue: UInt8 let rawValue: UInt32
static let no_action_bar = EventViewOptions(rawValue: 1 << 0) static let no_action_bar = EventViewOptions(rawValue: 1 << 0)
static let no_replying_to = EventViewOptions(rawValue: 1 << 1) static let no_replying_to = EventViewOptions(rawValue: 1 << 1)
static let no_images = EventViewOptions(rawValue: 1 << 2) static let no_images = EventViewOptions(rawValue: 1 << 2)
@@ -16,6 +17,10 @@ struct EventViewOptions: OptionSet {
static let truncate_content = EventViewOptions(rawValue: 1 << 4) static let truncate_content = EventViewOptions(rawValue: 1 << 4)
static let pad_content = EventViewOptions(rawValue: 1 << 5) static let pad_content = EventViewOptions(rawValue: 1 << 5)
static let no_translate = EventViewOptions(rawValue: 1 << 6) static let no_translate = EventViewOptions(rawValue: 1 << 6)
static let small_pfp = EventViewOptions(rawValue: 1 << 7)
static let nested = EventViewOptions(rawValue: 1 << 8)
static let embedded: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested]
} }
struct TextEvent: View { struct TextEvent: View {
@@ -37,14 +42,13 @@ struct TextEvent: View {
} }
} }
.contentShape(Rectangle()) .contentShape(Rectangle())
.background(event_validity_color(event.validity))
.id(event.id) .id(event.id)
.frame(maxWidth: .infinity, minHeight: PFP_SIZE) .frame(maxWidth: .infinity, minHeight: PFP_SIZE)
.padding([.bottom], 2) .padding([.bottom], 2)
} }
func Pfp(is_anon: Bool) -> some View { func Pfp(is_anon: Bool) -> some View {
MaybeAnonPfpView(state: damus, is_anon: is_anon, pubkey: pubkey) MaybeAnonPfpView(state: damus, is_anon: is_anon, pubkey: pubkey, size: options.contains(.small_pfp) ? eventview_pfp_size(.small) : PFP_SIZE )
} }
func TopPart(is_anon: Bool) -> some View { func TopPart(is_anon: Bool) -> some View {
@@ -83,7 +87,7 @@ struct TextEvent: View {
EvBody(options: self.options.union(.pad_content)) EvBody(options: self.options.union(.pad_content))
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) { if let mention = get_mention() {
Mention(mention) Mention(mention)
.padding(.horizontal) .padding(.horizontal)
} }
@@ -136,6 +140,14 @@ struct TextEvent: View {
return Rectangle().frame(height: 2).opacity(0) return Rectangle().frame(height: 2).opacity(0)
} }
func get_mention() -> Mention? {
if self.options.contains(.nested) {
return nil
}
return first_eref_mention(ev: event, privkey: damus.keypair.privkey)
}
var ThreadedStyle: some View { var ThreadedStyle: some View {
HStack(alignment: .top) { HStack(alignment: .top) {
@@ -152,7 +164,7 @@ struct TextEvent: View {
ReplyPart ReplyPart
EvBody(options: self.options) EvBody(options: self.options)
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) { if let mention = get_mention() {
Mention(mention) Mention(mention)
} }
+2 -5
View File
@@ -21,14 +21,11 @@ struct FollowUserView: View {
} }
HStack { HStack {
UserView(damus_state: damus_state, pubkey: target.pubkey) UserViewRow(damus_state: damus_state, pubkey: target.pubkey)
.contentShape(Rectangle())
.onTapGesture {
navigating = true
}
FollowButtonView(target: target, follows_you: false, follow_state: damus_state.contacts.follow_state(target.pubkey)) FollowButtonView(target: target, follows_you: false, follow_state: damus_state.contacts.follow_state(target.pubkey))
} }
Spacer()
} }
} }
+8 -4
View File
@@ -15,6 +15,7 @@ struct ImagePicker: UIViewControllerRepresentable {
let sourceType: UIImagePickerController.SourceType let sourceType: UIImagePickerController.SourceType
let pubkey: String let pubkey: String
@Binding var image_upload_confirm: Bool
var imagesOnly: Bool = false var imagesOnly: Bool = false
let onImagePicked: (URL) -> Void let onImagePicked: (URL) -> Void
let onVideoPicked: (URL) -> Void let onVideoPicked: (URL) -> Void
@@ -24,15 +25,18 @@ struct ImagePicker: UIViewControllerRepresentable {
private let sourceType: UIImagePickerController.SourceType private let sourceType: UIImagePickerController.SourceType
private let onImagePicked: (URL) -> Void private let onImagePicked: (URL) -> Void
private let onVideoPicked: (URL) -> Void private let onVideoPicked: (URL) -> Void
@Binding var image_upload_confirm: Bool
init(presentationMode: Binding<PresentationMode>, init(presentationMode: Binding<PresentationMode>,
sourceType: UIImagePickerController.SourceType, sourceType: UIImagePickerController.SourceType,
onImagePicked: @escaping (URL) -> Void, onImagePicked: @escaping (URL) -> Void,
onVideoPicked: @escaping (URL) -> Void) { onVideoPicked: @escaping (URL) -> Void,
image_upload_confirm: Binding<Bool>) {
_presentationMode = presentationMode _presentationMode = presentationMode
self.sourceType = sourceType self.sourceType = sourceType
self.onImagePicked = onImagePicked self.onImagePicked = onImagePicked
self.onVideoPicked = onVideoPicked self.onVideoPicked = onVideoPicked
self._image_upload_confirm = image_upload_confirm
} }
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
@@ -51,9 +55,9 @@ struct ImagePicker: UIViewControllerRepresentable {
onImagePicked(editedImageURL) onImagePicked(editedImageURL)
} }
} }
presentationMode.dismiss() image_upload_confirm = true
} }
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
presentationMode.dismiss() presentationMode.dismiss()
} }
@@ -98,7 +102,7 @@ struct ImagePicker: UIViewControllerRepresentable {
onVideoPicked: { videoURL in onVideoPicked: { videoURL in
// Handle the selected video URL // Handle the selected video URL
onVideoPicked(videoURL) onVideoPicked(videoURL)
}) }, image_upload_confirm: $image_upload_confirm)
} }
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController { func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
+1 -1
View File
@@ -37,7 +37,7 @@ struct MutelistView: View {
var body: some View { var body: some View {
List(users, id: \.self) { pubkey in List(users, id: \.self) { pubkey in
UserView(damus_state: damus_state, pubkey: pubkey) UserViewRow(damus_state: damus_state, pubkey: pubkey)
.id(pubkey) .id(pubkey)
.swipeActions { .swipeActions {
RemoveAction(pubkey: pubkey) RemoveAction(pubkey: pubkey)
+146 -42
View File
@@ -7,11 +7,90 @@
import SwiftUI import SwiftUI
enum NotificationFilterState: String { enum FriendFilter: String, StringCodable {
case all
case friends
init?(from string: String) {
guard let ff = FriendFilter(rawValue: string) else {
return nil
}
self = ff
}
func to_string() -> String {
self.rawValue
}
func filter(contacts: Contacts, pubkey: String) -> Bool {
switch self {
case .all:
return true
case .friends:
return contacts.is_in_friendosphere(pubkey)
}
}
}
class NotificationFilter: ObservableObject, Equatable {
@Published var state: NotificationFilterState
@Published var fine_filter: FriendFilter
static func == (lhs: NotificationFilter, rhs: NotificationFilter) -> Bool {
return lhs.state == rhs.state && lhs.fine_filter == rhs.fine_filter
}
init() {
self.state = .all
self.fine_filter = .all
}
init(state: NotificationFilterState, fine_filter: FriendFilter) {
self.state = state
self.fine_filter = fine_filter
}
func toggle_fine_filter() {
switch self.fine_filter {
case .all:
self.fine_filter = .friends
case .friends:
self.fine_filter = .all
}
}
func filter(contacts: Contacts, items: [NotificationItem]) -> [NotificationItem] {
return items.reduce(into: []) { acc, item in
if !self.state.filter(item) {
return
}
if let item = item.filter({ self.fine_filter.filter(contacts: contacts, pubkey: $0.pubkey) }) {
acc.append(item)
}
}
}
}
enum NotificationFilterState: String, StringCodable {
case all case all
case zaps case zaps
case replies 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 { func is_other( item: NotificationItem) -> Bool {
return item.is_zap == nil && item.is_reply == nil return item.is_zap == nil && item.is_reply == nil
} }
@@ -31,34 +110,67 @@ enum NotificationFilterState: String {
struct NotificationsView: View { struct NotificationsView: View {
let state: DamusState let state: DamusState
@ObservedObject var notifications: NotificationsModel @ObservedObject var notifications: NotificationsModel
@State var filter_state: NotificationFilterState = .all @StateObject var filter_state: NotificationFilter = NotificationFilter()
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
var body: some View { var mystery: some View {
TabView(selection: $filter_state) { VStack(spacing: 20) {
// This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why. Text("Wake up, \(Profile.displayName(profile: state.profiles.lookup(id: state.pubkey), pubkey: state.pubkey).display_name)", comment: "Text telling the user to wake up, where the argument is their display name.")
Text("") Text("You are dreaming...", comment: "Text telling the user that they are dreaming.")
.id("what")
NotificationTab(NotificationFilterState.all)
.tag(NotificationFilterState.all)
NotificationTab(NotificationFilterState.zaps)
.tag(NotificationFilterState.zaps)
NotificationTab(NotificationFilterState.replies)
.tag(NotificationFilterState.replies)
} }
.onChange(of: filter_state) { val in .id("what")
save_notification_filter_state(pubkey: state.pubkey, state: val) }
var body: some View {
TabView(selection: $filter_state.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
)
)
.tag(NotificationFilterState.all)
NotificationTab(
NotificationFilter(
state: .zaps,
fine_filter: filter_state.fine_filter
)
)
.tag(NotificationFilterState.zaps)
NotificationTab(
NotificationFilter(
state: .replies,
fine_filter: filter_state.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)
}
}
}
.onChange(of: filter_state.fine_filter) { val in
state.settings.friend_filter = val
}
.onChange(of: filter_state.state) { val in
state.settings.notification_state = val
} }
.onAppear { .onAppear {
self.filter_state = load_notification_filter_state(pubkey: state.pubkey) self.filter_state.fine_filter = state.settings.friend_filter
self.filter_state.state = state.settings.notification_state
} }
.safeAreaInset(edge: .top, spacing: 0) { .safeAreaInset(edge: .top, spacing: 0) {
VStack(spacing: 0) { VStack(spacing: 0) {
CustomPicker(selection: $filter_state, content: { CustomPicker(selection: $filter_state.state, content: {
Text("All", comment: "Label for filter for all notifications.") Text("All", comment: "Label for filter for all notifications.")
.tag(NotificationFilterState.all) .tag(NotificationFilterState.all)
@@ -76,14 +188,14 @@ struct NotificationsView: View {
} }
} }
func NotificationTab(_ filter: NotificationFilterState) -> some View { func NotificationTab(_ filter: NotificationFilter) -> some View {
ScrollViewReader { scroller in ScrollViewReader { scroller in
ScrollView { ScrollView {
LazyVStack(alignment: .leading) { LazyVStack(alignment: .leading) {
Color.white.opacity(0) Color.white.opacity(0)
.id("startblock") .id("startblock")
.frame(height: 5) .frame(height: 5)
ForEach(notifications.notifications.filter(filter.filter), id: \.id) { item in ForEach(filter.filter(contacts: state.contacts, items: notifications.notifications), id: \.id) { item in
NotificationItemView(state: state, item: item) NotificationItemView(state: state, item: item)
} }
} }
@@ -109,30 +221,22 @@ struct NotificationsView: View {
struct NotificationsView_Previews: PreviewProvider { struct NotificationsView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
NotificationsView(state: test_damus_state(), notifications: NotificationsModel(), filter_state: NotificationFilterState.all) NotificationsView(state: test_damus_state(), notifications: NotificationsModel(), filter_state: NotificationFilter())
} }
} }
func notification_filter_state_key(pubkey: String) -> String { func would_filter_non_friends_from_notifications(contacts: Contacts, state: NotificationFilterState, items: [NotificationItem]) -> Bool {
return pk_setting_key(pubkey, key: "notification_filter_state") for item in items {
} // this is only valid depending on which tab we're looking at
if !state.filter(item) {
func load_notification_filter_state(pubkey: String) -> NotificationFilterState { continue
let key = notification_filter_state_key(pubkey: pubkey) }
guard let state_str = UserDefaults.standard.string(forKey: key) else { if item.would_filter({ ev in FriendFilter.friends.filter(contacts: contacts, pubkey: ev.pubkey) }) {
return .all return true
}
} }
guard let state = NotificationFilterState(rawValue: state_str) else { return false
return .all
}
return state
} }
func save_notification_filter_state(pubkey: String, state: NotificationFilterState) {
let key = notification_filter_state_key(pubkey: pubkey)
UserDefaults.standard.set(state.rawValue, forKey: key)
}
@@ -1,5 +1,5 @@
// //
// ParicipantsView.swift // ParticipantsView.swift
// damus // damus
// //
// Created by Joel Klabo on 1/18/23. // Created by Joel Klabo on 1/18/23.
+255 -64
View File
@@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import AVFoundation
enum NostrPostResult { enum NostrPostResult {
case post(NostrPost) case post(NostrPost)
@@ -14,6 +15,23 @@ enum NostrPostResult {
let POST_PLACEHOLDER = NSLocalizedString("Type your post here...", comment: "Text box prompt to ask user to type their post.") let POST_PLACEHOLDER = NSLocalizedString("Type your post here...", comment: "Text box prompt to ask user to type their post.")
enum PostAction {
case replying_to(NostrEvent)
case quoting(NostrEvent)
case posting
var ev: NostrEvent? {
switch self {
case .replying_to(let ev):
return ev
case .quoting(let ev):
return ev
case .posting:
return nil
}
}
}
struct PostView: View { struct PostView: View {
@State var post: NSMutableAttributedString = NSMutableAttributedString() @State var post: NSMutableAttributedString = NSMutableAttributedString()
@FocusState var focus: Bool @FocusState var focus: Bool
@@ -21,13 +39,16 @@ struct PostView: View {
@State var attach_media: Bool = false @State var attach_media: Bool = false
@State var attach_camera: Bool = false @State var attach_camera: Bool = false
@State var error: String? = nil @State var error: String? = nil
@State var uploadedMedias: [UploadedMedia] = []
@State var image_upload_confirm: Bool = false
@State var originalReferences: [ReferencedId] = [] @State var originalReferences: [ReferencedId] = []
@State var references: [ReferencedId] = [] @State var references: [ReferencedId] = []
@State var mediaToUpload: MediaUpload? = nil
@StateObject var image_upload: ImageUploadModel = ImageUploadModel() @StateObject var image_upload: ImageUploadModel = ImageUploadModel()
let replying_to: NostrEvent? let action: PostAction
let damus_state: DamusState let damus_state: DamusState
@Environment(\.presentationMode) var presentationMode @Environment(\.presentationMode) var presentationMode
@@ -47,7 +68,8 @@ struct PostView: View {
func send_post() { func send_post() {
var kind: NostrKind = .text var kind: NostrKind = .text
if replying_to?.known_kind == .chat {
if case .replying_to(let ev) = action, ev.known_kind == .chat {
kind = .chat kind = .chat
} }
@@ -57,22 +79,27 @@ struct PostView: View {
} }
} }
let content = self.post.string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) var content = self.post.string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: " ")
content.append(" " + imagesString + " ")
if case .quoting(let ev) = action, let id = bech32_note_id(ev.id) {
content.append(" nostr:" + id)
}
let new_post = NostrPost(content: content, references: references, kind: kind) let new_post = NostrPost(content: content, references: references, kind: kind)
NotificationCenter.default.post(name: .post, object: NostrPostResult.post(new_post)) NotificationCenter.default.post(name: .post, object: NostrPostResult.post(new_post))
if let replying_to { clear_draft()
damus_state.drafts.replies.removeValue(forKey: replying_to)
} else {
damus_state.drafts.post = NSMutableAttributedString(string: "")
}
dismiss() dismiss()
} }
var is_post_empty: Bool { var is_post_empty: Bool {
return post.string.allSatisfy { $0.isWhitespace } return post.string.allSatisfy { $0.isWhitespace } && uploadedMedias.isEmpty
} }
var ImageButton: some View { var ImageButton: some View {
@@ -118,17 +145,67 @@ struct PostView: View {
.clipShape(Capsule()) .clipShape(Capsule())
} }
var isEmpty: Bool {
self.uploadedMedias.count == 0 &&
self.post.mutableString.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
func clear_draft() {
switch action {
case .replying_to(let replying_to):
damus_state.drafts.replies.removeValue(forKey: replying_to)
case .quoting(let quoting):
damus_state.drafts.quotes.removeValue(forKey: quoting)
case .posting:
damus_state.drafts.post = nil
}
}
func load_draft() {
guard let draft = load_draft_for_post(drafts: self.damus_state.drafts, action: self.action) else {
self.post = NSMutableAttributedString("")
self.uploadedMedias = []
return
}
self.uploadedMedias = draft.media
self.post = draft.content
}
func post_changed(post: NSMutableAttributedString, media: [UploadedMedia]) {
switch action {
case .replying_to(let ev):
if let draft = damus_state.drafts.replies[ev] {
draft.content = post
draft.media = media
} else {
damus_state.drafts.replies[ev] = DraftArtifacts(content: post, media: media)
}
case .quoting(let ev):
if let draft = damus_state.drafts.quotes[ev] {
draft.content = post
draft.media = media
} else {
damus_state.drafts.quotes[ev] = DraftArtifacts(content: post, media: media)
}
case .posting:
if let draft = damus_state.drafts.post {
draft.content = post
draft.media = media
} else {
damus_state.drafts.post = DraftArtifacts(content: post, media: media)
}
}
}
var TextEntry: some View { var TextEntry: some View {
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
TextViewWrapper(attributedText: $post) TextViewWrapper(attributedText: $post)
.focused($focus) .focused($focus)
.textInputAutocapitalization(.sentences) .textInputAutocapitalization(.sentences)
.onChange(of: post) { _ in .onChange(of: post) { p in
if let replying_to { post_changed(post: p, media: uploadedMedias)
damus_state.drafts.replies[replying_to] = post
} else {
damus_state.drafts.post = post
}
} }
if post.string.isEmpty { if post.string.isEmpty {
@@ -168,29 +245,20 @@ struct PostView: View {
.padding() .padding()
} }
func append_url(_ url: String) {
let uploadedImageURL = NSMutableAttributedString(string: url)
let combinedAttributedString = NSMutableAttributedString()
combinedAttributedString.append(post)
if !post.string.hasSuffix(" ") {
combinedAttributedString.append(NSAttributedString(string: " "))
}
combinedAttributedString.append(uploadedImageURL)
// make sure we have a space at the end
combinedAttributedString.append(NSAttributedString(string: " "))
post = combinedAttributedString
}
func handle_upload(media: MediaUpload) { func handle_upload(media: MediaUpload) {
let uploader = get_media_uploader(damus_state.pubkey) let uploader = get_media_uploader(damus_state.pubkey)
Task.init { Task.init {
let img = getImage(media: media)
let res = await image_upload.start(media: media, uploader: uploader) let res = await image_upload.start(media: media, uploader: uploader)
switch res { switch res {
case .success(let url): case .success(let url):
append_url(url) guard let url = URL(string: url) else {
self.error = "Error uploading image :("
return
}
let uploadedMedia = UploadedMedia(localURL: media.localURL, uploadedURL: url, representingImage: img)
uploadedMedias.append(uploadedMedia)
case .failed(let error): case .failed(let error):
if let error { if let error {
@@ -203,29 +271,50 @@ struct PostView: View {
} }
} }
var has_artifacts: Bool {
if case .quoting = action {
return true
}
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)
TextEntry
}
.frame(height: has_artifacts ? deviceSize.size.height*0.4 : deviceSize.size.height)
.id("post")
PVImageCarouselView(media: $uploadedMedias, deviceWidth: deviceSize.size.width)
.onChange(of: uploadedMedias) { media in
post_changed(post: post, media: media)
}
if case .quoting(let ev) = action {
BuilderEventView(damus: damus_state, event: ev)
}
}
.padding(.horizontal)
}
var body: some View { var body: some View {
GeometryReader { (deviceSize: GeometryProxy) in GeometryReader { (deviceSize: GeometryProxy) in
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
let searching = get_searching_string(post.string) let searching = get_searching_string(post.string)
TopBar TopBar
ScrollViewReader { scroller in ScrollViewReader { scroller in
ScrollView { ScrollView {
if let replying_to = replying_to { if case .replying_to(let replying_to) = self.action {
ReplyView(replying_to: replying_to, damus: damus_state, originalReferences: $originalReferences, references: $references) ReplyView(replying_to: replying_to, damus: damus_state, originalReferences: $originalReferences, references: $references)
} }
VStack(alignment: .leading, spacing: 0) {
HStack(alignment: .top) { Editor(deviceSize: deviceSize)
ProfilePicView(pubkey: damus_state.pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
TextEntry
}
.frame(height: deviceSize.size.height*0.78)
.id("post")
}
.padding(.horizontal)
} }
.frame(maxHeight: searching == nil ? .infinity : 70) .frame(maxHeight: searching == nil ? .infinity : 70)
.onAppear { .onAppear {
@@ -247,31 +336,41 @@ struct PostView: View {
} }
} }
.sheet(isPresented: $attach_media) { .sheet(isPresented: $attach_media) {
ImagePicker(sourceType: .photoLibrary, pubkey: damus_state.pubkey) { img in ImagePicker(sourceType: .photoLibrary, pubkey: damus_state.pubkey, image_upload_confirm: $image_upload_confirm) { img in
handle_upload(media: .image(img)) self.mediaToUpload = .image(img)
} onVideoPicked: { url in } onVideoPicked: { url in
handle_upload(media: .video(url)) 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) {
Button(NSLocalizedString("Upload", comment: "Button to proceed with uploading."), role: .none) {
if let mediaToUpload {
self.handle_upload(media: mediaToUpload)
self.attach_media = false
}
}
Button(NSLocalizedString("Cancel", comment: "Button to cancel the upload."), role: .cancel) {}
} }
} }
.sheet(isPresented: $attach_camera) { .sheet(isPresented: $attach_camera) {
ImagePicker(sourceType: .camera, pubkey: damus_state.pubkey) { img in // 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)) handle_upload(media: .image(img))
} onVideoPicked: { url in } onVideoPicked: { url in
handle_upload(media: .video(url)) handle_upload(media: .video(url))
} }
} }
.onAppear() { .onAppear() {
if let replying_to { load_draft()
references = gather_reply_ids(our_pubkey: damus_state.pubkey, from: replying_to)
switch action {
case .replying_to(let replying_to):
references = gather_reply_ids(our_pubkey: damus_state.pubkey, from: replying_to)
originalReferences = references originalReferences = references
if damus_state.drafts.replies[replying_to] == nil { case .quoting(let quoting):
damus_state.drafts.post = NSMutableAttributedString(string: "") references = gather_quote_ids(our_pubkey: damus_state.pubkey, from: quoting)
} originalReferences = references
if let p = damus_state.drafts.replies[replying_to] { case .posting:
post = p break
}
} else {
post = damus_state.drafts.post
} }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
@@ -279,10 +378,8 @@ struct PostView: View {
} }
} }
.onDisappear { .onDisappear {
if let replying_to, let reply = damus_state.drafts.replies[replying_to], reply.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { if isEmpty {
damus_state.drafts.replies.removeValue(forKey: replying_to) clear_draft()
} else if replying_to == nil && damus_state.drafts.post.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
damus_state.drafts.post = NSMutableAttributedString(string : "")
} }
} }
.alert(NSLocalizedString("Note contains \"nsec1\" private key. Are you sure?", comment: "Alert user that they might be attempting to paste a private key and ask them to confirm."), isPresented: $showPrivateKeyWarning, actions: { .alert(NSLocalizedString("Note contains \"nsec1\" private key. Are you sure?", comment: "Alert user that they might be attempting to paste a private key and ask them to confirm."), isPresented: $showPrivateKeyWarning, actions: {
@@ -320,6 +417,100 @@ func get_searching_string(_ post: String) -> String? {
struct PostView_Previews: PreviewProvider { struct PostView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
PostView(replying_to: nil, damus_state: test_damus_state()) PostView(action: .posting, damus_state: test_damus_state())
}
}
struct PVImageCarouselView: View {
@Binding var media: [UploadedMedia]
let deviceWidth: CGFloat
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(media.map({$0.representingImage}), id: \.self) { image in
ZStack(alignment: .topTrailing) {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: media.count == 1 ? deviceWidth*0.8 : 250, height: media.count == 1 ? 400 : 250)
.cornerRadius(10)
.padding()
.contextMenu {
if let uploadedURL = media.first(where: { $0.representingImage == image })?.uploadedURL {
Button(action: {
UIPasteboard.general.string = uploadedURL.absoluteString
}) {
Label(NSLocalizedString("Copy URL", comment: "Label for button in context menu to copy URL of the selected uploaded media asset."), systemImage: "doc.on.doc")
}
}
}
Image(systemName: "xmark.circle.fill")
.foregroundColor(.white)
.padding(20)
.shadow(radius: 5)
.onTapGesture {
if let index = media.map({$0.representingImage}).firstIndex(of: image) {
media.remove(at: index)
}
}
}
}
}
.padding()
}
}
}
fileprivate func getImage(media: MediaUpload) -> UIImage {
var uiimage: UIImage = UIImage()
if media.is_image {
// fetch the image data
if let data = try? Data(contentsOf: media.localURL) {
uiimage = UIImage(data: data) ?? UIImage()
}
} else {
let asset = AVURLAsset(url: media.localURL)
let generator = AVAssetImageGenerator(asset: asset)
generator.appliesPreferredTrackTransform = true
let time = CMTimeMake(value: 1, timescale: 60) // get the thumbnail image at the 1st second
do {
let cgImage = try generator.copyCGImage(at: time, actualTime: nil)
uiimage = UIImage(cgImage: cgImage)
} catch {
print("No thumbnail: \(error)")
}
// create a play icon on the top to differentiate if media upload is image or a video, gif is an image
let playIcon = UIImage(systemName: "play.fill")?.withTintColor(.white, renderingMode: .alwaysOriginal)
let size = uiimage.size
let scale = UIScreen.main.scale
UIGraphicsBeginImageContextWithOptions(size, false, scale)
uiimage.draw(at: .zero)
let playIconSize = CGSize(width: 60, height: 60)
let playIconOrigin = CGPoint(x: (size.width - playIconSize.width) / 2, y: (size.height - playIconSize.height) / 2)
playIcon?.draw(in: CGRect(origin: playIconOrigin, size: playIconSize))
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
uiimage = newImage ?? UIImage()
}
return uiimage
}
struct UploadedMedia: Equatable {
let localURL: URL
let uploadedURL: URL
let representingImage: UIImage
}
func load_draft_for_post(drafts: Drafts, action: PostAction) -> DraftArtifacts? {
switch action {
case .replying_to(let ev):
return drafts.replies[ev]
case .quoting(let ev):
return drafts.quotes[ev]
case .posting:
return drafts.post
} }
} }
@@ -63,6 +63,7 @@ struct EditMetadataView: View {
@State var name: String @State var name: String
@State var ln: String @State var ln: String
@State var website: String @State var website: String
let profile: Profile?
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@@ -73,6 +74,7 @@ struct EditMetadataView: View {
init (damus_state: DamusState) { init (damus_state: DamusState) {
self.damus_state = damus_state self.damus_state = damus_state
let data = damus_state.profiles.lookup(id: damus_state.pubkey) let data = damus_state.profiles.lookup(id: damus_state.pubkey)
self.profile = data
_name = State(initialValue: data?.name ?? "") _name = State(initialValue: data?.name ?? "")
_display_name = State(initialValue: data?.display_name ?? "") _display_name = State(initialValue: data?.display_name ?? "")
@@ -85,27 +87,31 @@ struct EditMetadataView: View {
} }
func imageBorderColor() -> Color { func imageBorderColor() -> Color {
colorScheme == .light ? DamusColors.white : DamusColors.black colorScheme == .light ? DamusColors.white : DamusColors.black
} }
func to_profile() -> Profile {
let profile = self.profile ?? Profile()
profile.name = name
profile.display_name = display_name
profile.about = about
profile.website = website
profile.nip05 = nip05.isEmpty ? nil : nip05
profile.picture = picture.isEmpty ? nil : picture
profile.banner = banner.isEmpty ? nil : banner
profile.lud06 = ln.contains("@") ? nil : ln
profile.lud16 = ln.contains("@") ? ln : nil
return profile
}
func save() { func save() {
let metadata = NostrMetadata( let profile = to_profile()
display_name: display_name, guard let metadata_ev = make_metadata_event(keypair: damus_state.keypair, metadata: profile) else {
name: name, return
about: about,
website: website,
nip05: nip05.isEmpty ? nil : nip05,
picture: picture.isEmpty ? nil : picture,
banner: banner.isEmpty ? nil : banner,
lud06: ln.contains("@") ? nil : ln,
lud16: ln.contains("@") ? ln : nil
);
let m_metadata_ev = make_metadata_event(keypair: damus_state.keypair, metadata: metadata)
if let metadata_ev = m_metadata_ev {
damus_state.postbox.send(metadata_ev)
} }
damus_state.postbox.send(metadata_ev)
} }
func is_ln_valid(ln: String) -> Bool { func is_ln_valid(ln: String) -> Bool {
@@ -18,7 +18,8 @@ struct EditProfilePictureControl: View {
@State private var show_camera = false @State private var show_camera = false
@State private var show_library = false @State private var show_library = false
@State var image_upload_confirm: Bool = false
var body: some View { var body: some View {
Menu { Menu {
Button(action: { Button(action: {
@@ -44,14 +45,16 @@ struct EditProfilePictureControl: View {
} }
} }
.sheet(isPresented: $show_camera) { .sheet(isPresented: $show_camera) {
ImagePicker(sourceType: .camera, pubkey: pubkey, imagesOnly: true) { img in // 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)) handle_upload(media: .image(img))
} onVideoPicked: { url in } onVideoPicked: { url in
print("Cannot upload videos as profile image") print("Cannot upload videos as profile image")
} }
} }
.sheet(isPresented: $show_library) { .sheet(isPresented: $show_library) {
ImagePicker(sourceType: .photoLibrary, pubkey: pubkey, imagesOnly: true) { img in // 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)) handle_upload(media: .image(img))
} onVideoPicked: { url in } onVideoPicked: { url in
print("Cannot upload videos as profile image") print("Cannot upload videos as profile image")
+18 -7
View File
@@ -39,8 +39,8 @@ struct EventProfileName: View {
self.size = size self.size = size
} }
var friend_icon: String? { var friend_type: FriendType? {
return get_friend_icon(contacts: damus_state.contacts, pubkey: pubkey, show_confirmed: show_friend_confirmed) return get_friend_type(contacts: damus_state.contacts, pubkey: self.pubkey)
} }
var current_nip05: NIP05? { var current_nip05: NIP05? {
@@ -50,7 +50,15 @@ struct EventProfileName: View {
var current_display_name: DisplayName { var current_display_name: DisplayName {
return display_name ?? Profile.displayName(profile: profile, pubkey: pubkey) return display_name ?? Profile.displayName(profile: profile, pubkey: pubkey)
} }
var onlyzapper: Bool {
guard let profile else {
return false
}
return profile.reactions == false
}
var body: some View { var body: some View {
HStack(spacing: 2) { HStack(spacing: 2) {
switch current_display_name { switch current_display_name {
@@ -71,10 +79,13 @@ struct EventProfileName: View {
NIP05Badge(nip05: nip05, pubkey: pubkey, contacts: damus_state.contacts, show_domain: false, clickable: false) NIP05Badge(nip05: nip05, pubkey: pubkey, contacts: damus_state.contacts, show_domain: false, clickable: false)
} }
if let frend = friend_icon, current_nip05 == nil { if current_nip05 == nil, let frend = friend_type {
Label("", systemImage: frend) FriendIcon(friend: frend)
.foregroundColor(.gray) }
.font(.footnote)
if onlyzapper {
Image("zap-hashtag")
.frame(width: 14, height: 14)
} }
} }
.onReceive(handle_notify(.profile_updated)) { notif in .onReceive(handle_notify(.profile_updated)) { notif in
+39
View File
@@ -0,0 +1,39 @@
//
// FriendIcon.swift
// damus
//
// Created by William Casarin on 2023-04-20.
//
import SwiftUI
struct FriendIcon: View {
let friend: FriendType
var body: some View {
Group {
switch friend {
case .friend:
LINEAR_GRADIENT
.mask(Image(systemName: "person.fill.checkmark")
.resizable()
).frame(width: 20, height: 14)
case .fof:
Image(systemName: "person.fill.and.arrow.left.and.arrow.right")
.resizable()
.frame(width: 21, height: 14)
.foregroundColor(.gray)
}
}
}
}
struct FriendIcon_Previews: PreviewProvider {
static var previews: some View {
VStack {
FriendIcon(friend: .friend)
FriendIcon(friend: .fof)
}
}
}
+8 -5
View File
@@ -11,17 +11,20 @@ struct MaybeAnonPfpView: View {
let state: DamusState let state: DamusState
let is_anon: Bool let is_anon: Bool
let pubkey: String let pubkey: String
let size: CGFloat
init(state: DamusState, event: NostrEvent, pubkey: String) { init(state: DamusState, event: NostrEvent, pubkey: String, size: CGFloat) {
self.state = state self.state = state
self.is_anon = event_is_anonymous(ev: event) self.is_anon = event_is_anonymous(ev: event)
self.pubkey = pubkey self.pubkey = pubkey
self.size = size
} }
init(state: DamusState, is_anon: Bool, pubkey: String) { init(state: DamusState, is_anon: Bool, pubkey: String, size: CGFloat) {
self.state = state self.state = state
self.is_anon = is_anon self.is_anon = is_anon
self.pubkey = pubkey self.pubkey = pubkey
self.size = size
} }
var body: some View { var body: some View {
@@ -29,10 +32,10 @@ struct MaybeAnonPfpView: View {
if is_anon { if is_anon {
Image(systemName: "person.fill.questionmark") Image(systemName: "person.fill.questionmark")
.font(.largeTitle) .font(.largeTitle)
.frame(width: PFP_SIZE, height: PFP_SIZE) .frame(width: size, height: size)
} else { } else {
NavigationLink(destination: ProfileView(damus_state: state, pubkey: pubkey)) { NavigationLink(destination: ProfileView(damus_state: state, pubkey: pubkey)) {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: state.profiles) ProfilePicView(pubkey: pubkey, size: size, highlight: .none, profiles: state.profiles)
} }
} }
} }
@@ -41,6 +44,6 @@ struct MaybeAnonPfpView: View {
struct MaybeAnonPfpView_Previews: PreviewProvider { struct MaybeAnonPfpView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
MaybeAnonPfpView(state: test_damus_state(), is_anon: true, pubkey: "anon") MaybeAnonPfpView(state: test_damus_state(), is_anon: true, pubkey: "anon", size: PFP_SIZE)
} }
} }
+24 -12
View File
@@ -7,17 +7,18 @@
import SwiftUI import SwiftUI
func get_friend_icon(contacts: Contacts, pubkey: String, show_confirmed: Bool) -> String? { enum FriendType {
if !show_confirmed { case friend
return nil case fof
} }
func get_friend_type(contacts: Contacts, pubkey: String) -> FriendType? {
if contacts.is_friend_or_self(pubkey) { if contacts.is_friend_or_self(pubkey) {
return "person.fill.checkmark" return .friend
} }
if contacts.is_friend_of_friend(pubkey) { if contacts.is_friend_of_friend(pubkey) {
return "person.fill.and.arrow.left.and.arrow.right" return .fof
} }
return nil return nil
@@ -53,8 +54,8 @@ struct ProfileName: View {
self.show_nip5_domain = show_nip5_domain self.show_nip5_domain = show_nip5_domain
} }
var friend_icon: String? { var friend_type: FriendType? {
return get_friend_icon(contacts: damus_state.contacts, pubkey: pubkey, show_confirmed: show_friend_confirmed) return get_friend_type(contacts: damus_state.contacts, pubkey: self.pubkey)
} }
var current_nip05: NIP05? { var current_nip05: NIP05? {
@@ -69,6 +70,14 @@ struct ProfileName: View {
return prefix == "@" ? current_display_name.username : current_display_name.display_name return prefix == "@" ? current_display_name.username : current_display_name.display_name
} }
var onlyzapper: Bool {
guard let profile else {
return false
}
return profile.reactions == false
}
var body: some View { var body: some View {
HStack(spacing: 2) { HStack(spacing: 2) {
Text(verbatim: "\(prefix)\(name_choice)") Text(verbatim: "\(prefix)\(name_choice)")
@@ -77,9 +86,12 @@ struct ProfileName: View {
if let nip05 = current_nip05 { if let nip05 = current_nip05 {
NIP05Badge(nip05: nip05, pubkey: pubkey, contacts: damus_state.contacts, show_domain: show_nip5_domain, clickable: true) NIP05Badge(nip05: nip05, pubkey: pubkey, contacts: damus_state.contacts, show_domain: show_nip5_domain, clickable: true)
} }
if let friend = friend_icon, current_nip05 == nil { if let friend = friend_type, current_nip05 == nil {
Image(systemName: friend) FriendIcon(friend: friend)
.foregroundColor(.gray) }
if onlyzapper {
Image("zap-hashtag")
.frame(width: 14, height: 14)
} }
} }
.onReceive(handle_notify(.profile_updated)) { notif in .onReceive(handle_notify(.profile_updated)) { notif in
+28 -8
View File
@@ -119,6 +119,7 @@ struct ProfileView: View {
@State var showing_select_wallet: Bool = false @State var showing_select_wallet: Bool = false
@State var is_zoomed: Bool = false @State var is_zoomed: Bool = false
@State var show_share_sheet: Bool = false @State var show_share_sheet: Bool = false
@State var show_qr_code: Bool = false
@State var action_sheet_presented: Bool = false @State var action_sheet_presented: Bool = false
@State var filter_state : FilterState = .posts @State var filter_state : FilterState = .posts
@State var yOffset: CGFloat = 0 @State var yOffset: CGFloat = 0
@@ -213,6 +214,10 @@ struct ProfileView: View {
Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) { Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) {
show_share_sheet = true show_share_sheet = true
} }
Button(NSLocalizedString("QR Code", comment: "Button to view profile's qr code.")) {
show_qr_code = true
}
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile. // Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
if profile.pubkey != damus_state.pubkey && damus_state.is_privkey_user { if profile.pubkey != damus_state.pubkey && damus_state.is_privkey_user {
@@ -240,20 +245,33 @@ struct ProfileView: View {
} }
func lnButton(lnurl: String, profile: Profile) -> some View { func lnButton(lnurl: String, profile: Profile) -> some View {
Button(action: { let button_img = profile.reactions == false ? "bolt.brakesignal" : "bolt.circle"
return Button(action: {
if damus_state.settings.show_wallet_selector { if damus_state.settings.show_wallet_selector {
showing_select_wallet = true showing_select_wallet = true
} else { } else {
open_with_wallet(wallet: damus_state.settings.default_wallet.model, invoice: lnurl) open_with_wallet(wallet: damus_state.settings.default_wallet.model, invoice: lnurl)
} }
}) { }) {
Image(systemName: "bolt.circle") Image(systemName: button_img)
.profile_button_style(scheme: colorScheme) .profile_button_style(scheme: colorScheme)
.contextMenu { .contextMenu {
Button { if profile.reactions == false {
UIPasteboard.general.string = profile.lnurl ?? "" Text("OnlyZaps Enabled")
} label: { }
Label(NSLocalizedString("Copy LNURL", comment: "Context menu option for copying a user's Lightning URL."), systemImage: "doc.on.doc")
if let addr = profile.lud16 {
Button {
UIPasteboard.general.string = addr
} label: {
Label(addr, systemImage: "doc.on.doc")
}
} else if let lnurl = profile.lnurl {
Button {
UIPasteboard.general.string = lnurl
} label: {
Label(NSLocalizedString("Copy LNURL", comment: "Context menu option for copying a user's Lightning URL."), systemImage: "doc.on.doc")
}
} }
} }
@@ -266,8 +284,7 @@ struct ProfileView: View {
var dmButton: some View { var dmButton: some View {
let dm_model = damus_state.dms.lookup_or_create(profile.pubkey) let dm_model = damus_state.dms.lookup_or_create(profile.pubkey)
let dmview = DMChatView(damus_state: damus_state, pubkey: profile.pubkey) let dmview = DMChatView(damus_state: damus_state, dms: dm_model)
.environmentObject(dm_model)
return NavigationLink(destination: dmview) { return NavigationLink(destination: dmview) {
Image(systemName: "bubble.left.circle") Image(systemName: "bubble.left.circle")
.profile_button_style(scheme: colorScheme) .profile_button_style(scheme: colorScheme)
@@ -465,6 +482,9 @@ struct ProfileView: View {
} }
} }
} }
.fullScreenCover(isPresented: $show_qr_code) {
QRCodeView(damus_state: damus_state, pubkey: profile.pubkey)
}
} }
} }
+7 -5
View File
@@ -10,12 +10,13 @@ import CoreImage.CIFilterBuiltins
struct QRCodeView: View { struct QRCodeView: View {
let damus_state: DamusState let damus_state: DamusState
@State var pubkey: String
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
@Environment(\.presentationMode) var presentationMode @Environment(\.presentationMode) var presentationMode
var maybe_key: String? { var maybe_key: String? {
guard let key = bech32_pubkey(damus_state.pubkey) else { guard let key = bech32_pubkey(pubkey) else {
return nil return nil
} }
@@ -39,10 +40,11 @@ struct QRCodeView: View {
} }
VStack(alignment: .center) { VStack(alignment: .center) {
let profile = damus_state.profiles.lookup(id: damus_state.pubkey)
if (damus_state.profiles.lookup(id: damus_state.pubkey)?.picture) != nil { let profile = damus_state.profiles.lookup(id: pubkey)
ProfilePicView(pubkey: damus_state.pubkey, size: 90.0, highlight: .custom(DamusColors.white, 4.0), profiles: damus_state.profiles)
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)
.padding(.top, 50) .padding(.top, 50)
} else { } else {
Image(systemName: "person.fill") Image(systemName: "person.fill")
@@ -119,6 +121,6 @@ struct QRCodeView: View {
struct QRCodeView_Previews: PreviewProvider { struct QRCodeView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
QRCodeView(damus_state: test_damus_state()) QRCodeView(damus_state: test_damus_state(), pubkey: test_event.pubkey)
} }
} }
+1 -1
View File
@@ -70,7 +70,7 @@ struct RelayDetailView: View {
if let pubkey = nip11.pubkey { if let pubkey = nip11.pubkey {
Section(NSLocalizedString("Admin", comment: "Label to display relay contact user.")) { Section(NSLocalizedString("Admin", comment: "Label to display relay contact user.")) {
UserView(damus_state: state, pubkey: pubkey) UserViewRow(damus_state: state, pubkey: pubkey)
} }
} }
Section(NSLocalizedString("Relay", comment: "Label to display relay address.")) { Section(NSLocalizedString("Relay", comment: "Label to display relay address.")) {
+34
View File
@@ -0,0 +1,34 @@
//
// SignalView.swift
// damus
//
// Created by William Casarin on 2023-04-14.
//
import SwiftUI
struct SignalView: View {
let state: DamusState
@ObservedObject var signal: SignalModel
var body: some View {
Group {
if signal.signal != signal.max_signal {
NavigationLink(destination: RelayConfigView(state: state)) {
Text("\(signal.signal)/\(signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
.font(.callout)
.foregroundColor(.gray)
}
} else {
Text("")
}
}
}
}
struct SignalView_Previews: PreviewProvider {
static var previews: some View {
SignalView(state: test_damus_state(), signal: SignalModel(signal: 5, max_signal: 10))
}
}
+4
View File
@@ -217,3 +217,7 @@ struct SaveKeysView_Previews: PreviewProvider {
SaveKeysView(account: model) SaveKeysView(account: model)
} }
} }
func create_account_to_metadata(_ model: CreateAccountModel) -> Profile {
return Profile(name: model.nick_name, display_name: model.real_name, about: model.about, picture: model.profile_image, banner: nil, website: nil, lud06: nil, lud16: nil, nip05: nil)
}
+1 -1
View File
@@ -50,7 +50,7 @@ struct SearchHomeView: View {
damus: damus_state, damus: damus_state,
show_friend_icon: true, show_friend_icon: true,
filter: { filter: {
if damus_state.muted_threads.isMutedThread($0) { if damus_state.muted_threads.isMutedThread($0, privkey: self.damus_state.keypair.privkey) {
return false return false
} }
@@ -50,7 +50,7 @@ struct AppearanceSettingsView: View {
} }
.navigationTitle("Appearance") .navigationTitle(NSLocalizedString("Appearance", comment: "Navigation title for text and appearance settings."))
.onReceive(handle_notify(.switched_timeline)) { _ in .onReceive(handle_notify(.switched_timeline)) { _ in
dismiss() dismiss()
} }
+1 -1
View File
@@ -100,7 +100,7 @@ struct KeySettingsView: View {
} }
} }
.navigationTitle("Keys") .navigationTitle(NSLocalizedString("Keys", comment: "Navigation title for managing keys."))
.onReceive(handle_notify(.switched_timeline)) { _ in .onReceive(handle_notify(.switched_timeline)) { _ in
dismiss() dismiss()
} }
@@ -76,7 +76,7 @@ struct TranslationSettingsView: View {
} }
} }
} }
.navigationTitle("Translation") .navigationTitle(NSLocalizedString("Translation", comment: "Navigation title for translation settings."))
.onReceive(handle_notify(.switched_timeline)) { _ in .onReceive(handle_notify(.switched_timeline)) { _ in
dismiss() dismiss()
} }
+23 -8
View File
@@ -14,17 +14,29 @@ struct ZapSettingsView: View {
@State var default_zap_amount: String @State var default_zap_amount: String
@Environment(\.dismiss) var dismiss @Environment(\.dismiss) var dismiss
init(pubkey: String, settings: UserSettingsStore) { init(pubkey: String, settings: UserSettingsStore) {
self.pubkey = pubkey self.pubkey = pubkey
let zap_amt = get_default_zap_amount(pubkey: pubkey).map({ "\($0)" }) ?? "1000" let zap_amt = get_default_zap_amount(pubkey: pubkey).formatted()
_default_zap_amount = State(initialValue: zap_amt) _default_zap_amount = State(initialValue: zap_amt)
self._settings = ObservedObject(initialValue: settings) self._settings = ObservedObject(initialValue: settings)
} }
var body: some View { var body: some View {
Form { Form {
Section("Wallet") { 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"))
) {
Toggle(NSLocalizedString("Enable 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)
}
}
Section(NSLocalizedString("Wallet", comment: "Title for section in zap settings that controls the Lightning wallet selection.")) {
Toggle(NSLocalizedString("Show wallet selector", comment: "Toggle to show or hide selection of wallet."), isOn: $settings.show_wallet_selector).toggleStyle(.switch) Toggle(NSLocalizedString("Show wallet selector", comment: "Toggle to show or hide selection of wallet."), isOn: $settings.show_wallet_selector).toggleStyle(.switch)
Picker(NSLocalizedString("Select default wallet", comment: "Prompt selection of user's default wallet"), Picker(NSLocalizedString("Select default wallet", comment: "Prompt selection of user's default wallet"),
@@ -36,23 +48,26 @@ struct ZapSettingsView: View {
} }
} }
Section("Zaps") { Section(NSLocalizedString("Zaps", comment: "Title for section in zap settings that controls general zap preferences.")) {
Toggle(NSLocalizedString("Zap Vibration", comment: "Setting to enable vibration on zap"), isOn: $settings.zap_vibration) Toggle(NSLocalizedString("Zap Vibration", comment: "Setting to enable vibration on zap"), isOn: $settings.zap_vibration)
.toggleStyle(.switch) .toggleStyle(.switch)
} }
Section("Default Zap Amount in sats") { Section(NSLocalizedString("Default Zap Amount in sats", comment: "Title for section in zap settings that controls the default zap amount in sats.")) {
TextField(String("1000"), text: $default_zap_amount) TextField(fallback_zap_amount.formatted(), text: $default_zap_amount)
.keyboardType(.numberPad) .keyboardType(.numberPad)
.onReceive(Just(default_zap_amount)) { newValue in .onReceive(Just(default_zap_amount)) { newValue in
if let parsed = handle_string_amount(new_value: newValue) { if let parsed = handle_string_amount(new_value: newValue) {
self.default_zap_amount = String(parsed) self.default_zap_amount = parsed.formatted()
set_default_zap_amount(pubkey: self.pubkey, amount: parsed) set_default_zap_amount(pubkey: self.pubkey, amount: parsed)
} else {
self.default_zap_amount = ""
set_default_zap_amount(pubkey: self.pubkey, amount: 0)
} }
} }
} }
} }
.navigationTitle("Zaps") .navigationTitle(NSLocalizedString("Zaps", comment: "Navigation title for zap settings."))
.onReceive(handle_notify(.switched_timeline)) { _ in .onReceive(handle_notify(.switched_timeline)) { _ in
dismiss() dismiss()
} }
+1 -1
View File
@@ -142,7 +142,7 @@ struct SideMenuView: View {
.font(.title) .font(.title)
.foregroundColor(textColor()) .foregroundColor(textColor())
}).fullScreenCover(isPresented: $showQRCode) { }).fullScreenCover(isPresented: $showQRCode) {
QRCodeView(damus_state: damus_state) QRCodeView(damus_state: damus_state, pubkey: damus_state.pubkey)
} }
} }
.padding(.top, verticalSpacing) .padding(.top, verticalSpacing)
-3
View File
@@ -29,7 +29,6 @@ struct ThreadView: View {
ForEach(parent_events, id: \.id) { parent_event in ForEach(parent_events, id: \.id) { parent_event in
MutedEventView(damus_state: state, MutedEventView(damus_state: state,
event: parent_event, event: parent_event,
scroller: reader,
selected: false) selected: false)
.padding(.horizontal) .padding(.horizontal)
.onTapGesture { .onTapGesture {
@@ -56,7 +55,6 @@ struct ThreadView: View {
MutedEventView( MutedEventView(
damus_state: state, damus_state: state,
event: self.thread.event, event: self.thread.event,
scroller: reader,
selected: true selected: true
) )
.id(self.thread.event.id) .id(self.thread.event.id)
@@ -65,7 +63,6 @@ struct ThreadView: View {
MutedEventView( MutedEventView(
damus_state: state, damus_state: state,
event: child_event, event: child_event,
scroller: nil,
selected: false selected: false
) )
.padding(.horizontal) .padding(.horizontal)
+8 -4
View File
@@ -25,7 +25,7 @@ struct ZapAmountItem: Identifiable, Hashable {
} }
func get_default_zap_amount_item(_ pubkey: String) -> ZapAmountItem { func get_default_zap_amount_item(_ pubkey: String) -> ZapAmountItem {
let def = get_default_zap_amount(pubkey: pubkey) ?? 1000 let def = get_default_zap_amount(pubkey: pubkey)
return ZapAmountItem(amount: def, icon: "🤙") return ZapAmountItem(amount: def, icon: "🤙")
} }
@@ -181,13 +181,17 @@ struct CustomizeZapView: View {
}) })
Section(content: { Section(content: {
TextField(String("100000"), text: $custom_amount) // 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) .keyboardType(.numberPad)
.onReceive(Just(custom_amount)) { newValue in .onReceive(Just(custom_amount)) { newValue in
if let parsed = handle_string_amount(new_value: newValue) { if let parsed = handle_string_amount(new_value: newValue) {
self.custom_amount = String(parsed) self.custom_amount = parsed.formatted()
self.custom_amount_sats = parsed self.custom_amount_sats = parsed
} else {
self.custom_amount = ""
self.custom_amount_sats = nil
} }
} }
}, header: { }, header: {
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -39,7 +39,7 @@
<key>many</key> <key>many</key>
<string>Followers</string> <string>Followers</string>
<key>other</key> <key>other</key>
<string>Sledují</string> <string>Sledují</string>
</dict> </dict>
</dict> </dict>
<key>following_count</key> <key>following_count</key>
+10
View File
@@ -9,6 +9,16 @@
<string>applinks:damus.io</string> <string>applinks:damus.io</string>
<string>webcredentials:damus.io</string> <string>webcredentials:damus.io</string>
</array> </array>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.personal-information.photos-library</key>
<true/>
<key>keychain-access-groups</key> <key>keychain-access-groups</key>
<array> <array>
<string>$(AppIdentifierPrefix)com.jb55.damus2</string> <string>$(AppIdentifierPrefix)com.jb55.damus2</string>
+7
View File
@@ -55,6 +55,13 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
// Display the notification in the foreground // Display the notification in the foreground
completionHandler([.banner, .list, .sound, .badge]) completionHandler([.banner, .list, .sound, .badge])
} }
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
let notification = LossyLocalNotification.from_user_info(user_info: userInfo)
notify(.local_notification, notification)
completionHandler()
}
} }
func needs_setup() -> Keypair? { func needs_setup() -> Keypair? {
Binary file not shown.
Binary file not shown.
+6 -6
View File
@@ -61,9 +61,9 @@
<key>NSStringFormatValueTypeKey</key> <key>NSStringFormatValueTypeKey</key>
<string>d</string> <string>d</string>
<key>one</key> <key>one</key>
<string>%2$@ und %1$d andere*r reagierten auf einen Beitrag in dem Du markiert warst</string> <string>%2$@ und %1$d andere*r reagierten auf einen Beitrag in dem du markiert warst</string>
<key>other</key> <key>other</key>
<string>%2$@ und %1$d andere reagierten auf einen Beitrag in dem Du markiert warst</string> <string>%2$@ und %1$d andere reagierten auf einen Beitrag in dem du markiert warst</string>
</dict> </dict>
</dict> </dict>
<key>reacted_your_post_3</key> <key>reacted_your_post_3</key>
@@ -157,9 +157,9 @@
<key>NSStringFormatValueTypeKey</key> <key>NSStringFormatValueTypeKey</key>
<string>d</string> <string>d</string>
<key>one</key> <key>one</key>
<string>%2$@ und %1$d andere*r teilten einen Beitrag in dem Du markiert warst</string> <string>%2$@ und %1$d andere*r teilten einen Beitrag in dem du markiert warst</string>
<key>other</key> <key>other</key>
<string>%2$@ und %1$d andere teilten ein Beitrag in dem Du markiert warst</string> <string>%2$@ und %1$d andere teilten ein Beitrag in dem du markiert warst</string>
</dict> </dict>
</dict> </dict>
<key>reposted_your_post_3</key> <key>reposted_your_post_3</key>
@@ -269,9 +269,9 @@
<key>NSStringFormatValueTypeKey</key> <key>NSStringFormatValueTypeKey</key>
<string>d</string> <string>d</string>
<key>one</key> <key>one</key>
<string>%2$@ und %1$d andere:r zappten einen Beitrag in dem Du markiert warst</string> <string>%2$@ und %1$d andere:r zappten einen Beitrag in dem du markiert warst</string>
<key>other</key> <key>other</key>
<string>%2$@ und %1$d andere zappten einen Beitrag in dem Du markiert warst</string> <string>%2$@ und %1$d andere zappten einen Beitrag in dem du markiert warst</string>
</dict> </dict>
</dict> </dict>
<key>zapped_your_post_3</key> <key>zapped_your_post_3</key>
Binary file not shown.
+4 -4
View File
@@ -47,7 +47,7 @@
<key>one</key> <key>one</key>
<string>Ακόλουθος</string> <string>Ακόλουθος</string>
<key>other</key> <key>other</key>
<string>Ακολουθείτε</string> <string>Ακολουθεί</string>
</dict> </dict>
</dict> </dict>
<key>reacted_tagged_in_3</key> <key>reacted_tagged_in_3</key>
@@ -127,7 +127,7 @@
<key>one</key> <key>one</key>
<string>Διακομιστής Relay</string> <string>Διακομιστής Relay</string>
<key>other</key> <key>other</key>
<string>Διακομιστές Relays</string> <string>Relays</string>
</dict> </dict>
</dict> </dict>
<key>replying_to_two_and_others</key> <key>replying_to_two_and_others</key>
@@ -141,9 +141,9 @@
<key>NSStringFormatValueTypeKey</key> <key>NSStringFormatValueTypeKey</key>
<string>d</string> <string>d</string>
<key>one</key> <key>one</key>
<string>Απάντηση προς %2$@, %3$@ &amp; %1$d άλλον</string> <string>Απάντηση προς %2$@, %3$@ &amp; %1$d ακόμα</string>
<key>other</key> <key>other</key>
<string>Απάντηση προς %2$@, %3$@ &amp; %1$d άλλους</string> <string>Απάντηση προς %2$@, %3$@ &amp; %1$d ακόμα</string>
</dict> </dict>
</dict> </dict>
<key>reposted_tagged_in_3</key> <key>reposted_tagged_in_3</key>
@@ -45,7 +45,7 @@
<trans-unit id="%@" xml:space="preserve"> <trans-unit id="%@" xml:space="preserve">
<source>%@</source> <source>%@</source>
<target>%@</target> <target>%@</target>
<note>No comment provided by engineer.</note> <note>DM by heading in local notification</note>
</trans-unit> </trans-unit>
<trans-unit id="%@ %@" xml:space="preserve"> <trans-unit id="%@ %@" xml:space="preserve">
<source>%@ %@</source> <source>%@ %@</source>
@@ -188,12 +188,18 @@ Sentence composed of 2 variables to describe how many people are following a use
<trans-unit id="Appearance" xml:space="preserve"> <trans-unit id="Appearance" xml:space="preserve">
<source>Appearance</source> <source>Appearance</source>
<target>Appearance</target> <target>Appearance</target>
<note>Section header for text and appearance settings</note> <note>Navigation title for text and appearance settings.
Section header for text and appearance settings</note>
</trans-unit> </trans-unit>
<trans-unit id="Are you sure you want to repost this?" xml:space="preserve"> <trans-unit id="Are you lost?" xml:space="preserve">
<source>Are you sure you want to repost this?</source> <source>Are you lost?</source>
<target>Are you sure you want to repost this?</target> <target>Are you lost?</target>
<note>Alert message to ask if user wants to repost a post.</note> <note>Text asking the user if they are lost in the app.</note>
</trans-unit>
<trans-unit id="Are you sure you want to upload this image?" xml:space="preserve">
<source>Are you sure you want to upload this image?</source>
<target>Are you sure you want to upload this image?</target>
<note>Alert message asking if the user wants to upload an image.</note>
</trans-unit> </trans-unit>
<trans-unit id="Automatically translate notes" xml:space="preserve"> <trans-unit id="Automatically translate notes" xml:space="preserve">
<source>Automatically translate notes</source> <source>Automatically translate notes</source>
@@ -244,8 +250,8 @@ Sentence composed of 2 variables to describe how many people are following a use
Button to cancel a repost. Button to cancel a repost.
Button to cancel out of alert that creates a new mutelist. Button to cancel out of alert that creates a new mutelist.
Button to cancel out of posting a note. Button to cancel out of posting a note.
Button to cancel out of reposting a post.
Button to cancel out of view adding user inputted relay. Button to cancel out of view adding user inputted relay.
Button to cancel the upload.
Cancel deleting the user. Cancel deleting the user.
Cancel out of logging out the user.</note> Cancel out of logging out the user.</note>
</trans-unit> </trans-unit>
@@ -349,6 +355,11 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>Copy Text</target> <target>Copy Text</target>
<note>Context menu option for copying the text from an note.</note> <note>Context menu option for copying the text from an note.</note>
</trans-unit> </trans-unit>
<trans-unit id="Copy URL" xml:space="preserve">
<source>Copy URL</source>
<target>Copy URL</target>
<note>Label for button in context menu to copy URL of the selected uploaded media asset.</note>
</trans-unit>
<trans-unit id="Copy User Pubkey" xml:space="preserve"> <trans-unit id="Copy User Pubkey" xml:space="preserve">
<source>Copy User Pubkey</source> <source>Copy User Pubkey</source>
<target>Copy User Pubkey</target> <target>Copy User Pubkey</target>
@@ -399,11 +410,6 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>Custom Zap Amount</target> <target>Custom Zap Amount</target>
<note>Header text to indicate that the text field below it is to enter a custom zap amount.</note> <note>Header text to indicate that the text field below it is to enter a custom zap amount.</note>
</trans-unit> </trans-unit>
<trans-unit id="DM by %@" xml:space="preserve">
<source>DM by %@</source>
<target>DM by %@</target>
<note>DM by heading in local notification</note>
</trans-unit>
<trans-unit id="DMs" xml:space="preserve"> <trans-unit id="DMs" xml:space="preserve">
<source>DMs</source> <source>DMs</source>
<target>DMs</target> <target>DMs</target>
@@ -430,7 +436,7 @@ Sentence composed of 2 variables to describe how many people are following a use
<trans-unit id="Default Zap Amount in sats" xml:space="preserve"> <trans-unit id="Default Zap Amount in sats" xml:space="preserve">
<source>Default Zap Amount in sats</source> <source>Default Zap Amount in sats</source>
<target>Default Zap Amount in sats</target> <target>Default Zap Amount in sats</target>
<note>No comment provided by engineer.</note> <note>Title for section in zap settings that controls the default zap amount in sats.</note>
</trans-unit> </trans-unit>
<trans-unit id="Delete" xml:space="preserve"> <trans-unit id="Delete" xml:space="preserve">
<source>Delete</source> <source>Delete</source>
@@ -622,7 +628,8 @@ Sentence composed of 2 variables to describe how many people are following a use
<trans-unit id="Keys" xml:space="preserve"> <trans-unit id="Keys" xml:space="preserve">
<source>Keys</source> <source>Keys</source>
<target>Keys</target> <target>Keys</target>
<note>Settings section for managing keys</note> <note>Navigation title for managing keys.
Settings section for managing keys</note>
</trans-unit> </trans-unit>
<trans-unit id="Left Handed" xml:space="preserve"> <trans-unit id="Left Handed" xml:space="preserve">
<source>Left Handed</source> <source>Left Handed</source>
@@ -721,8 +728,7 @@ Sentence composed of 2 variables to describe how many people are following a use
<source>Mute</source> <source>Mute</source>
<target>Mute</target> <target>Mute</target>
<note>Alert button to mute a user. <note>Alert button to mute a user.
Button to mute a profile. Button to mute a profile.</note>
Context menu option for muting users.</note>
</trans-unit> </trans-unit>
<trans-unit id="Mute %@?" xml:space="preserve"> <trans-unit id="Mute %@?" xml:space="preserve">
<source>Mute %@?</source> <source>Mute %@?</source>
@@ -732,7 +738,13 @@ Sentence composed of 2 variables to describe how many people are following a use
<trans-unit id="Mute User" xml:space="preserve"> <trans-unit id="Mute User" xml:space="preserve">
<source>Mute User</source> <source>Mute User</source>
<target>Mute User</target> <target>Mute User</target>
<note>Title of alert for muting a user.</note> <note>Context menu option for muting users.
Title of alert for muting a user.</note>
</trans-unit>
<trans-unit id="Mute conversation" xml:space="preserve">
<source>Mute conversation</source>
<target>Mute conversation</target>
<note>Context menu option for muting a conversation.</note>
</trans-unit> </trans-unit>
<trans-unit id="Muted" xml:space="preserve"> <trans-unit id="Muted" xml:space="preserve">
<source>Muted</source> <source>Muted</source>
@@ -749,6 +761,11 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>NIP-05 Verification</target> <target>NIP-05 Verification</target>
<note>Label for NIP-05 Verification section of user profile form.</note> <note>Label for NIP-05 Verification section of user profile form.</note>
</trans-unit> </trans-unit>
<trans-unit id="New encrypted direct message" xml:space="preserve">
<source>New encrypted direct message</source>
<target>New encrypted direct message</target>
<note>Notification that the user has received a new direct message</note>
</trans-unit>
<trans-unit id="No" xml:space="preserve"> <trans-unit id="No" xml:space="preserve">
<source>No</source> <source>No</source>
<target>No</target> <target>No</target>
@@ -779,6 +796,11 @@ Sentence composed of 2 variables to describe how many people are following a use
<target>Nothing to see here. Check back later!</target> <target>Nothing to see here. Check back later!</target>
<note>Indicates that there are no notes in the timeline to view.</note> <note>Indicates that there are no notes in the timeline to view.</note>
</trans-unit> </trans-unit>
<trans-unit id="Notification Dots" xml:space="preserve">
<source>Notification Dots</source>
<target>Notification Dots</target>
<note>Section header for notification indicator dot settings</note>
</trans-unit>
<trans-unit id="Notification Preference" xml:space="preserve"> <trans-unit id="Notification Preference" xml:space="preserve">
<source>Notification Preference</source> <source>Notification Preference</source>
<target>Notification Preference</target> <target>Notification Preference</target>
@@ -787,7 +809,8 @@ Sentence composed of 2 variables to describe how many people are following a use
<trans-unit id="Notifications" xml:space="preserve"> <trans-unit id="Notifications" xml:space="preserve">
<source>Notifications</source> <source>Notifications</source>
<target>Notifications</target> <target>Notifications</target>
<note>Toolbar label for Notifications view.</note> <note>Section header for Damus notifications
Toolbar label for Notifications view.</note>
</trans-unit> </trans-unit>
<trans-unit id="Nudity or explicit content" xml:space="preserve"> <trans-unit id="Nudity or explicit content" xml:space="preserve">
<source>Nudity or explicit content</source> <source>Nudity or explicit content</source>
@@ -913,6 +936,16 @@ Picker option to indicate that a zap should be sent privately and not identify t
<target>Public key</target> <target>Public key</target>
<note>Label indicating that the text is a user's public account key.</note> <note>Label indicating that the text is a user's public account key.</note>
</trans-unit> </trans-unit>
<trans-unit id="QR Code" xml:space="preserve">
<source>QR Code</source>
<target>QR Code</target>
<note>Button to view profile's qr code.</note>
</trans-unit>
<trans-unit id="Quote" xml:space="preserve">
<source>Quote</source>
<target>Quote</target>
<note>Title of alert for confirming to make a quoted post.</note>
</trans-unit>
<trans-unit id="Reactions" xml:space="preserve"> <trans-unit id="Reactions" xml:space="preserve">
<source>Reactions</source> <source>Reactions</source>
<target>Reactions</target> <target>Reactions</target>
@@ -1001,8 +1034,7 @@ Picker option to indicate that a zap should be sent privately and not identify t
<trans-unit id="Repost" xml:space="preserve"> <trans-unit id="Repost" xml:space="preserve">
<source>Repost</source> <source>Repost</source>
<target>Repost</target> <target>Repost</target>
<note>Button to confirm reposting a post. <note>Title of alert for confirming to repost a post.</note>
Title of alert for confirming to repost a post.</note>
</trans-unit> </trans-unit>
<trans-unit id="Reposted" xml:space="preserve"> <trans-unit id="Reposted" xml:space="preserve">
<source>Reposted</source> <source>Reposted</source>
@@ -1253,7 +1285,8 @@ Picker option to indicate that a zap should be sent privately and not identify t
<trans-unit id="Translation" xml:space="preserve"> <trans-unit id="Translation" xml:space="preserve">
<source>Translation</source> <source>Translation</source>
<target>Translation</target> <target>Translation</target>
<note>Section header for text and appearance settings</note> <note>Navigation title for translation settings.
Section header for text and appearance settings</note>
</trans-unit> </trans-unit>
<trans-unit id="Translations" xml:space="preserve"> <trans-unit id="Translations" xml:space="preserve">
<source>Translations</source> <source>Translations</source>
@@ -1300,6 +1333,16 @@ Picker option to indicate that a zap should be sent privately and not identify t
<target>Universe 🛸</target> <target>Universe 🛸</target>
<note>Toolbar label for the universal view where posts from all connected relay servers appear.</note> <note>Toolbar label for the universal view where posts from all connected relay servers appear.</note>
</trans-unit> </trans-unit>
<trans-unit id="Unmute conversation" xml:space="preserve">
<source>Unmute conversation</source>
<target>Unmute conversation</target>
<note>Context menu option for unmuting a conversation.</note>
</trans-unit>
<trans-unit id="Upload" xml:space="preserve">
<source>Upload</source>
<target>Upload</target>
<note>Button to proceed with uploading.</note>
</trans-unit>
<trans-unit id="User has been muted" xml:space="preserve"> <trans-unit id="User has been muted" xml:space="preserve">
<source>User has been muted</source> <source>User has been muted</source>
<target>User has been muted</target> <target>User has been muted</target>
@@ -1339,10 +1382,16 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
ARE YOU SURE YOU WANT TO CONTINUE?</target> ARE YOU SURE YOU WANT TO CONTINUE?</target>
<note>Alert for deleting the users account.</note> <note>Alert for deleting the users account.</note>
</trans-unit> </trans-unit>
<trans-unit id="Wake up, %@" xml:space="preserve">
<source>Wake up, %@</source>
<target>Wake up, %@</target>
<note>Text telling the user to wake up, where the argument is their display name.</note>
</trans-unit>
<trans-unit id="Wallet" xml:space="preserve"> <trans-unit id="Wallet" xml:space="preserve">
<source>Wallet</source> <source>Wallet</source>
<target>Wallet</target> <target>Wallet</target>
<note>Sidebar menu label for Wallet view.</note> <note>Sidebar menu label for Wallet view.
Title for section in zap settings that controls the Lightning wallet selection.</note>
</trans-unit> </trans-unit>
<trans-unit id="Website" xml:space="preserve"> <trans-unit id="Website" xml:space="preserve">
<source>Website</source> <source>Website</source>
@@ -1374,6 +1423,11 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
<target>Yes, Post with Private Key</target> <target>Yes, Post with Private Key</target>
<note>Button to proceed with posting a note even though it looks like they might be posting a private key.</note> <note>Button to proceed with posting a note even though it looks like they might be posting a private key.</note>
</trans-unit> </trans-unit>
<trans-unit id="You are dreaming..." xml:space="preserve">
<source>You are dreaming...</source>
<target>You are dreaming...</target>
<note>Text telling the user that they are dreaming.</note>
</trans-unit>
<trans-unit id="You have no bookmarks yet, add them in the context menu" xml:space="preserve"> <trans-unit id="You have no bookmarks yet, add them in the context menu" xml:space="preserve">
<source>You have no bookmarks yet, add them in the context menu</source> <source>You have no bookmarks yet, add them in the context menu</source>
<target>You have no bookmarks yet, add them in the context menu</target> <target>You have no bookmarks yet, add them in the context menu</target>
@@ -1425,8 +1479,10 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
<source>Zaps</source> <source>Zaps</source>
<target>Zaps</target> <target>Zaps</target>
<note>Navigation bar title for the Zaps view. <note>Navigation bar title for the Zaps view.
Navigation title for zap settings.
Section header for zap settings Section header for zap settings
Setting to enable Zap Local Notification</note> Setting to enable Zap Local Notification
Title for section in zap settings that controls general zap preferences.</note>
</trans-unit> </trans-unit>
<trans-unit id="https://example.com/pic.jpg" xml:space="preserve"> <trans-unit id="https://example.com/pic.jpg" xml:space="preserve">
<source>https://example.com/pic.jpg</source> <source>https://example.com/pic.jpg</source>
Binary file not shown.
Binary file not shown.
Binary file not shown.
-366
View File
@@ -1,366 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>collapsed_event_view_other_notes</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@NOTES@</string>
<key>NOTES</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>... %d autre note ...</string>
<key>many</key>
<string>... %d autres notes ...</string>
<key>other</key>
<string>... %d autres notes ...</string>
</dict>
</dict>
<key>followers_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@FOLLOWERS@</string>
<key>FOLLOWERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Abonné</string>
<key>many</key>
<string>Abonnés</string>
<key>other</key>
<string>Abonnés</string>
</dict>
</dict>
<key>following_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@FOLLOWING@</string>
<key>FOLLOWING</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Abonnement</string>
<key>many</key>
<string>Abonnements</string>
<key>other</key>
<string>Abonnements</string>
</dict>
</dict>
<key>reacted_tagged_in_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REACTED@</string>
<key>REACTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ et %1$d autre ont réagi à une note dans laquelle vous apparaissez</string>
<key>many</key>
<string>%2$@ et %1$d autres ont réagi à une note dans laquelle vous apparaissez</string>
<key>other</key>
<string>%2$@ et %1$d autres ont réagi à une note dans laquelle vous apparaissez</string>
</dict>
</dict>
<key>reacted_your_post_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REACTED@</string>
<key>REACTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ et %1$d autre ont réagi à votre note</string>
<key>many</key>
<string>%2$@ et %1$d autres ont réagi à votre note</string>
<key>other</key>
<string>%2$@ et %1$d autres ont réagi à votre note</string>
</dict>
</dict>
<key>reacted_your_profile_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REACTED@</string>
<key>REACTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ et %1$d autre ont réagi à votre profil</string>
<key>many</key>
<string>%2$@ et %1$d autres ont réagi à votre profil</string>
<key>other</key>
<string>%2$@ et %1$d autres ont réagi à votre profil</string>
</dict>
</dict>
<key>reactions_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REACTIONS@</string>
<key>REACTIONS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Réaction</string>
<key>many</key>
<string>Réactions</string>
<key>other</key>
<string>Réactions</string>
</dict>
</dict>
<key>relays_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@RELAYS@</string>
<key>RELAYS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Relai</string>
<key>many</key>
<string>Relais</string>
<key>other</key>
<string>Relais</string>
</dict>
</dict>
<key>replying_to_two_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Réponse à %2$@, %3$@ &amp; %1$d autre</string>
<key>many</key>
<string>Réponse à %2$@, %3$@ &amp; %1$d autres</string>
<key>other</key>
<string>Réponse à %2$@, %3$@ &amp; %1$d autres</string>
</dict>
</dict>
<key>reposted_tagged_in_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REPOSTED@</string>
<key>REPOSTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ et %1$d autre ont cité une note dans laquelle vous apparaissez</string>
<key>many</key>
<string>%2$@ et %1$d autres ont cité une note dans laquelle vous apparaissez</string>
<key>other</key>
<string>%2$@ et %1$d autres ont republié une note dans laquelle vous apparaissez</string>
</dict>
</dict>
<key>reposted_your_post_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REPOSTED@</string>
<key>REPOSTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ et %1$d autre ont cité votre note</string>
<key>many</key>
<string>%2$@ et %1$d autres ont cité votre note</string>
<key>other</key>
<string>%2$@ et %1$d autres ont republié votre note</string>
</dict>
</dict>
<key>reposted_your_profile_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REPOSTED@</string>
<key>REPOSTED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ et %1$d autre ont republié votre profile</string>
<key>many</key>
<string>%2$@ et %1$d autres ont republié votre profile</string>
<key>other</key>
<string>%2$@ et %1$d autres ont republié votre profile</string>
</dict>
</dict>
<key>reposts_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REPOSTS@</string>
<key>REPOSTS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Republication</string>
<key>many</key>
<string>Republications</string>
<key>other</key>
<string>Republications</string>
</dict>
</dict>
<key>sats_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%1$#@SATS@</string>
<key>SATS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>one</key>
<string>%2$@ sat</string>
<key>many</key>
<string>%2$@ sats</string>
<key>other</key>
<string>%2$@ sats</string>
</dict>
</dict>
<key>zap_notification_no_message</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%1$#@NOTIFICATION@</string>
<key>NOTIFICATION</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>one</key>
<string>Vous avez reçu %2$@ sat de %3$@</string>
<key>many</key>
<string>Vous avez reçu %2$@ sats de %3$@</string>
<key>other</key>
<string>Vous avez reçu %2$@ sats de %3$@</string>
</dict>
</dict>
<key>zap_notification_with_message</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%1$#@NOTIFICATION@</string>
<key>NOTIFICATION</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>one</key>
<string>You received %2$@ sat from %3$@: "%4$@"</string>
<key>many</key>
<string>You received %2$@ sats from %3$@: "%4$@"</string>
<key>other</key>
<string>You received %2$@ sats from %3$@: "%4$@"</string>
</dict>
</dict>
<key>zapped_tagged_in_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@ZAPPED@</string>
<key>ZAPPED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ et %1$d autre ont zappé une note dans laquelle vous apparaissez</string>
<key>many</key>
<string>%2$@ et %1$d autres ont zappé une note dans laquelle vous apparaissez</string>
<key>other</key>
<string>%2$@ et %1$d autres ont zappé une note dans laquelle vous apparaissez</string>
</dict>
</dict>
<key>zapped_your_post_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@ZAPPED@</string>
<key>ZAPPED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ et %1$d autre ont zappé votre note</string>
<key>many</key>
<string>%2$@ et %1$d autres ont zappé votre note</string>
<key>other</key>
<string>%2$@ et %1$d autres ont zappé votre note</string>
</dict>
</dict>
<key>zapped_your_profile_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@ZAPPED@</string>
<key>ZAPPED</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ et %1$d autre ont zappé votre profile</string>
<key>many</key>
<string>%2$@ et %1$d autres ont zappé votre profile</string>
<key>other</key>
<string>%2$@ et %1$d autres ont zappé votre profile</string>
</dict>
</dict>
<key>zaps_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@ZAPS@</string>
<key>ZAPS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Zap</string>
<key>many</key>
<string>Zaps</string>
<key>other</key>
<string>Zaps</string>
</dict>
</dict>
</dict>
</plist>

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