Compare commits

..

208 Commits

Author SHA1 Message Date
tyiu c1e5787120 Add validation to prevent whitespaces be inputted on NIP-05 input field 2023-03-27 10:34:39 -04:00
William Casarin 0b40cd127c Revert "Revert "Don't make previews full bleed""
This reverts commit 57006b928b.
2023-03-26 09:36:02 -06:00
William Casarin 754ee254e9 Revert "Revert "New Timeline""
This reverts commit f5ed9cd5d4.
2023-03-26 09:35:53 -06:00
William Casarin 963cb37762 Revert "Increase image size"
This reverts commit b6d5b6f45e.
2023-03-26 09:35:07 -06:00
William Casarin 159d0fa2b5 Don't render @note link if there is only one 2023-03-25 07:54:04 -06:00
William Casarin 61fddf800e Reduced padding for more information density
Changelog-Changed: Reduced padding for more information density
2023-03-25 06:51:56 -06:00
William Casarin b6d5b6f45e Increase image size 2023-03-25 06:38:06 -06:00
William Casarin f5ed9cd5d4 Revert "New Timeline"
This reverts commit f84d4516db.
2023-03-25 06:31:24 -06:00
William Casarin 57006b928b Revert "Don't make previews full bleed"
This reverts commit 98f0b2f2d2.
2023-03-25 06:31:18 -06:00
William Casarin 98f0b2f2d2 Don't make previews full bleed 2023-03-24 08:14:32 -06:00
William Casarin 9a4d93824a v1.3.0-7 changelog 2023-03-24 08:00:38 -06:00
William Casarin f76563b354 v1.3.0-7 2023-03-24 07:59:58 -06:00
William Casarin f84d4516db New Timeline
Switch to a new timeline style that has higher information density and
better image display
2023-03-23 19:03:54 -06:00
William Casarin 2e34230119 Clean up image views 2023-03-23 08:54:25 -06:00
William Casarin cad89525b7 Remove filenames from image preview
Keep it clean

Suggested-by: jack
2023-03-23 07:33:48 -06:00
William Casarin d2cf18aeee v1.3.0-6 changelog 2023-03-21 06:41:46 -06:00
William Casarin a8ce39fc96 v1.3.0-6 2023-03-21 06:41:06 -06:00
William Casarin ed90139b0c Fix bug where nostr: links and QRs stopped working
Changelog-Fixed: Fix bug where nostr: links and QRs stopped working
2023-03-21 06:35:08 -06:00
William Casarin 022045d916 v1.3.0-5 changelog 2023-03-20 09:28:18 -06:00
William Casarin 4bda490010 v1.3.0-5 2023-03-20 09:27:13 -06:00
William Casarin 97382adb63 Switch DM relative time color to gray
Looks better in light mode
2023-03-20 08:58:55 -06:00
William Casarin c582755246 Fix internal links opening in other nostr clients
This prevents internal links from opening in other nostr apps

Changelog-Fixed: Fixed internal links opening in other nostr clients
2023-03-20 08:52:16 -06:00
Swift 44a59e8d57 Remove authentication for copying npub
Changelog-Fixed: Remove authentication for copying npub
Closes: #778
2023-03-20 08:31:16 -06:00
Joel Klabo 98685645d3 Add Time Ago to DM View
Changelog-Added: Add Time Ago to DM View
Closes: #790
2023-03-20 08:27:20 -06:00
William Casarin 14f71f1a1d v1.3.0-4 changelog 2023-03-17 11:48:01 -06:00
William Casarin 91cb6a6763 v1.3.0-4 2023-03-17 11:47:29 -06:00
William Casarin a65351154b Make it much easier to tag users in replies and posts
Changelog-Changed: It's much easier to tag users in replies and posts
2023-03-17 11:33:15 -06:00
William Casarin 2e2b33e21d Fix bug where small black text appears during image upload
Changelog-Fixed: Fix bug where small black text appears during image upload
2023-03-17 10:13:23 -06:00
William Casarin c24b0afb8f Don't show test event by accident 2023-03-17 10:13:00 -06:00
William Casarin a357bbe4a6 v1.3.0-3 changelog 2023-03-17 08:35:50 -06:00
William Casarin b687006b64 v1.3.0-3 2023-03-17 08:33:32 -06:00
William Casarin 1f095b0896 Make sure to publish progress update on main thread 2023-03-17 08:33:11 -06:00
William Casarin 4f7ed36a7c Fix image upload url delay after progress bar disappears
Changelog-Fixed: Fix image upload url delay after progress bar disappears
2023-03-17 08:23:33 -06:00
William Casarin 393809c7d7 Merge remote-tracking branch 'github/translations' 2023-03-17 08:01:43 -06:00
William Casarin 9091cb1aae Revert "Reduce battery usage by using exp backoff on connections"
This is causing pretty bad fail to reconnect issues

This reverts commit 252a77fd97, reversing
changes made to a611a5d252.
2023-03-17 07:54:29 -06:00
transifex-integration[bot] e78a82e5b7 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-03-17 10:02:56 +00:00
transifex-integration[bot] 7b0ef5f4a7 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-03-17 09:40:16 +00:00
transifex-integration[bot] 66a5df68b3 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-03-17 09:27:45 +00:00
transifex-integration[bot] fa2344b9ba 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-03-17 09:22:00 +00:00
transifex-integration[bot] 68c018cf44 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-03-17 08:41:21 +00:00
tyiu f367df2225 Fix localization issues, and export and import translations 2023-03-16 23:00:52 -04:00
William Casarin e0984aab34 Add space at end of image url so you don't accidently corrupt things 2023-03-16 09:46:43 -06:00
William Casarin eabbb12195 v1.3.0-2 changelog 2023-03-16 09:17:48 -06:00
William Casarin 7b1f4b7701 Show image upload progress 2023-03-16 09:13:03 -06:00
William Casarin 7b6d3ef9df Refactor image uploader 2023-03-15 17:12:05 -06:00
William Casarin bc58686016 Add post attachment bar for images and future things 2023-03-15 17:12:05 -06:00
Swift a574dcb27c Add image uploader
Changelog-Added: Add image uploader
2023-03-15 17:12:05 -06:00
William Casarin 761982e359 Merge remote-tracking branch 'github/translations' 2023-03-15 17:03:28 -06:00
William Casarin 57d48a0395 Add option to always show images (never blur)
Changelog-Added: Add option to always show images (never blur)
2023-03-15 16:56:25 -06:00
William Casarin 4f96c88b9b Add nostr.wine to bootstrap relay list, remove others 2023-03-15 16:22:30 -06:00
William Casarin da11bc575a Remove snort from bootstrap relay list 2023-03-15 16:21:08 -06:00
William Casarin cc9532d958 Fix zap button long press scrolling issue
Changelog-Fixed: Fix zap button preventing scrolling
2023-03-15 16:19:52 -06:00
William Casarin 35f4e7c78d Don't pop-in embedded note if we have it cached
Changelog-Changed: Fixed embedded note popping
2023-03-15 15:50:13 -06:00
transifex-integration[bot] d8c822858a Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'es_419' language.
2023-03-15 17:47:15 -04:00
transifex-integration[bot] ca0c837231 Apply translations in es_419
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'es_419' language.
2023-03-15 17:47:15 -04:00
transifex-integration[bot] 38fc5afa44 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-03-15 17:47:14 -04:00
tyiu 9b76afae4f Add Hungarian translations 2023-03-15 17:47:14 -04:00
transifex-integration[bot] f911f1646d 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-03-15 17:47:14 -04:00
transifex-integration[bot] 20fd061293 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-03-15 17:47:13 -04:00
transifex-integration[bot] 3f5262cd5d 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-03-15 17:47:13 -04:00
transifex-integration[bot] 982d15ab4a 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-03-15 17:47:13 -04:00
transifex-integration[bot] 074b6efc0f 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-03-15 17:47:13 -04:00
transifex-integration[bot] ad0ca6ca1a 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-03-15 17:47:12 -04:00
transifex-integration[bot] e140cacfdf 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-03-15 17:47:12 -04:00
transifex-integration[bot] b825aa80d8 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-03-15 17:47:12 -04:00
transifex-integration[bot] 9ee91553c1 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-03-15 17:47:11 -04:00
transifex-integration[bot] 7ce862f552 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-03-15 17:47:11 -04:00
transifex-integration[bot] 231f9d1853 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ko' language.
2023-03-15 17:47:11 -04:00
transifex-integration[bot] 63acf11065 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ko' language.
2023-03-15 17:47:10 -04:00
transifex-integration[bot] 0502f06ef8 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ko' language.
2023-03-15 17:47:10 -04:00
transifex-integration[bot] d921a40f24 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ko' language.
2023-03-15 17:47:10 -04:00
tyiu 2e82b349b7 Add Korean and Swedish 2023-03-15 17:47:09 -04:00
transifex-integration[bot] b0007af030 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ko' language.
2023-03-15 17:47:09 -04:00
transifex-integration[bot] dd5c2d7301 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ko' language.
2023-03-15 17:47:09 -04:00
transifex-integration[bot] 27c0fbf453 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ko' language.
2023-03-15 17:47:08 -04:00
transifex-integration[bot] d1ad4dc9ff Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ko' language.
2023-03-15 17:47:08 -04:00
transifex-integration[bot] 4c58c4ffef Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ko' language.
2023-03-15 17:47:08 -04:00
transifex-integration[bot] cb3603fb35 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-03-15 17:47:07 -04:00
transifex-integration[bot] 6df5288294 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ko' language.
2023-03-15 17:47:07 -04:00
transifex-integration[bot] 9e02dac5d0 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-03-15 17:47:07 -04:00
transifex-integration[bot] b7d9db5cec 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-03-15 17:47:07 -04:00
transifex-integration[bot] e46792e596 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-03-15 17:47:06 -04:00
transifex-integration[bot] fc65da3473 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-03-15 17:47:06 -04:00
transifex-integration[bot] 4f15469320 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-03-15 17:47:06 -04:00
transifex-integration[bot] 3ea3595902 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-03-15 17:47:05 -04:00
transifex-integration[bot] 3caebd9c63 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-03-15 17:47:05 -04:00
transifex-integration[bot] 4d4f340ab0 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-03-15 17:47:05 -04:00
transifex-integration[bot] 6a549e5019 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-03-15 17:47:04 -04:00
transifex-integration[bot] 52bf47a494 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-03-15 17:47:04 -04:00
transifex-integration[bot] aee243d3e0 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'ko' language.
2023-03-15 17:47:04 -04:00
transifex-integration[bot] 18745403ce 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-03-15 17:47:03 -04:00
William Casarin 07a20040a4 Bump notification limit from 100 to 500
Changelog-Changed: Bump notification limit from 100 to 500
2023-03-15 15:41:11 -06:00
William Casarin ef3ef03b7f v1.3.0 changelog 2023-03-15 10:57:36 -06:00
William Casarin 71e3ee4867 v1.3.0 2023-03-15 10:55:37 -06:00
Bryan Montz 252a77fd97 Reduce battery usage by using exp backoff on connections
Changelog-Changed: Reduce battery usage by using exp backoff on connections
2023-03-15 10:48:47 -06:00
William Casarin a611a5d252 Fix tests 2023-03-15 09:43:48 -06:00
William Casarin 1533be77d8 Extend user tagging search to all local profiles
Changelog-Added: Extend user tagging search to all local profiles
Changelog-Fixed: Show @ mentions for users with display_names and no username
Changelog-Fixed: Make user search case insensitive
2023-03-15 08:47:15 -06:00
William Casarin c05223ca2b refactor: extract on_user_tapped in UserSearch 2023-03-14 17:12:37 -06:00
William Casarin 5d441d3192 refactor: create search_profiles helper 2023-03-14 17:10:43 -06:00
William Casarin 04bce34297 Don't show both realname and username if they are the same
Changelog-Changed: Don't show both realname and username if they are the same
2023-03-14 16:53:13 -06:00
William Casarin af8ce3d32d Revert "Fix cursor jumping around after pressing return"
This reverts commit dd511c3061.
2023-03-14 16:34:31 -06:00
Bryan Montz cabe584938 fix "Replying to..." issues and improve related tests 2023-03-14 11:44:40 -06:00
gladius dd511c3061 Fix cursor jumping around after pressing return
Changelog-Fixed: Fix cursor jumping around after pressing return
Fixes: #728, #747
Closes: #742
2023-03-13 13:00:52 -06:00
OlegAba 18449c8c0d Fix repost button sometimes not working
Changelog-Fixed: Fix repost button sometimes not working
Closes: #738
2023-03-13 12:54:13 -06:00
William Casarin 044631b324 Merge remote-tracking branch 'github/translations' 2023-03-13 12:46:36 -06:00
William Casarin 318b254b5d Revert "Merge remote-tracking branch 'tyiu/translations'"
This reverts commit 6872382bb7, reversing
changes made to 42ea150d45.
2023-03-13 12:45:36 -06:00
benthecarman 487419d098 Don't show follows you for own profile
Changelog-Fixed: Don't show follows you for your own profile
Closes: #740
2023-03-13 12:44:52 -06:00
Jack Chakany ba82f19a11 Fix Damus logo overlaying over the sidebar
Changlog-Fixed: Fix Damus logo overlaying over the sidebar
Closes: #743
2023-03-13 12:43:51 -06:00
ericholguin cba6b3aef7 Dismiss Keyboard in Search View
Changlog-Fixed: Dismiss keyboard in search view
Closes: #749
2023-03-13 12:43:14 -06:00
William Casarin 6872382bb7 Merge remote-tracking branch 'tyiu/translations' 2023-03-13 12:38:45 -06:00
Swift 42ea150d45 Show error on invalid lightning tip address
Changelog-Changed: Show error on invalid lightning tip address
Closes: #752
2023-03-13 12:37:17 -06:00
OlegAba 85f86ee31f Fix selected event text padding
Closes: #753
2023-03-13 12:33:04 -06:00
William Casarin 96decd2392 Revert "Fix mentions not working in middle of new note"
This breaks other things, the autocomplete doesn't go away after tag
selection now

This reverts commit 1e7d9a6373.
2023-03-13 11:49:55 -06:00
Swift 73f7b69654 Add vibrate on zap
Changelog-Added: Vibrate when a zap is received
Closes: #768
2023-03-13 11:41:03 -06:00
ericholguin d982bb886e Match event time font color
Closes: #755
2023-03-13 10:11:50 -06:00
ericholguin 9766653969 Add dot operator separate event time from profile name 2023-03-13 10:11:27 -06:00
ericholguin 5d91e7e595 Use light gray in light mode and medium gray in dark for ellipsis 2023-03-13 10:11:27 -06:00
ericholguin ae00c103ad Adjusted repost font size and weight 2023-03-13 10:11:27 -06:00
gladiusKatana 88aa713729 Fix json appearing in profile searches
Changelog-Fixed: Fix json appearing in profile searches
Closes: #757
Fixes: #748
2023-03-13 10:09:40 -06:00
OlegAba be1c03ad0e Fix KF options order
Closes: #758
2023-03-13 09:47:03 -06:00
Bryan Montz b2b62828e3 Fix unexpected font size on PostView
Changelog-Fixed: Fix unexpected font size when posting
Closes: #761
2023-03-13 09:44:59 -06:00
Joel Klabo d1a77891c7 Make DM Content More Visible
Changelog-Changed: Make DM Content More Visible
Closes: #760
2023-03-13 09:43:52 -06:00
OlegAba 20505236ae Fix tabbar sticking to keyboard 2023-03-13 09:40:49 -06:00
OlegAba 094ac34135 Fix keyboard sticking issues
Changelog-Fixed: Fix keyboard sticking issues
Closes: #763
2023-03-13 09:40:11 -06:00
ericholguin 6b6743fcbb Added new and improved Share sheet
Changelog-Added: New and Improved Share sheet
Closes: #764
2023-03-13 09:37:05 -06:00
gladiusKatana 8059408d5f Remove spaces from hashtag searches
Changelog-Changed: Remove spaces from hashtag searches
Closes: #773
Fixes: #741
2023-03-13 09:04:10 -06:00
Joel Klabo 04fa4edad8 Fixed tab bar background color on macOS
Changelog-Fixed: Fixed tab bar background color on macOS
Closes: #765
2023-03-13 09:04:10 -06:00
gladiusKatana 6fffe250c2 Fix some links getting interpreted as images
Changelog-Fixed: Fix some links getting interpreted as images
Closes: #774
Fixes: #766
2023-03-13 09:03:53 -06:00
gladiusKatana 1e7d9a6373 Fix mentions not working in middle of new note
Changelog-Fixed: Fix mentions not working in middle of new note
Closes: #775
2023-03-13 08:30:16 -06:00
transifex-integration[bot] 21989719fc 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-03-13 08:38:46 +00:00
transifex-integration[bot] d5e4866c55 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-03-13 08:38:29 +00:00
transifex-integration[bot] f305df3471 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-03-13 08:38:19 +00:00
transifex-integration[bot] 21320367b1 Apply translations in zh_CN
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'zh_CN' language.
2023-03-13 08:25:07 +00:00
transifex-integration[bot] 82723faf33 Apply translations in zh_HK
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'zh_HK' language.
2023-03-13 08:16:11 +00:00
transifex-integration[bot] 48434f83ae Apply translations in zh_TW
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'zh_TW' language.
2023-03-13 08:16:04 +00:00
transifex-integration[bot] 083d0fa0e5 Apply translations in sv_SE
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'sv_SE' language.
2023-03-13 06:56:37 +00:00
tyiu d5a646f9ce Update Translations 🤖 2023-03-12 18:15:48 +00:00
tyiu 38a1ad7611 WIP translations CI 2023-03-13 05:13:20 +11:00
transifex-integration[bot] 9bc3860f00 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ko' language.
2023-03-12 17:23:06 +00:00
transifex-integration[bot] 35f5ac04b4 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ko' language.
2023-03-12 17:22:59 +00:00
transifex-integration[bot] 75b73718d1 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ko' language.
2023-03-12 17:22:53 +00:00
transifex-integration[bot] 29cacebe58 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ko' language.
2023-03-12 17:22:46 +00:00
transifex-integration[bot] 84ae914bcc Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ko' language.
2023-03-12 17:22:40 +00:00
transifex-integration[bot] ef5f3ae649 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ko' language.
2023-03-12 17:22:33 +00:00
transifex-integration[bot] f8068a42e5 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ko' language.
2023-03-12 17:22:27 +00:00
transifex-integration[bot] bdde33bb51 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ko' language.
2023-03-12 17:22:20 +00:00
transifex-integration[bot] e3b602df13 Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ko' language.
2023-03-12 17:22:14 +00:00
transifex-integration[bot] 38b17f1acd Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'ko' language.
2023-03-12 17:18:22 +00:00
transifex-integration[bot] 575b91554c Apply translations in ko
100% translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.stringsdict'
on the 'ko' language.
2023-03-12 17:08:21 +00:00
transifex-integration[bot] f36bc84618 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-03-12 12:07:15 +00:00
transifex-integration[bot] d54c9b7d12 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-03-12 11:59:18 +00:00
tyiu 2c6647c95a Fix localization issues, import translations, and add Bulgarian, Persian, and Ukrainian 2023-03-12 09:06:34 +11:00
William Casarin 3c2f281c6d Remember last notification tab
Suggested-By: Jack Dorsey
2023-03-05 20:36:53 -05:00
William Casarin 4ba63b0dbd v1.2.0-5 2023-03-05 19:47:49 -05:00
William Casarin e2df7d5df6 Notification Filters
Changelog-Added: Add filters to notification view
2023-03-05 19:44:28 -05:00
William Casarin 0dfea0680f v1.2.0-4 changelog 2023-03-05 18:58:40 -05:00
William Casarin 6cc34632fd v1.2.0-4 2023-03-05 18:57:49 -05:00
William Casarin dffb60a601 Immediately search for events and profiles
Instead of having to click twice

Changelog-Changed: Immediately search for events and profiles
2023-03-05 18:55:59 -05:00
William Casarin df076b03fd Possibly fix repost button not working issue 2023-03-05 15:47:43 -05:00
William Casarin fc83cd4db7 Use long-press gesture for custom zaps
Changelog-Changed: Use long-press for custom zaps
2023-03-05 15:43:35 -05:00
OlegAba e01761ce72 Fixed hit detection bugs on profile page
Changelog-Fixed: Fixed hit detection bugs on profile page
Closes: #652
2023-03-05 15:25:59 -05:00
percy-g2 efc50f5b18 Preview profile name
Closes: #663
2023-03-05 15:25:07 -05:00
Bryan Montz 10c9e8ddbc Fix disappearing text on Thread view
Changelog-Fixed: Fix disappearing text on Thread view
Closes: #665
2023-03-05 15:22:42 -05:00
Joel Klabo f88718d56e Render Links etc. in Notification Summaries
Changelog-Fixed: Render links in notification summaries
Closes: #721
2023-03-05 15:20:38 -05:00
ericholguin b6a7f52596 Add menu ellipsis button to notes
Changelog-Added: Add ellipsis button to notes
2023-03-05 15:17:04 -05:00
William Casarin cff98161ee Don't show notifications from ourselves
Changelog-Fixed: Don't show notifications from ourselves
2023-03-05 15:15:23 -05:00
Jack Chakany 8a70240968 Dedupe timelineNavItem
Changelog-Fixed: Fix issue where navbar back button would show the wrong text
Closes: #687
2023-03-05 15:11:26 -05:00
Jack Chakany a4855775ef Fix navbar title so it changes based on what page you were on previously. 2023-03-05 15:06:42 -05:00
randymcmillan 06c2741bf4 Always make hashtag filters lowercased
Changelog-Fixed: Fix case sensitivity when searching hashtags
Closes: #737
2023-03-05 14:56:36 -05:00
Swift 721bb9abf5 Make shaka animation smoother
Changelog-Changed: Make shaka animation smoother
Closes: #734
2023-03-05 14:52:03 -05:00
Bryan Montz 89bb293acd Prune EventCache when iOS fires memory warning
Closes: #736
2023-03-05 14:50:12 -05:00
William Casarin f9c330aebf Fix issue where opening reposts shows json
Changelog-Fixed: Fix issue where opening reposts shows json
2023-03-05 14:37:44 -05:00
William Casarin ffbfcd36f5 v1.2.0-3 changelog 2023-03-04 18:25:24 -05:00
William Casarin 52f568f9b3 v1.2.0-3 2023-03-04 18:24:20 -05:00
William Casarin 1c2a7db328 slightly smoother shaka animation 2023-03-04 18:18:58 -05:00
OlegAba 3110abc65b Wrap long profile display name
Changelog-Fixed: Wrap long profile display names
Closes: #702
2023-03-04 17:53:26 -05:00
ericholguin a9f62960ec Add additional info to recommended relay view
Changelog-Added: Add additional info to recommended relay view
Closes: #703
2023-03-04 17:52:25 -05:00
Swift 150bbb1eb2 Add shaka animation
Changelog-Added: Add shaka animation
Closes: #705
2023-03-04 17:51:01 -05:00
OlegAba 0aff41d384 Add option to disable image animation
Changelog-Added: Add option to disable image animation
Closes: #707
2023-03-04 17:49:39 -05:00
ericholguin 3fec9dd209 Additional delete confirmation and sign out on config view
Changelog-Added: Add additional warning when deleting account
Closes: #729
2023-03-04 17:48:35 -05:00
OlegAba a560d50366 Scale to fill pfp
Changelog-Fixed: Fixed weird scaling on profile pictures
Closes: #712
2023-03-04 17:47:41 -05:00
Joel Klabo 174f7f6cc5 Update Width of Copy Pubkey Background
Changelog-Fixed: Fixed width of copy pubkey on profile page
Closes: #714
2023-03-04 17:45:42 -05:00
tyiu a325a3c064 Translations (#722)
* Add missing comments to localizable strings and change zap type picker style

* Apply translations in nl

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

* Apply translations in ja

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

* Apply translations in de

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

* Apply translations in de

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in pl_PL

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 fr_FR

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

* Apply translations in es_419

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

* Apply translations in pl_PL

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

* Apply translations in es_419

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

* Apply translations in es_419

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

* Apply translations in el_GR

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 cs

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

* Apply translations in uk

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

* Apply translations in ru

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2023-03-04 17:45:02 -05:00
William Casarin d0a6c2e2e4 Thread Caching
Changelog-Added: Threads now load instantly and are cached
2023-03-04 17:40:22 -05:00
William Casarin b58baca227 Bookmarks Refactor
- Don't do async loading stuff
- Move bookmarkmanager to damus state
- Remove bookmarks update notififcation and switch to observed object
- Switch api to use events explicitly instead of strings
2023-03-03 11:57:18 -05:00
Joel Klabo 5423704980 Make purple color more consistent in mentions
Changelog-Fixed: Make damus purple use more consistent in mentions
Closes: #709
2023-03-03 10:59:29 -05:00
William Casarin 241ed1041d build 2 2023-03-03 10:59:14 -05:00
William Casarin 5134004ff7 Fix zap creation 2023-03-01 21:59:01 -08:00
William Casarin 071a4209ea Only send lud12 comment if its not a private zap 2023-03-01 10:51:49 -08:00
William Casarin 7f385b2e7e Switch to new build train 2023-03-01 10:51:38 -08:00
William Casarin 502c4daf6f v1.1.0-10 changelog 2023-03-01 10:03:10 -08:00
William Casarin ffe2c7284a v1.1.0-10 2023-03-01 10:02:30 -08:00
OlegAba 6b1f57d6d0 Truncate long notes (#715)
Changelog-Added: Truncate large posts and add a show more button
2023-03-01 09:57:39 -08:00
William Casarin 77f5268336 Private Zaps
This adds private zaps, which have messages and authors encrypted to
the target. Keys are deterministically generated so that both the
receiver and sender can decrypt.

Changelog-Added: Private Zaps
2023-03-01 09:56:25 -08:00
tyiu c72c0079cc Translations (#701)
* Apply translations in fr_FR

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

* Apply translations in fr_FR

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

* Apply translations in fr_FR

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

* Apply translations in fr_FR

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

* Apply translations in fr_FR

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

* Apply translations in fr_FR

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

* Apply translations in fr_FR

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

* Apply translations in fr_FR

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

* Apply translations in fr_FR

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

* Apply translations in fr_FR

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

* Apply translations in ar

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

* Apply translations in de

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

* Apply translations in de

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

* Apply translations in nl

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

* Apply translations in nl

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in ar

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

* Apply translations in cs

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

* Apply translations in cs

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

* Apply translations in cs

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

* Apply translations in el_GR

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

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

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

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

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

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

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 fr_FR

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

* Apply translations in ja

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

* Apply translations in ja

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

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

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

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

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

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

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

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

* Apply translations in ja

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

* Apply translations in ja

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

* Apply translations in ja

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

* Apply translations in ja

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

* Apply translations in ja

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

* Apply translations in ja

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

* Apply translations in ja

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

* Apply translations in ja

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

* Apply translations in ja

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

* Apply translations in ja

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

* Apply translations in ja

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

* Apply translations in ja

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

* Apply translations in ja

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

* Apply translations in ja

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

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

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

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

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

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

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

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

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

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

* Apply translations in ja

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

* Apply translations in ja

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

* Apply translations in ja

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

* Apply translations in ja

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

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

* Apply translations in ja

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

* Apply translations in ja

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

* Apply translations in ja

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

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

* Apply translations in ja

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

* Apply translations in ja

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

* Apply translations in ja

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

* Apply translations in ja

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

* Apply translations in ja

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

* Apply translations in ja

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

* Apply translations in ja

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Apply translations in ar

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

* Apply translations in de

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

* Apply translations in cs

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

* Apply translations in it_IT

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

* Apply translations in de

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

* Apply translations in de

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

* Apply translations in de

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

* Apply translations in de

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

* Apply translations in ja

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

* Apply translations in ja

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

* Apply translations in lv_LV

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

* Apply translations in lv_LV

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

* Apply translations in lv_LV

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

* Apply translations in lv_LV

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

* Apply translations in lv_LV

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

* Apply translations in lv_LV

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

* Apply translations in lv_LV

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

* Apply translations in lv_LV

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

* Apply translations in lv_LV

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

* Apply translations in ja

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

* Apply translations in ja

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

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

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2023-03-01 07:42:13 -08:00
William Casarin 5ab1d6294c Fix default zap amount setting not getting updated
Changelog-Fixed: Fix default zap amount setting not getting updated
2023-02-27 11:08:03 -08:00
William Casarin 2f90f2d4b7 Fix issue where keyboard covers custom zap comment
Changelog-Fixed: Fix issue where keyboard covers custom zap comment
2023-02-27 11:03:09 -08:00
Bryan Montz 7c2e8a6cc5 Merge branch 'master' into exp-backoff 2023-02-27 06:23:38 -06:00
William Casarin 1288732e5d v1.1.0-9 changelog 2023-02-26 16:01:21 -08:00
William Casarin 4a6c6a65ab v1.1.0-9 2023-02-26 15:59:55 -08:00
William Casarin 0f29d67e1f ensure blocked users do not show in notifications 2023-02-26 15:56:31 -08:00
William Casarin 9fd2f51971 Merge remote-tracking branch 'tyiu/tyiu/translations' 2023-02-26 15:48:55 -08:00
William Casarin 386bae64ca scroll coordinate space 2023-02-26 15:46:17 -08:00
William Casarin 4b5c217213 Add scroll queue detection in notification view
This will stop injecting events into the timeline if you're scrolling
2023-02-26 14:14:25 -08:00
Bryan Montz 6c63f8f22a add unit tests for RelayPool 2023-02-25 07:34:31 -06:00
Bryan Montz 673358408a refinements to RelayConnection and RelayPool 2023-02-24 22:39:58 -06:00
Bryan Montz 0210ae5d61 fix build 2023-02-23 07:11:09 -06:00
Bryan Montz e5749c8748 apply exponential backoff to retrying stale relay connections to reduce energy use 2023-02-23 06:45:14 -06:00
149 changed files with 8634 additions and 1985 deletions
+196 -1
View File
@@ -1,3 +1,199 @@
## [1.3.0-7] - 2023-03-24
- New experimental timeline view
[1.3.0-7]: https://github.com/damus-io/damus/releases/tag/v1.3.0-7
## [1.3.0-6] - 2023-03-21
### Fixed
- Fix bug where nostr: links and QRs stopped working (William Casarin)
[1.3.0-6]: https://github.com/damus-io/damus/releases/tag/v1.3.0-6
## [1.3.0-5] - 2023-03-20
### Added
- Add Time Ago to DM View (Joel Klabo)
### Fixed
- Fixed internal links opening in other nostr clients (William Casarin)
- Remove authentication for copying npub (Swift)
[1.3.0-5]: https://github.com/damus-io/damus/releases/tag/v1.3.0-5
## [1.3.0-4] - 2023-03-17
### Changed
- It's much easier to tag users in replies and posts (William Casarin)
### Fixed
- Fix bug where small black text appears during image upload (William Casarin)
[1.3.0-4]: https://github.com/damus-io/damus/releases/tag/v1.3.0-4
## [1.3.0-3] - 2023-03-17
### Fixed
- Fix image upload url delay after progress bar disappears (William Casarin)
- Fix issue where damus stops trying to reconnect (William Casarin)
[1.3.0-3]: https://github.com/damus-io/damus/releases/tag/v1.3.0-3
## [1.3.0-2] - 2023-03-16
### Added
- Add image uploader (Swift)
- Add option to always show images (never blur) (William Casarin)
### Changed
- Fixed embedded note popping (William Casarin)
- Bump notification limit from 100 to 500 (William Casarin)
### Fixed
- Fix zap button preventing scrolling (William Casarin)
[1.3.0-2]: https://github.com/damus-io/damus/releases/tag/v1.3.0-2
## [1.3.0] - 2023-03-15
### Added
- Extend user tagging search to all local profiles (William Casarin)
- Vibrate when a zap is received (Swift)
- New and Improved Share sheet (ericholguin)
### Changed
- Reduce battery usage by using exp backoff on connections (Bryan Montz)
- Don't show both realname and username if they are the same (William Casarin)
- Show error on invalid lightning tip address (Swift)
- Make DM Content More Visible (Joel Klabo)
- Remove spaces from hashtag searches (gladiusKatana)
### Fixed
- Show @ mentions for users with display_names and no username (William Casarin)
- Make user search case insensitive (William Casarin)
- Fix repost button sometimes not working (OlegAba)
- Don't show follows you for your own profile (benthecarman)
- Fix json appearing in profile searches (gladiusKatana)
- Fix unexpected font size when posting (Bryan Montz)
- Fix keyboard sticking issues (OlegAba)
- Fixed tab bar background color on macOS (Joel Klabo)
- Fix some links getting interpreted as images (gladiusKatana)
[1.3.0]: https://github.com/damus-io/damus/releases/tag/v1.3.0
## [1.2.0-4] - 2023-03-05
### Added
- Add ellipsis button to notes (ericholguin)
### Changed
- Immediately search for events and profiles (William Casarin)
- Use long-press for custom zaps (William Casarin)
- Make shaka animation smoother (Swift)
### Fixed
- Fixed hit detection bugs on profile page (OlegAba)
- Fix disappearing text on Thread view (Bryan Montz)
- Render links in notification summaries (Joel Klabo)
- Don't show notifications from ourselves (William Casarin)
- Fix issue where navbar back button would show the wrong text (Jack Chakany)
- Fix case sensitivity when searching hashtags (randymcmillan)
- Fix issue where opening reposts shows json (William Casarin)
[1.2.0-4]: https://github.com/damus-io/damus/releases/tag/v1.2.0-4
## [1.2.0-3] - 2023-03-04
### Added
- Add additional info to recommended relay view (ericholguin)
- Add shaka animation (Swift)
- Add option to disable image animation (OlegAba)
- Add additional warning when deleting account (ericholguin)
- Threads now load instantly and are cached (William Casarin)
### Fixed
- Wrap long profile display names (OlegAba)
- Fixed weird scaling on profile pictures (OlegAba)
- Fixed width of copy pubkey on profile page (Joel Klabo)
- Make damus purple use more consistent in mentions (Joel Klabo)
[1.2.0-3]: https://github.com/damus-io/damus/releases/tag/v1.2.0-3
## [1.1.0-10] - 2023-03-01
### Added
- Truncate large posts and add a show more button (OlegAba)
- Private Zaps (William Casarin)
### Fixed
- Fix default zap amount setting not getting updated (William Casarin)
- Fix issue where keyboard covers custom zap comment (William Casarin)
[1.1.0-10]: https://github.com/damus-io/damus/releases/tag/v1.1.0-10
## [1.1.0-9] - 2023-02-26
### Added
- Customized zaps (William Casarin)
- Add new Notifications View (William Casarin)
- Bookmarking (Joel Klabo)
### Changed
- No more inline npubs when tagging users (Swift)
### Fixed
- Fix alignment of side menu labels (Joel Klabo)
- Fix duplicated participants in reply-to view (Joel Klabo)
- Load missing profiles in Zaps view (William Casarin)
- Fix memory leak with inline videos (William Casarin)
- Eliminate popping when scrolling (William Casarin)
[1.1.0-9]: https://github.com/damus-io/damus/releases/tag/v1.1.0-9
## [1.1.0-3] - 2023-02-20
### Added
@@ -610,4 +806,3 @@
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2
+150 -13
View File
@@ -135,6 +135,8 @@
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */; };
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C987B56283FD07F0042CE38 /* FollowersModel.swift */; };
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */; };
4C9BB83129C0ED4F00FC4E37 /* DisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */; };
4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */; };
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */; };
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */; };
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; };
@@ -170,6 +172,10 @@
4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF5297F1A6A00430951 /* EventBody.swift */; };
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */; };
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; };
4CCEB7A929B29DD50078AA28 /* SearchResultsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7A829B29DD50078AA28 /* SearchResultsModel.swift */; };
4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */; };
4CCEB7B029B5415A0078AA28 /* SearchingProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */; };
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; };
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */; };
4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */; };
@@ -216,26 +222,34 @@
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABED29844B5500D66079 /* AnyEncodable.swift */; };
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; };
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABF52985CD5500D66079 /* UserSearch.swift */; };
4CFF8F6329CC9AD7008DB934 /* ImageContextMenuModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */; };
4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6629CC9E3A008DB934 /* ImageView.swift */; };
4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */; };
4CFF8F6B29CD0079008DB934 /* RepostedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */; };
4CFF8F6D29CD022E008DB934 /* WideEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6C29CD022E008DB934 /* WideEventView.swift */; };
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; };
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF72FC129B9142F00124A13 /* ShareAction.swift */; };
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfileZoomView.swift */; };
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643EA5C7296B764E005081BB /* RelayFilterView.swift */; };
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; };
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FBD06E296255C400D9D3B2 /* Theme.swift */; };
6C7DE41F2955169800E66263 /* Vault in Frameworks */ = {isa = PBXBuildFile; productRef = 6C7DE41E2955169800E66263 /* Vault */; };
7C0F392F29B57CAF0039859C /* Binding+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C0F392E29B57CAF0039859C /* Binding+.swift */; };
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C60CAEE298471A1009C80D6 /* CoreSVG.swift */; };
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */; };
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */; };
7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CFF6316299FEFE5005D382A /* SelectableText.swift */; };
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; };
9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C83F89229A937B900136C08 /* TextViewWrapper.swift */; };
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; };
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; };
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; };
E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */; };
E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadView.swift */; };
F75BA12D29A1855400E10810 /* BookmarksManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75BA12C29A1855400E10810 /* BookmarksManager.swift */; };
F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75BA12E29A18EF500E10810 /* BookmarksView.swift */; };
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E91298B0F0700AB113A /* RelayDetailView.swift */; };
@@ -317,6 +331,12 @@
3AA247FE297E3D900090C62D /* RepostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostsView.swift; sourceTree = "<group>"; };
3AA24801297E3DC20090C62D /* RepostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostView.swift; sourceTree = "<group>"; };
3AA59D1C2999B0400061C48E /* DraftsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsModel.swift; sourceTree = "<group>"; };
3AA5E70229B682A5002701ED /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = "<group>"; };
3AA5E70329B682AD002701ED /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3AA5E70429B682B3002701ED /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3AA5E70529B9E83E002701ED /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3AA5E70629B9E844002701ED /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; };
3AA5E70729B9E84A002701ED /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = bg; path = bg.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3AAA95C9298DF87B00F3D526 /* TranslationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationService.swift; sourceTree = "<group>"; };
3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLPlan.swift; sourceTree = "<group>"; };
3AB5B86A2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@@ -329,6 +349,21 @@
3ACB685B297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3ACB685E297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = "<group>"; };
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeAgoTests.swift; sourceTree = "<group>"; };
3AD14EB529C40F38009D2D9C /* hu-HU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "hu-HU"; path = "hu-HU.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3AD14EB629C40F38009D2D9C /* hu-HU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hu-HU"; path = "hu-HU.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AD14EB729C40F38009D2D9C /* hu-HU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hu-HU"; path = "hu-HU.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AD14EB829C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "sv-SE"; path = "sv-SE.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3AD14EB929C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sv-SE"; path = "sv-SE.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AD14EBA29C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sv-SE"; path = "sv-SE.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AD14EBB29C40F47009D2D9C /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AD14EBC29C40F47009D2D9C /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-CA"; path = "fr-CA.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3AD14EBD29C40F47009D2D9C /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/Localizable.strings"; sourceTree = "<group>"; };
3AD5662B29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3AD5662C29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fa; path = fa.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3AD5662D29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = "<group>"; };
3AD5663129C0DA4B00BF77C5 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = "<group>"; };
3AD5663229C0DA4B00BF77C5 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ko; path = ko.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3AD5663329C0DA4B00BF77C5 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreTranslateServer.swift; sourceTree = "<group>"; };
3AEB8003297CCEA800713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "tr-TR"; path = "tr-TR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3AEB8004297CCEA800713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "tr-TR"; path = "tr-TR.lproj/Localizable.strings"; sourceTree = "<group>"; };
@@ -475,6 +510,8 @@
4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Tests.swift; sourceTree = "<group>"; };
4C987B56283FD07F0042CE38 /* FollowersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersModel.swift; sourceTree = "<group>"; };
4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatroomMetadata.swift; sourceTree = "<group>"; };
4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayName.swift; sourceTree = "<group>"; };
4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfileName.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>"; };
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
@@ -510,6 +547,10 @@
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>"; };
4CC7AAF9297F64AC00430951 /* EventMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMenu.swift; sourceTree = "<group>"; };
4CCEB7A829B29DD50078AA28 /* SearchResultsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsModel.swift; sourceTree = "<group>"; };
4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingEventView.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>"; };
4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.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>"; };
@@ -559,25 +600,33 @@
4CF0ABED29844B5500D66079 /* AnyEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = "<group>"; };
4CF0ABEF29857E9200D66079 /* Bech32Object.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Object.swift; sourceTree = "<group>"; };
4CF0ABF52985CD5500D66079 /* UserSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSearch.swift; sourceTree = "<group>"; };
4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContextMenuModifier.swift; sourceTree = "<group>"; };
4CFF8F6629CC9E3A008DB934 /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = "<group>"; };
4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContainerView.swift; sourceTree = "<group>"; };
4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostedEvent.swift; sourceTree = "<group>"; };
4CFF8F6C29CD022E008DB934 /* WideEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WideEventView.swift; sourceTree = "<group>"; };
4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; };
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; };
5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; };
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
5CF72FC129B9142F00124A13 /* ShareAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAction.swift; sourceTree = "<group>"; };
6439E013296790CF0020672B /* ProfileZoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZoomView.swift; sourceTree = "<group>"; };
643EA5C7296B764E005081BB /* RelayFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterView.swift; sourceTree = "<group>"; };
647D9A8C2968520300A295DE /* SideMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = "<group>"; };
64FBD06E296255C400D9D3B2 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
7C0F392E29B57CAF0039859C /* Binding+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+.swift"; sourceTree = "<group>"; };
7C60CAEE298471A1009C80D6 /* CoreSVG.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreSVG.swift; sourceTree = "<group>"; };
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomableScrollView.swift; sourceTree = "<group>"; };
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KFOptionSetter+.swift"; sourceTree = "<group>"; };
7CFF6316299FEFE5005D382A /* SelectableText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableText.swift; sourceTree = "<group>"; };
9609F057296E220800069BF3 /* BannerImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerImageView.swift; sourceTree = "<group>"; };
9C83F89229A937B900136C08 /* TextViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewWrapper.swift; sourceTree = "<group>"; };
9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachMediaUtility.swift; sourceTree = "<group>"; };
BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; };
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; };
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTests.swift; sourceTree = "<group>"; };
E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; };
E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadV2View.swift; sourceTree = "<group>"; };
E9E4ED0A295867B900DD7078 /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; };
F75BA12C29A1855400E10810 /* BookmarksManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksManager.swift; sourceTree = "<group>"; };
F75BA12E29A18EF500E10810 /* BookmarksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksView.swift; sourceTree = "<group>"; };
F7908E91298B0F0700AB113A /* RelayDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayDetailView.swift; sourceTree = "<group>"; };
@@ -627,6 +676,7 @@
isa = PBXGroup;
children = (
3AA24801297E3DC20090C62D /* RepostView.swift */,
4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */,
);
path = Reposts;
sourceTree = "<group>";
@@ -688,6 +738,7 @@
4C0A3F8D280F63FF000448DE /* Models */ = {
isa = PBXGroup;
children = (
4CCEB7A729B29DC90078AA28 /* Search */,
4C54AA0829A55416003E4487 /* Notifications */,
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */,
4C0A3F8E280F640A000448DE /* ThreadModel.swift */,
@@ -729,6 +780,7 @@
4CE8795A2996C47A00F758CC /* ZapsModel.swift */,
3AA59D1C2999B0400061C48E /* DraftsModel.swift */,
4C54AA0629A540BA003E4487 /* NotificationsModel.swift */,
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -756,6 +808,8 @@
4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup;
children = (
4CFF8F6129CC9A80008DB934 /* Images */,
4CCEB7AC29B53D180078AA28 /* Search */,
4C30AC7029A5676F00E2BD5A /* Notifications */,
4CE0E2B029A3DF4700DB4CA2 /* Timeline */,
4CE879562996C44A00F758CC /* Zaps */,
@@ -789,12 +843,9 @@
4C363A8D28236FE4006E126D /* NoteContentView.swift */,
4C75EFAC28049CFB0006080F /* PostButton.swift */,
4C75EFA327FA577B0006080F /* PostView.swift */,
9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */,
9C83F89229A937B900136C08 /* TextViewWrapper.swift */,
4CEE2AF8280B2EAC00AB5EEF /* PowView.swift */,
4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */,
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */,
4CEE2AF2280B25C500AB5EEF /* ProfilePicView.swift */,
4C8682862814DE470026224F /* ProfileView.swift */,
4C3AC7A42836987600E1F516 /* MainTabView.swift */,
4C363A8B28236B92006E126D /* PubkeyView.swift */,
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */,
@@ -805,7 +856,7 @@
4C363AA128296A7E006E126D /* SearchView.swift */,
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */,
4C3AC7A02835A81400E1F516 /* SetupView.swift */,
E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */,
E9E4ED0A295867B900DD7078 /* ThreadView.swift */,
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */,
4CB55EF4295E679D007FD187 /* UserRelaysView.swift */,
647D9A8C2968520300A295DE /* SideMenuView.swift */,
@@ -844,6 +895,7 @@
4C7FF7D628233637009601DB /* Util */ = {
isa = PBXGroup;
children = (
7C0F392D29B57C8F0039859C /* Extensions */,
4CE879492995B58700F758CC /* Relays */,
4CF0ABEA29844B2F00D66079 /* AnyCodable */,
4CC7AAE6297EFA7B00430951 /* Zap.swift */,
@@ -871,10 +923,10 @@
4CB883B5297730E400DC99E7 /* LNUrls.swift */,
3AB72AB8298ECF30004BB58C /* Translator.swift */,
4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */,
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */,
4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */,
3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */,
4C30AC7729A577AB00E2BD5A /* EventCache.swift */,
4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -899,6 +951,7 @@
children = (
4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */,
4CB88388296AF99A00DC99E7 /* EventDetailBar.swift */,
5CF72FC129B9142F00124A13 /* ShareAction.swift */,
);
path = ActionBar;
sourceTree = "<group>";
@@ -914,9 +967,14 @@
4CB9D4A52992D01900A9A7E4 /* Profile */ = {
isa = PBXGroup;
children = (
4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */,
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */,
4CEE2AF2280B25C500AB5EEF /* ProfilePicView.swift */,
4C8682862814DE470026224F /* ProfileView.swift */,
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */,
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */,
4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */,
4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */,
);
path = Profile;
sourceTree = "<group>";
@@ -934,10 +992,28 @@
4CF0ABE6298444FC00D66079 /* MutedEventView.swift */,
4C3D52B5298DB4E6001C5831 /* ZapEvent.swift */,
4C3D52B7298DB5C6001C5831 /* TextEvent.swift */,
4CFF8F6C29CD022E008DB934 /* WideEventView.swift */,
);
path = Events;
sourceTree = "<group>";
};
4CCEB7A729B29DC90078AA28 /* Search */ = {
isa = PBXGroup;
children = (
4CCEB7A829B29DD50078AA28 /* SearchResultsModel.swift */,
);
path = Search;
sourceTree = "<group>";
};
4CCEB7AC29B53D180078AA28 /* Search */ = {
isa = PBXGroup;
children = (
4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */,
4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */,
);
path = Search;
sourceTree = "<group>";
};
4CE0E2B029A3DF4700DB4CA2 /* Timeline */ = {
isa = PBXGroup;
children = (
@@ -1114,6 +1190,25 @@
path = Posting;
sourceTree = "<group>";
};
4CFF8F6129CC9A80008DB934 /* Images */ = {
isa = PBXGroup;
children = (
4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */,
4CFF8F6629CC9E3A008DB934 /* ImageView.swift */,
4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */,
);
path = Images;
sourceTree = "<group>";
};
7C0F392D29B57C8F0039859C /* Extensions */ = {
isa = PBXGroup;
children = (
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */,
7C0F392E29B57CAF0039859C /* Binding+.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
F7F0BA23297892AE009531F3 /* Modifiers */ = {
isa = PBXGroup;
children = (
@@ -1233,6 +1328,13 @@
ru,
"zh-HK",
"zh-TW",
uk,
bg,
fa,
ko,
"hu-HU",
"sv-SE",
"fr-CA",
);
mainGroup = 4CE6DEDA27F7A08100C66700;
packageReferences = (
@@ -1287,6 +1389,7 @@
buildActionMask = 2147483647;
files = (
4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */,
5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */,
4C363A8A28236B57006E126D /* MentionView.swift in Sources */,
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */,
4C30AC7829A577AB00E2BD5A /* EventCache.swift in Sources */,
@@ -1306,6 +1409,7 @@
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */,
4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */,
4CCEB7A929B29DD50078AA28 /* SearchResultsModel.swift in Sources */,
4C3EA67728FF7A9800C48A62 /* talstr.c in Sources */,
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */,
@@ -1342,6 +1446,7 @@
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */,
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */,
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */,
4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */,
4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */,
4C649844285A952100EAE2B3 /* LocalUserConfig.swift in Sources */,
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
@@ -1352,10 +1457,12 @@
4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */,
4C363A9A28283854006E126D /* Reply.swift in Sources */,
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */,
4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */,
4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */,
4CB8838B296F6E1E00DC99E7 /* NIP05Badge.swift in Sources */,
4C3EA66828FF5F9900C48A62 /* hex.c in Sources */,
E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */,
E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */,
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */,
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */,
9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */,
@@ -1373,6 +1480,7 @@
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */,
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */,
4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */,
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */,
@@ -1381,6 +1489,7 @@
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */,
4CCEB7B029B5415A0078AA28 /* SearchingProfileView.swift in Sources */,
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */,
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */,
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */,
@@ -1399,13 +1508,16 @@
4CE879582996C45300F758CC /* ZapsView.swift in Sources */,
4C30AC7429A5680900E2BD5A /* EventGroupView.swift in Sources */,
4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */,
4CFF8F6D29CD022E008DB934 /* WideEventView.swift in Sources */,
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */,
4C363A94282704FA006E126D /* Post.swift in Sources */,
4C216F32286E388800040376 /* DMChatView.swift in Sources */,
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */,
4CFF8F6329CC9AD7008DB934 /* ImageContextMenuModifier.swift in Sources */,
4C54AA0A29A55429003E4487 /* EventGroup.swift in Sources */,
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */,
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */,
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
@@ -1425,6 +1537,7 @@
4C3EA64F28FF59F200C48A62 /* tal.c in Sources */,
4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */,
4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */,
4CFF8F6B29CD0079008DB934 /* RepostedEvent.swift in Sources */,
4C8682872814DE470026224F /* ProfileView.swift in Sources */,
4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */,
4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */,
@@ -1459,7 +1572,9 @@
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
4C9BB83129C0ED4F00FC4E37 /* DisplayName.swift in Sources */,
7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */,
4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */,
4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */,
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */,
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
@@ -1491,6 +1606,7 @@
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
7C0F392F29B57CAF0039859C /* Binding+.swift in Sources */,
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */,
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
@@ -1567,6 +1683,13 @@
3A827A1A299FC69D00C4D171 /* ru */,
3A3040FB29A91F03008A0F29 /* zh-HK */,
3A3040FD29A91F31008A0F29 /* zh-TW */,
3AA5E70429B682B3002701ED /* uk */,
3AA5E70729B9E84A002701ED /* bg */,
3AD5662C29BD2F5300BF77C5 /* fa */,
3AD5663229C0DA4B00BF77C5 /* ko */,
3AD14EB529C40F38009D2D9C /* hu-HU */,
3AD14EB829C40F3F009D2D9C /* sv-SE */,
3AD14EBC29C40F47009D2D9C /* fr-CA */,
);
name = Localizable.stringsdict;
sourceTree = "<group>";
@@ -1592,6 +1715,13 @@
3A827A18299FC69D00C4D171 /* ru */,
3A3040F929A91ED6008A0F29 /* zh-HK */,
3A3040FC29A91F31008A0F29 /* zh-TW */,
3AA5E70329B682AD002701ED /* uk */,
3AA5E70529B9E83E002701ED /* bg */,
3AD5662B29BD2F5300BF77C5 /* fa */,
3AD5663329C0DA4B00BF77C5 /* ko */,
3AD14EB629C40F38009D2D9C /* hu-HU */,
3AD14EB929C40F3F009D2D9C /* sv-SE */,
3AD14EBB29C40F47009D2D9C /* fr-CA */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
@@ -1618,6 +1748,13 @@
3A3040FA29A91EFC008A0F29 /* zh-HK */,
3A3040FE29A91F31008A0F29 /* zh-TW */,
3A3040FF29AB02D1008A0F29 /* en-US */,
3AA5E70229B682A5002701ED /* uk */,
3AA5E70629B9E844002701ED /* bg */,
3AD5662D29BD2F5300BF77C5 /* fa */,
3AD5663129C0DA4B00BF77C5 /* ko */,
3AD14EB729C40F38009D2D9C /* hu-HU */,
3AD14EBA29C40F3F009D2D9C /* sv-SE */,
3AD14EBD29C40F47009D2D9C /* fr-CA */,
);
name = Localizable.strings;
sourceTree = "<group>";
@@ -1753,7 +1890,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1776,7 +1913,7 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.1.0;
MARKETING_VERSION = 1.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -1795,7 +1932,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1818,7 +1955,7 @@
"$(inherited)",
"$(PROJECT_DIR)",
);
MARKETING_VERSION = 1.1.0;
MARKETING_VERSION = 1.3.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
+1
View File
@@ -52,6 +52,7 @@ struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
.accentColor(tag == selection ? textColor() : .gray)
}
}
.background(Color(UIColor.systemBackground))
}
func textColor() -> Color {
+8 -162
View File
@@ -31,160 +31,7 @@ struct ShareSheet: UIViewControllerRepresentable {
}
}
struct ImageContextMenuModifier: ViewModifier {
let url: URL?
let image: UIImage?
@Binding var showShareSheet: Bool
func body(content: Content) -> some View {
return content.contextMenu {
Button {
UIPasteboard.general.url = url
} label: {
Label(NSLocalizedString("Copy Image URL", comment: "Context menu option to copy the URL of an image into clipboard."), systemImage: "doc.on.doc")
}
if let someImage = image {
Button {
UIPasteboard.general.image = someImage
} label: {
Label(NSLocalizedString("Copy Image", comment: "Context menu option to copy an image into clipboard."), systemImage: "photo.on.rectangle")
}
Button {
UIImageWriteToSavedPhotosAlbum(someImage, nil, nil, nil)
} label: {
Label(NSLocalizedString("Save Image", comment: "Context menu option to save an image."), systemImage: "square.and.arrow.down")
}
}
Button {
showShareSheet = true
} label: {
Label(NSLocalizedString("Share", comment: "Button to share an image."), systemImage: "square.and.arrow.up")
}
}
}
}
private struct ImageContainerView: View {
let url: URL?
@State private var image: UIImage?
@State private var showShareSheet = false
private struct ImageHandler: ImageModifier {
@Binding var handler: UIImage?
func modify(_ image: UIImage) -> UIImage {
handler = image
return image
}
}
var body: some View {
KFAnimatedImage(url)
.imageContext(.note)
.configure { view in
view.framePreloadCount = 3
}
.imageModifier(ImageHandler(handler: $image))
.clipped()
.modifier(ImageContextMenuModifier(url: url, image: image, showShareSheet: $showShareSheet))
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [url])
}
}
}
struct ImageView: View {
let urls: [URL?]
@Environment(\.presentationMode) var presentationMode
@State private var selectedIndex = 0
@State var showMenu = true
var navBarView: some View {
VStack {
HStack {
Text(urls[selectedIndex]?.lastPathComponent ?? "")
.bold()
Spacer()
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Image(systemName: "xmark")
})
}
.padding()
Divider()
.ignoresSafeArea()
}
.background(.regularMaterial)
}
var tabViewIndicator: some View {
HStack(spacing: 10) {
ForEach(urls.indices, id: \.self) { index in
Capsule()
.fill(index == selectedIndex ? Color(UIColor.label) : Color.secondary)
.frame(width: 7, height: 7)
}
}
.padding()
.background(.regularMaterial)
.clipShape(Capsule())
}
var body: some View {
ZStack {
Color(.systemBackground)
.ignoresSafeArea()
TabView(selection: $selectedIndex) {
ForEach(urls.indices, id: \.self) { index in
ZoomableScrollView {
ImageContainerView(url: urls[index])
.aspectRatio(contentMode: .fit)
.padding(.top, Theme.safeAreaInsets?.top)
.padding(.bottom, Theme.safeAreaInsets?.bottom)
}
.modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
presentationMode.wrappedValue.dismiss()
}))
.ignoresSafeArea()
.tag(index)
}
}
.ignoresSafeArea()
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.gesture(TapGesture(count: 2).onEnded {
// Prevents menu from hiding on double tap
})
.gesture(TapGesture(count: 1).onEnded {
showMenu.toggle()
})
.overlay(
VStack {
if showMenu {
navBarView
Spacer()
if (urls.count > 1) {
tabViewIndicator
}
}
}
.animation(.easeInOut, value: showMenu)
.padding(.bottom, Theme.safeAreaInsets?.bottom)
)
}
}
}
struct ImageCarousel: View {
var urls: [URL]
@@ -204,25 +51,24 @@ struct ImageCarousel: View {
.configure { view in
view.framePreloadCount = 3
}
.aspectRatio(contentMode: .fit)
.cornerRadius(10)
.aspectRatio(contentMode: .fill)
//.cornerRadius(10)
.tabItem {
Text(url.absoluteString)
}
.id(url.absoluteString)
.contextMenu {
Button(NSLocalizedString("Copy Image", comment: "Context menu option to copy an image to clipboard.")) {
UIPasteboard.general.string = url.absoluteString
}
}
// .contextMenu {
// Button(NSLocalizedString("Copy Image", comment: "Context menu option to copy an image to clipboard.")) {
// UIPasteboard.general.string = url.absoluteString
// }
// }
}
}
}
.fullScreenCover(isPresented: $open_sheet) {
ImageView(urls: urls)
}
.frame(height: 200)
.clipped()
.frame(height: 350)
.onTapGesture {
open_sheet = true
}
-2
View File
@@ -15,12 +15,10 @@ struct Reposted: View {
var body: some View {
HStack(alignment: .center) {
Image(systemName: "arrow.2.squarepath")
.font(.footnote)
.foregroundColor(Color.gray)
ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true, show_nip5_domain: false)
.foregroundColor(Color.gray)
Text("Reposted", comment: "Text indicating that the post was reposted (i.e. re-shared).")
.font(.footnote)
.foregroundColor(Color.gray)
}
}
+4
View File
@@ -24,6 +24,7 @@ struct SelectableText: View {
fixedWidth: selectedTextWidth,
height: $selectedTextHeight
)
.padding([.leading, .trailing], -1.0)
.onAppear {
self.selectedTextWidth = geo.size.width
}
@@ -49,8 +50,11 @@ struct SelectableText: View {
view.isEditable = false
view.dataDetectorTypes = .all
view.isSelectable = true
view.backgroundColor = .clear
view.textContainer.lineFragmentPadding = 0
view.textContainerInset = .zero
view.textContainerInset.left = 1.0
view.textContainerInset.right = 1.0
return view
}
+24 -19
View File
@@ -68,24 +68,28 @@ struct ZapButton: View {
var body: some View {
HStack(spacing: 4) {
Image(systemName: zap_img)
.foregroundColor(zap_color == nil ? Color.gray : zap_color!)
.font(.footnote.weight(.medium))
.onTapGesture {
if bar.zapped {
//notify(.delete, bar.our_tip)
} else if !zapping {
self.showing_zap_customizer = true
//send_zap(damus_state: damus_state, event: event, lnurl: lnurl, is_custom: false)
//self.zapping = true
}
Button(action: {
}, label: {
Image(systemName: zap_img)
.foregroundColor(zap_color == nil ? Color.gray : zap_color!)
.font(.footnote.weight(.medium))
})
.simultaneousGesture(LongPressGesture().onEnded {_ in
guard !zapping else {
return
}
.onLongPressGesture(minimumDuration: 0, pressing: { is_charing in
self.is_charging = is_charging
}, perform: {
self.showing_zap_customizer = true
})
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
self.showing_zap_customizer = true
})
.highPriorityGesture(TapGesture().onEnded {_ in
guard !zapping else {
return
}
send_zap(damus_state: damus_state, event: event, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: ZapType.pub)
self.zapping = true
})
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
if bar.zap_total > 0 {
Text(verbatim: format_msats_abbrev(bar.zap_total))
@@ -138,7 +142,7 @@ struct ZapButton_Previews: PreviewProvider {
func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) {
guard let privkey = damus_state.keypair.privkey else {
guard let keypair = damus_state.keypair.to_full() else {
return
}
@@ -146,7 +150,8 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
let relays = Array(damus_state.pool.descriptors.prefix(10))
let target = ZapTarget.note(id: event.id, author: event.pubkey)
let content = comment ?? ""
let zapreq = make_zap_request_event(pubkey: damus_state.pubkey, privkey: privkey, content: content, relays: relays, target: target, is_anon: zap_type == .anon)
let zapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type)
Task {
var mpayreq = damus_state.lnurls.lookup(target.pubkey)
+170 -94
View File
@@ -11,11 +11,8 @@ import Starscream
var BOOTSTRAP_RELAYS = [
"wss://relay.damus.io",
"wss://eden.nostr.land",
"wss://relay.snort.social",
"wss://offchain.pub",
"wss://nostr.wine",
"wss://nos.lol",
"wss://relay.current.fyi",
"wss://brb.io",
]
struct TimestampedProfile {
@@ -81,7 +78,7 @@ struct ContentView: View {
@State var event: NostrEvent? = nil
@State var active_profile: String? = nil
@State var active_search: NostrFilter? = nil
@State var active_event_id: String? = nil
@State var active_event: NostrEvent? = nil
@State var profile_open: Bool = false
@State var thread_open: Bool = false
@State var search_open: Bool = false
@@ -89,6 +86,7 @@ struct ContentView: View {
@State var confirm_block: Bool = false
@State var user_blocked_confirm: Bool = false
@State var confirm_overwrite_mutelist: Bool = false
@State var current_boost: NostrEvent? = nil
@State var filter_state : FilterState = .posts_and_replies
@State private var isSideBarOpened = false
@StateObject var home: HomeModel = HomeModel()
@@ -147,27 +145,23 @@ struct ContentView: View {
search_open = false
isSideBarOpened = false
}
var timelineNavItem: some View {
VStack {
switch selected_timeline {
case .home:
Image("damus-home")
.resizable()
.frame(width:30,height:30)
.shadow(color: Color("DamusPurple"), radius: 2)
case .dms:
Text("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
.bold()
case .notifications:
Text("Notifications", comment: "Toolbar label for Notifications view.")
.bold()
case .search:
Text("Universe 🛸", comment: "Toolbar label for the universal view where posts from all connected relay servers appear.")
.bold()
case .none:
Text(verbatim: "")
}
var timelineNavItem: Text {
switch selected_timeline {
case .home:
return Text("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.")
.bold()
case .dms:
return Text("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
.bold()
case .notifications:
return Text("Notifications", comment: "Toolbar label for Notifications view.")
.bold()
case .search:
return Text("Universe 🛸", comment: "Toolbar label for the universal view where posts from all connected relay servers appear.")
.bold()
case .none:
return Text(verbatim: "")
}
}
@@ -176,24 +170,31 @@ struct ContentView: View {
NavigationLink(destination: MaybeProfileView, isActive: $profile_open) {
EmptyView()
}
NavigationLink(destination: MaybeThreadView, isActive: $thread_open) {
EmptyView()
if let active_event {
let thread = ThreadModel(event: active_event, damus_state: damus_state!)
NavigationLink(destination: ThreadView(state: damus_state!, thread: thread), isActive: $thread_open) {
EmptyView()
}
}
NavigationLink(destination: MaybeSearchView, isActive: $search_open) {
EmptyView()
}
switch selected_timeline {
case .search:
SearchHomeView(damus_state: damus_state!, model: SearchHomeModel(damus_state: damus_state!))
if #available(iOS 16.0, *) {
SearchHomeView(damus_state: damus_state!, model: SearchHomeModel(damus_state: damus_state!))
.scrollDismissesKeyboard(.immediately)
} else {
// Fallback on earlier versions
SearchHomeView(damus_state: damus_state!, model: SearchHomeModel(damus_state: damus_state!))
}
case .home:
PostingTimelineView
case .notifications:
VStack(spacing: 0) {
Divider()
NotificationsView(state: damus, notifications: home.notifications)
}
NotificationsView(state: damus, notifications: home.notifications)
case .dms:
DirectMessagesView(damus_state: damus_state!)
.environmentObject(home.dms)
@@ -202,15 +203,25 @@ struct ContentView: View {
EmptyView()
}
}
.navigationBarTitle(selected_timeline == .home ? NSLocalizedString("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.") : NSLocalizedString("Universe 🛸", comment: "Navigation bar title for universal view where posts from all connected relay servers appear."), displayMode: .inline)
.navigationBarTitle(timeline_name(selected_timeline), displayMode: .inline)
.toolbar {
ToolbarItem(placement: .principal) {
timelineNavItem
.opacity(isSideBarOpened ? 0 : 1)
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
VStack {
if selected_timeline == .home {
Image("damus-home")
.resizable()
.frame(width:30,height:30)
.shadow(color: Color("DamusPurple"), radius: 2)
.opacity(isSideBarOpened ? 0 : 1)
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
} else {
timelineNavItem
.opacity(isSideBarOpened ? 0 : 1)
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
}
}
}
}
.ignoresSafeArea(.keyboard)
}
var MaybeSearchView: some View {
@@ -223,16 +234,6 @@ struct ContentView: View {
}
}
var MaybeThreadView: some View {
Group {
if let evid = self.active_event_id {
BuildThreadV2View(damus: damus_state!, event_id: evid)
} else {
EmptyView()
}
}
}
var MaybeProfileView: some View {
Group {
if let pk = self.active_profile {
@@ -263,49 +264,47 @@ struct ContentView: View {
VStack(alignment: .leading, spacing: 0) {
if let damus = self.damus_state {
NavigationView {
ZStack {
TabView { // Prevents navbar appearance change on scroll
MainContent(damus: damus)
.toolbar() {
ToolbarItem(placement: .navigationBarLeading) {
Button {
isSideBarOpened.toggle()
} label: {
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles)
.opacity(isSideBarOpened ? 0 : 1)
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
}
.disabled(isSideBarOpened)
TabView { // Prevents navbar appearance change on scroll
MainContent(damus: damus)
.toolbar() {
ToolbarItem(placement: .navigationBarLeading) {
Button {
isSideBarOpened.toggle()
} label: {
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles)
.opacity(isSideBarOpened ? 0 : 1)
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
}
ToolbarItem(placement: .navigationBarTrailing) {
HStack(alignment: .center) {
if home.signal.signal != home.signal.max_signal {
NavigationLink(destination: RelayConfigView(state: damus_state!)) {
Text("\(home.signal.signal)/\(home.signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
.font(.callout)
.foregroundColor(.gray)
}
.disabled(isSideBarOpened)
}
ToolbarItem(placement: .navigationBarTrailing) {
HStack(alignment: .center) {
if home.signal.signal != home.signal.max_signal {
NavigationLink(destination: RelayConfigView(state: damus_state!)) {
Text("\(home.signal.signal)/\(home.signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
.font(.callout)
.foregroundColor(.gray)
}
// maybe expand this to other timelines in the future
if selected_timeline == .search {
Button(action: {
//isFilterVisible.toggle()
self.active_sheet = .filter
}) {
// checklist, checklist.checked, lisdt.bullet, list.bullet.circle, line.3.horizontal.decrease..., line.3.horizontail.decrease
Label(NSLocalizedString("Filter", comment: "Button label text for filtering relay servers."), systemImage: "line.3.horizontal.decrease")
.foregroundColor(.gray)
//.contentShape(Rectangle())
}
}
// maybe expand this to other timelines in the future
if selected_timeline == .search {
Button(action: {
//isFilterVisible.toggle()
self.active_sheet = .filter
}) {
// checklist, checklist.checked, lisdt.bullet, list.bullet.circle, line.3.horizontal.decrease..., line.3.horizontail.decrease
Label(NSLocalizedString("Filter", comment: "Button label text for filtering relay servers."), systemImage: "line.3.horizontal.decrease")
.foregroundColor(.gray)
//.contentShape(Rectangle())
}
}
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
.overlay(
SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened.animation())
)
@@ -314,8 +313,10 @@ struct ContentView: View {
TabBar(new_events: $home.new_events, selected: $selected_timeline, isSidebarVisible: $isSideBarOpened, action: switch_timeline)
.padding([.bottom], 8)
.background(Color(uiColor: .systemBackground).ignoresSafeArea())
}
}
.ignoresSafeArea(.keyboard)
.onAppear() {
self.connect()
setup_notifications()
@@ -352,7 +353,11 @@ struct ContentView: View {
active_profile = ref.ref_id
profile_open = true
} else if ref.key == "e" {
active_event_id = ref.ref_id
find_event(state: damus_state!, evid: ref.ref_id, search_type: .event, find_from: nil) { ev in
if let ev {
active_event = ev
}
}
thread_open = true
}
case .filter(let filt):
@@ -364,12 +369,7 @@ struct ContentView: View {
}
.onReceive(handle_notify(.boost)) { notif in
guard let privkey = self.privkey else {
return
}
let ev = notif.object as! NostrEvent
let boost = make_boost_event(pubkey: pubkey, privkey: privkey, boosted: ev)
self.damus_state?.pool.send(.event(boost))
current_boost = (notif.object as? NostrEvent)
}
.onReceive(handle_notify(.open_thread)) { obj in
//let ev = obj.object as! NostrEvent
@@ -486,7 +486,7 @@ struct ContentView: View {
}, message: {
if let pubkey = self.blocking {
let profile = damus_state!.profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey).username
Text("\(name) has been blocked", comment: "Alert message that informs a user was blocked.")
} else {
Text("User has been blocked", comment: "Alert message that informs a user was blocked.")
@@ -554,12 +554,22 @@ struct ContentView: View {
}, message: {
if let pubkey = blocking {
let profile = damus_state?.profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey).username
Text("Block \(name)?", comment: "Alert message prompt to ask if a user should be blocked.")
} else {
Text("Could not find user to block...", comment: "Alert message to indicate that the blocked user could not be found.")
}
})
.alert(NSLocalizedString("Repost", comment: "Title of alert for confirming to repost a post."), isPresented: $current_boost.mappedToBool()) {
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of reposting a post.")) {
current_boost = nil
}
Button(NSLocalizedString("Repost", comment: "Button to confirm reposting a post.")) {
self.damus_state?.pool.send(.event(current_boost!))
}
} message: {
Text("Are you sure you want to repost this?", comment: "Alert message to ask if user wants to repost a post.")
}
}
func switch_timeline(_ timeline: Timeline) {
@@ -616,7 +626,8 @@ struct ContentView: View {
relay_filters: relay_filters,
relay_metadata: metadatas,
drafts: Drafts(),
events: EventCache()
events: EventCache(),
bookmarks: BookmarksManager(pubkey: pubkey)
)
home.damus_state = self.damus_state!
@@ -765,3 +776,68 @@ func setup_notifications() {
}
}
func find_event(state: DamusState, evid: String, search_type: SearchType, find_from: [String]?, callback: @escaping (NostrEvent?) -> ()) {
if let ev = state.events.lookup(evid) {
callback(ev)
return
}
let subid = UUID().description
var has_event = false
var filter = search_type == .event ? NostrFilter.filter_ids([ evid ]) : NostrFilter.filter_authors([ evid ])
if search_type == .profile {
filter.kinds = [0]
}
filter.limit = 1
var attempts = 0
state.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in
guard case .nostr_event(let ev) = res else {
return
}
guard ev.subid == subid else {
return
}
switch ev {
case .event(_, let ev):
has_event = true
callback(ev)
state.pool.unsubscribe(sub_id: subid)
case .eose:
if !has_event {
attempts += 1
if attempts == state.pool.descriptors.count / 2 {
callback(nil)
}
state.pool.unsubscribe(sub_id: subid, to: [relay_id])
}
case .notice(_):
break
}
}
}
func timeline_name(_ timeline: Timeline?) -> String {
guard let timeline else {
return ""
}
switch timeline {
case .home:
return "Home"
case .notifications:
return "Notifications"
case .search:
return "Universe 🛸"
case .dms:
return "DMs"
}
}
+10
View File
@@ -14,6 +14,16 @@
<string>nostr</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLName</key>
<string>io.damus</string>
<key>CFBundleURLSchemes</key>
<array>
<string>damus</string>
</array>
</dict>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
+41 -20
View File
@@ -7,44 +7,65 @@
import Foundation
class BookmarksManager {
fileprivate func get_bookmarks_key(pubkey: String) -> String {
pk_setting_key(pubkey, key: "bookmarks")
}
func load_bookmarks(pubkey: String) -> [NostrEvent] {
let key = get_bookmarks_key(pubkey: pubkey)
return (UserDefaults.standard.stringArray(forKey: key) ?? []).compactMap {
event_from_json(dat: $0)
}
}
func save_bookmarks(pubkey: String, current_value: [NostrEvent], value: [NostrEvent]) -> Bool {
let uniq_bookmarks = Array(Set(value))
if uniq_bookmarks != current_value {
let encoded = uniq_bookmarks.map(event_to_json)
UserDefaults.standard.set(encoded, forKey: get_bookmarks_key(pubkey: pubkey))
return true
}
return false
}
class BookmarksManager: ObservableObject {
private let userDefaults = UserDefaults.standard
private let pubkey: String
init(pubkey: String) {
self.pubkey = pubkey
}
var bookmarks: [String] {
private var _bookmarks: [NostrEvent]
var bookmarks: [NostrEvent] {
get {
return userDefaults.stringArray(forKey: storageKey()) ?? []
return _bookmarks
}
set {
let uniqueBookmarks = Array(Set(newValue))
if uniqueBookmarks != bookmarks {
userDefaults.set(uniqueBookmarks, forKey: storageKey())
if save_bookmarks(pubkey: pubkey, current_value: _bookmarks, value: newValue) {
self._bookmarks = newValue
self.objectWillChange.send()
}
}
}
func isBookmarked(_ string: String) -> Bool {
return bookmarks.contains(string)
init(pubkey: String) {
self._bookmarks = load_bookmarks(pubkey: pubkey)
self.pubkey = pubkey
}
func updateBookmark(_ string: String) {
if isBookmarked(string) {
bookmarks = bookmarks.filter { $0 != string }
func isBookmarked(_ ev: NostrEvent) -> Bool {
return bookmarks.contains(ev)
}
func updateBookmark(_ ev: NostrEvent) {
if isBookmarked(ev) {
bookmarks = bookmarks.filter { $0 != ev }
} else {
bookmarks.append(string)
bookmarks.append(ev)
}
}
func clearAll() {
bookmarks = []
}
private func storageKey() -> String {
pk_setting_key(pubkey, key: "bookmarks")
}
}
+2 -1
View File
@@ -25,6 +25,7 @@ struct DamusState {
let relay_metadata: RelayMetadatas
let drafts: Drafts
let events: EventCache
let bookmarks: BookmarksManager
var pubkey: String {
return keypair.pubkey
@@ -35,6 +36,6 @@ struct 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())
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: ""))
}
}
+42 -11
View File
@@ -130,33 +130,38 @@ class HomeModel: ObservableObject {
}
}
func handle_zap_event_with_zapper(_ ev: NostrEvent, our_pubkey: String, zapper: String) {
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper) else {
func handle_zap_event_with_zapper(_ ev: NostrEvent, our_keypair: Keypair, zapper: String) {
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
return
}
damus_state.zaps.add_zap(zap: zap)
guard zap.target.pubkey == our_pubkey else {
guard zap.target.pubkey == our_keypair.pubkey else {
return
}
if !notifications.insert_zap(zap) {
return
}
handle_last_event(ev: ev, timeline: .notifications)
if handle_last_event(ev: ev, timeline: .notifications) && damus_state.settings.zap_vibration {
// Generate zap vibration
zap_vibrate(zap_amount: zap.invoice.amount)
}
return
}
func handle_zap_event(_ ev: NostrEvent) {
// These are zap notifications
guard let ptag = event_tag(ev, name: "p") else {
return
}
let our_keypair = damus_state.keypair
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) {
handle_zap_event_with_zapper(ev, our_pubkey: damus_state.pubkey, zapper: local_zapper)
handle_zap_event_with_zapper(ev, our_keypair: our_keypair, zapper: local_zapper)
return
}
@@ -175,7 +180,7 @@ class HomeModel: ObservableObject {
DispatchQueue.main.async {
self.damus_state.profiles.zappers[ptag] = zapper
self.handle_zap_event_with_zapper(ev, our_pubkey: self.damus_state.pubkey, zapper: zapper)
self.handle_zap_event_with_zapper(ev, our_keypair: our_keypair, zapper: zapper)
}
}
@@ -386,7 +391,7 @@ class HomeModel: ObservableObject {
NostrKind.zap.rawValue,
])
notifications_filter.pubkeys = [damus_state.pubkey]
notifications_filter.limit = 100
notifications_filter.limit = 500
var home_filters = [home_filter]
var notifications_filters = [notifications_filter]
@@ -452,10 +457,19 @@ class HomeModel: ObservableObject {
}
func handle_notification(ev: NostrEvent) {
// don't show notifications from ourselves
guard ev.pubkey != damus_state.pubkey else {
return
}
guard event_has_our_pubkey(ev, our_pubkey: self.damus_state.pubkey) else {
return
}
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
return
}
damus_state.events.insert(ev)
if let inner_ev = ev.inner_event {
damus_state.events.insert(inner_ev)
@@ -467,10 +481,14 @@ class HomeModel: ObservableObject {
handle_last_event(ev: ev, timeline: .notifications)
}
func handle_last_event(ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) {
@discardableResult
func handle_last_event(ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) -> Bool {
if let new_bits = handle_last_events(new_events: self.new_events, ev: ev, timeline: timeline, shouldNotify: shouldNotify) {
new_events = new_bits
return true
} else {
return false
}
}
@@ -897,3 +915,16 @@ func should_show_event(contacts: Contacts, ev: NostrEvent) -> Bool {
return ev.should_show_event
}
func zap_vibrate(zap_amount: Int64) {
let sats = zap_amount / 1000
var vibration_generator: UIImpactFeedbackGenerator
if sats >= 10000 {
vibration_generator = UIImpactFeedbackGenerator(style: .heavy)
} else if sats >= 1000 {
vibration_generator = UIImpactFeedbackGenerator(style: .medium)
} else {
vibration_generator = UIImpactFeedbackGenerator(style: .light)
}
vibration_generator.impactOccurred()
}
+28
View File
@@ -0,0 +1,28 @@
//
// ImageUploadModel.swift
// damus
//
// Created by William Casarin on 2023-03-16.
//
import Foundation
import UIKit
class ImageUploadModel: NSObject, URLSessionTaskDelegate, ObservableObject {
@Published var progress: Double? = nil
func start(img: UIImage, uploader: ImageUploader) async -> ImageUploadResult {
let res = await create_image_upload_request(imageToUpload: img, imageUploader: uploader, progress: self)
DispatchQueue.main.async {
self.progress = nil
}
return res
}
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
DispatchQueue.main.async {
self.progress = Double(totalBytesSent) / Double(totalBytesExpectedToSend)
}
}
}
+10 -2
View File
@@ -94,6 +94,14 @@ enum Block {
return nil
}
var is_note_mention: Bool {
guard case .mention(let mention) = self else {
return false
}
return mention.type == .event
}
var is_mention: Bool {
if case .mention = self {
return true
@@ -274,8 +282,8 @@ func format_msats(_ msat: Int64, locale: Locale = Locale.current) -> String {
let sats = NSNumber(value: (Double(msat) / 1000.0))
let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue
let bundle = bundleForLocale(locale: locale)
return String(format: bundle.localizedString(forKey: "sats_count", value: nil, table: nil), locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats)
let format = localizedStringFormat(key: "sats_count", locale: locale)
return String(format: format, locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats)
}
func convert_invoice_block(_ b: invoice_block) -> Block? {
+7 -1
View File
@@ -21,7 +21,13 @@ class ZapGroup {
}
func zap_requests() -> [NostrEvent] {
zaps.map { z in z.request.ev }
zaps.map { z in
if let priv = z.private_request {
return priv
} else {
return z.request.ev
}
}
}
init(zaps: [Zap]) {
+27 -1
View File
@@ -14,6 +14,28 @@ enum NotificationItem {
case event_zap(String, ZapGroup)
case reply(NostrEvent)
var is_reply: NostrEvent? {
if case .reply(let ev) = self {
return ev
}
return nil
}
var is_zap: ZapGroup? {
switch self {
case .profile_zap(let zapgrp):
return zapgrp
case .event_zap(_, let zapgrp):
return zapgrp
case .reaction:
return nil
case .reply:
return nil
case .repost:
return nil
}
}
var id: String {
switch self {
case .repost(let evid, _):
@@ -45,7 +67,7 @@ enum NotificationItem {
}
}
class NotificationsModel: ObservableObject {
class NotificationsModel: ObservableObject, ScrollQueue {
var incoming_zaps: [Zap]
var incoming_events: [NostrEvent]
var should_queue: Bool
@@ -73,6 +95,10 @@ class NotificationsModel: ObservableObject {
self.notifications = []
}
func set_should_queue(_ val: Bool) {
self.should_queue = val
}
func uniq_pubkeys() -> [String] {
var pks = Set<String>()
+7 -4
View File
@@ -12,10 +12,12 @@ class ProfileModel: ObservableObject, Equatable {
@Published var contacts: NostrEvent? = nil
@Published var following: Int = 0
@Published var relays: [String: RelayInfo]? = nil
@Published var progress: Int = 0
let pubkey: String
let damus: DamusState
var seen_event: Set<String> = Set()
var sub_id = UUID().description
var prof_subid = UUID().description
@@ -127,15 +129,16 @@ class ProfileModel: ObservableObject, Equatable {
case .ws_event:
return
case .nostr_event(let resp):
guard resp.subid == self.sub_id || resp.subid == self.prof_subid else {
return
}
switch resp {
case .event(let sid, let ev):
if sid != self.sub_id && sid != self.prof_subid {
return
}
case .event(_, let ev):
add_event(ev)
case .notice(let notice):
notify(.notice, notice)
case .eose:
progress += 1
break
}
}
+17 -4
View File
@@ -8,12 +8,25 @@
import Foundation
class ReplyMap {
var replies: [String: String] = [:]
var replies: [String: Set<String>] = [:]
func lookup(_ id: String) -> String? {
func lookup(_ id: String) -> Set<String>? {
return replies[id]
}
func add(id: String, reply_id: String) {
replies[id] = reply_id
private func ensure_set(id: String) {
if replies[id] == nil {
replies[id] = Set()
}
}
@discardableResult
func add(id: String, reply_id: String) -> Bool {
ensure_set(id: id)
if (replies[id]!).contains(reply_id) {
return false
}
replies[id]!.insert(reply_id)
return true
}
}
@@ -0,0 +1,12 @@
//
// SearchResultsModel.swift
// damus
//
// Created by William Casarin on 2023-03-03.
//
import Foundation
class SearchResultsModel: ObservableObject {
}
+59 -123
View File
@@ -30,162 +30,100 @@ enum InitialEvent {
/// manages the lifetime of a thread
class ThreadModel: ObservableObject {
@Published var initial_event: InitialEvent
@Published var events: [NostrEvent] = []
@Published var event_map: [String: Int] = [:]
@Published var event: NostrEvent
var event_map: Set<NostrEvent>
@Published var loading: Bool = false
var replies: ReplyMap = ReplyMap()
var event: NostrEvent? {
switch initial_event {
case .event(let ev):
return ev
case .event_id(let evid):
for event in events {
if event.id == evid {
return event
}
}
return nil
}
init(event: NostrEvent, damus_state: DamusState) {
self.damus_state = damus_state
self.event_map = Set()
self.event = event
add_event(event, privkey: nil)
}
let damus_state: DamusState
let profiles_subid = UUID().description
var base_subid = UUID().description
init(evid: String, damus_state: DamusState) {
self.damus_state = damus_state
self.initial_event = .event_id(evid)
}
let base_subid = UUID().description
let meta_subid = UUID().description
init(event: NostrEvent, damus_state: DamusState) {
self.damus_state = damus_state
self.initial_event = .event(event)
var subids: [String] {
return [profiles_subid, base_subid, meta_subid]
}
func unsubscribe() {
self.damus_state.pool.remove_handler(sub_id: base_subid)
self.damus_state.pool.remove_handler(sub_id: meta_subid)
self.damus_state.pool.remove_handler(sub_id: profiles_subid)
self.damus_state.pool.unsubscribe(sub_id: base_subid)
print("unsubscribing from thread \(initial_event.id) with sub_id \(base_subid)")
self.damus_state.pool.unsubscribe(sub_id: meta_subid)
self.damus_state.pool.unsubscribe(sub_id: profiles_subid)
print("unsubscribing from thread \(event.id) with sub_id \(base_subid)")
}
func reset_events() {
self.events.removeAll()
self.event_map.removeAll()
self.replies.replies.removeAll()
}
func should_resubscribe(_ ev_b: NostrEvent) -> Bool {
if self.events.count == 0 {
return true
}
@discardableResult
func set_active_event(_ ev: NostrEvent, privkey: String?) -> Bool {
self.event = ev
add_event(ev, privkey: privkey)
if ev_b.is_root_event() {
return false
}
// rough heuristic to save us from resubscribing all the time
//return ev_b.count_ids() != self.event.count_ids()
return true
}
func set_active_event(_ ev: NostrEvent, privkey: String?) {
if should_resubscribe(ev) {
unsubscribe()
self.initial_event = .event(ev)
subscribe()
} else {
self.initial_event = .event(ev)
if events.count == 0 {
add_event(ev, privkey: privkey)
}
}
//self.objectWillChange.send()
return false
}
func subscribe() {
var meta_events = NostrFilter()
var event_filter = NostrFilter()
var ref_events = NostrFilter()
var events_filter = NostrFilter()
//var likes_filter = NostrFilter.filter_kinds(7])
// TODO: add referenced relays
switch self.initial_event {
case .event(let ev):
ref_events.referenced_ids = ev.referenced_ids.map { $0.ref_id }
ref_events.referenced_ids?.append(ev.id)
ref_events.limit = 50
events_filter.ids = ref_events.referenced_ids ?? []
events_filter.limit = 100
events_filter.ids?.append(ev.id)
case .event_id(let evid):
ref_events.referenced_ids = [evid]
ref_events.limit = 50
events_filter.ids = [evid]
events_filter.limit = 100
let thread_id = event.thread_id(privkey: nil)
ref_events.referenced_ids = [thread_id, event.id]
ref_events.kinds = [1]
ref_events.limit = 1000
event_filter.ids = [thread_id, event.id]
meta_events.referenced_ids = [event.id]
meta_events.kinds = [9735, 1, 6, 7]
meta_events.limit = 1000
/*
if let last_ev = self.events.last {
if last_ev.created_at <= Int64(Date().timeIntervalSince1970) {
ref_events.since = last_ev.created_at
}
}
*/
let base_filters = [event_filter, ref_events]
let meta_filters = [meta_events]
//likes_filter.ids = ref_events.referenced_ids!
print("subscribing to thread \(initial_event.id) with sub_id \(base_subid)")
damus_state.pool.register_handler(sub_id: base_subid, handler: handle_event)
print("subscribing to thread \(event.id) with sub_id \(base_subid)")
loading = true
damus_state.pool.send(.subscribe(.init(filters: [ref_events, events_filter], sub_id: base_subid)))
}
func lookup(_ event_id: String) -> NostrEvent? {
if let i = event_map[event_id] {
return events[i]
}
return nil
damus_state.pool.subscribe(sub_id: base_subid, filters: base_filters, handler: handle_event)
damus_state.pool.subscribe(sub_id: meta_subid, filters: meta_filters, handler: handle_event)
}
func add_event(_ ev: NostrEvent, privkey: String?) {
guard ev.should_show_event else {
if event_map.contains(ev) {
return
}
if event_map[ev.id] != nil {
return
}
let the_ev = damus_state.events.upsert(ev)
damus_state.events.add_replies(ev: the_ev)
for reply in ev.direct_replies(privkey) {
self.replies.add(id: ev.id, reply_id: reply.ref_id)
}
if insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at < $1.created_at }) {
objectWillChange.send()
}
//self.events.append(ev)
//self.events = self.events.sorted { $0.created_at < $1.created_at }
var i: Int = 0
for ev in events {
self.event_map[ev.id] = i
i += 1
}
if let evid = self.initial_event.is_event_id {
if ev.id == evid {
// this should trigger a resubscribe...
set_active_event(ev, privkey: privkey)
}
}
}
func handle_channel_meta(_ ev: NostrEvent) {
guard let meta: ChatroomMetadata = decode_json(ev.content) else {
return
}
notify(.chatroom_meta, meta)
event_map.insert(ev)
objectWillChange.send()
}
func handle_event(relay_id: String, ev: NostrConnectionEvent) {
let (sub_id, done) = handle_subid_event(pool: damus_state.pool, relay_id: relay_id, ev: ev) { sid, ev in
guard sid == base_subid || sid == profiles_subid else {
guard subids.contains(sid) else {
return
}
@@ -193,21 +131,19 @@ class ThreadModel: ObservableObject {
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
} else if ev.is_textlike {
self.add_event(ev, privkey: self.damus_state.keypair.privkey)
} else if ev.known_kind == .channel_meta || ev.known_kind == .channel_create {
handle_channel_meta(ev)
}
}
guard done && (sub_id == base_subid || sub_id == profiles_subid) else {
guard done, let sub_id, subids.contains(sub_id) else {
return
}
if (events.contains { ev in ev.id == initial_event.id }) {
if event_map.contains(event) {
loading = false
}
if sub_id == self.base_subid {
load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: damus_state)
load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(Array(event_map)), damus_state: damus_state)
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ enum TranslationService: String, CaseIterable, Identifiable {
var model: Model {
switch self {
case .none:
return .init(tag: self.rawValue, displayName: NSLocalizedString("None", comment: "Dropdown option for selecting no translation service."))
return .init(tag: self.rawValue, displayName: NSLocalizedString("none_translation_service", value: "None", comment: "Dropdown option for selecting no translation service."))
case .libretranslate:
return .init(tag: self.rawValue, displayName: NSLocalizedString("LibreTranslate (Open Source)", comment: "Dropdown option for selecting LibreTranslate as the translation service."))
case .deepl:
+43
View File
@@ -7,6 +7,7 @@
import Foundation
import Vault
import UIKit
func should_show_wallet_selector(_ pubkey: String) -> Bool {
return UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
@@ -34,6 +35,10 @@ func get_default_zap_amount(pubkey: String) -> Int? {
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"),
@@ -45,6 +50,15 @@ func get_default_wallet(_ pubkey: String) -> Wallet {
}
}
func get_image_uploader(_ pubkey: String) -> ImageUploader {
if let defaultImageUploader = UserDefaults.standard.string(forKey: "default_image_uploader"),
let defaultImageUploader = ImageUploader(rawValue: defaultImageUploader) {
return defaultImageUploader
} else {
return .nostrBuild
}
}
private func get_translation_service(_ pubkey: String) -> TranslationService? {
guard let translation_service = UserDefaults.standard.string(forKey: "translation_service") else {
return nil
@@ -83,6 +97,12 @@ class UserSettingsStore: ObservableObject {
UserDefaults.standard.set(default_wallet.rawValue, forKey: "default_wallet")
}
}
@Published var default_image_uploader: ImageUploader {
didSet {
UserDefaults.standard.set(default_image_uploader.rawValue, forKey: "default_image_uploader")
}
}
@Published var show_wallet_selector: Bool {
didSet {
@@ -95,6 +115,18 @@ class UserSettingsStore: ObservableObject {
UserDefaults.standard.set(left_handed, forKey: "left_handed")
}
}
@Published var always_show_images: Bool {
didSet {
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 translation_service: TranslationService {
didSet {
@@ -159,14 +191,25 @@ class UserSettingsStore: ObservableObject {
}
}
}
@Published var disable_animation: Bool {
didSet {
UserDefaults.standard.set(disable_animation, forKey: "disable_animation")
}
}
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_image_uploader = get_image_uploader(pubkey)
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
zap_vibration = UserDefaults.standard.object(forKey: "zap_vibration") as? Bool ?? false
disable_animation = should_disable_image_animation()
// Note from @tyiu:
// Default translation service is disabled by default for now until we gain some confidence that it is working well in production.
+1 -1
View File
@@ -65,7 +65,7 @@ class ZapsModel: ObservableObject {
return
}
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper) else {
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: state.keypair.privkey) else {
return
}
+2 -6
View File
@@ -140,12 +140,8 @@ struct Profile: Codable {
try container.encode(value)
}
static func displayName(profile: Profile?, pubkey: String) -> String {
if pubkey == "anon" {
return "Anonymous"
}
let pk = bech32_nopre_pubkey(pubkey) ?? pubkey
return profile?.name ?? abbrev_pubkey(pk)
static func displayName(profile: Profile?, pubkey: String) -> DisplayName {
return parse_display_name(profile: profile, pubkey: pubkey)
}
}
+152 -12
View File
@@ -157,7 +157,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
pubkey = refkey.ref_id
}
let dec = decrypt_dm(key, pubkey: pubkey, content: self.content)
let dec = decrypt_dm(key, pubkey: pubkey, content: self.content, encoding: .base64)
self.decrypted_content = dec
return dec
@@ -215,6 +215,16 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
}
}
}
public func thread_id(privkey: String?) -> String {
for ref in event_refs(privkey) {
if let thread_id = ref.is_thread_id {
return thread_id.ref_id
}
}
return self.id
}
public func last_refid() -> ReferencedId? {
var mlast: Int? = nil
@@ -577,25 +587,115 @@ func zap_target_to_tags(_ target: ZapTarget) -> [[String]] {
}
}
func make_zap_request_event(pubkey: String, privkey: String, content: String, relays: [RelayDescriptor], target: ZapTarget, is_anon: Bool) -> NostrEvent {
func make_private_zap_request_event(identity: FullKeypair, enc_key: FullKeypair, target: ZapTarget, message: String) -> String? {
// target tags must be the same as zap request target tags
let tags = zap_target_to_tags(target)
let note = NostrEvent(content: message, pubkey: identity.pubkey, kind: 9733, tags: tags)
note.id = calculate_event_id(ev: note)
note.sig = sign_event(privkey: identity.privkey, ev: note)
guard let note_json = encode_json(note) else {
return nil
}
return encrypt_message(message: note_json, privkey: enc_key.privkey, to_pk: target.pubkey, encoding: .bech32)
}
func decrypt_private_zap(our_privkey: String, zapreq: NostrEvent, target: ZapTarget) -> NostrEvent? {
guard let anon_tag = zapreq.tags.first(where: { t in t.count >= 2 && t[0] == "anon" }) else {
return nil
}
let enc_note = anon_tag[1]
var note = decrypt_note(our_privkey: our_privkey, their_pubkey: zapreq.pubkey, enc_note: enc_note, encoding: .bech32)
// check to see if the private note was from us
if note == nil {
guard let our_private_keypair = generate_private_keypair(our_privkey: our_privkey, id: target.id, created_at: zapreq.created_at) else{
return nil
}
// use our private keypair and their pubkey to get the shared secret
note = decrypt_note(our_privkey: our_private_keypair.privkey, their_pubkey: target.pubkey, enc_note: enc_note, encoding: .bech32)
}
guard let note else {
return nil
}
guard note.kind == 9733 else {
return nil
}
let zr_etag = zapreq.referenced_ids.first
let note_etag = note.referenced_ids.first
guard zr_etag == note_etag else {
return nil
}
let zr_ptag = zapreq.referenced_pubkeys.first
let note_ptag = note.referenced_pubkeys.first
guard let zr_ptag, let note_ptag, zr_ptag == note_ptag else {
return nil
}
guard validate_event(ev: note) == .ok else {
return nil
}
return note
}
func generate_private_keypair(our_privkey: String, id: String, created_at: Int64) -> FullKeypair? {
let to_hash = our_privkey + id + String(created_at)
guard let dat = to_hash.data(using: .utf8) else {
return nil
}
let privkey_bytes = sha256(dat)
let privkey = hex_encode(privkey_bytes)
guard let pubkey = privkey_to_pubkey(privkey: privkey) else {
return nil
}
return FullKeypair(pubkey: pubkey, privkey: privkey)
}
func make_zap_request_event(keypair: FullKeypair, content: String, relays: [RelayDescriptor], target: ZapTarget, zap_type: ZapType) -> NostrEvent? {
var tags = zap_target_to_tags(target)
var relay_tag = ["relays"]
relay_tag.append(contentsOf: relays.map { $0.url.absoluteString })
tags.append(relay_tag)
var priv = privkey
var pub = pubkey
var kp = keypair
if is_anon {
let now = Int64(Date().timeIntervalSince1970)
var message = content
switch zap_type {
case .pub:
break
case .non_zap:
break
case .anon:
tags.append(["anon"])
let kp = generate_new_keypair()
pub = kp.pubkey
priv = kp.privkey!
kp = generate_new_keypair().to_full()!
case .priv:
guard let priv_kp = generate_private_keypair(our_privkey: keypair.privkey, id: target.id, created_at: now) else {
return nil
}
kp = priv_kp
guard let privreq = make_private_zap_request_event(identity: keypair, enc_key: kp, target: target, message: message) else {
return nil
}
tags.append(["anon", privreq])
message = ""
}
let ev = NostrEvent(content: content, pubkey: pub, kind: 9734, tags: tags)
let ev = NostrEvent(content: message, pubkey: kp.pubkey, kind: 9734, tags: tags, createdAt: now)
ev.id = calculate_event_id(ev: ev)
ev.sig = sign_event(privkey: priv, ev: ev)
ev.sig = sign_event(privkey: kp.privkey, ev: ev)
return ev
}
@@ -625,14 +725,14 @@ func event_to_json(ev: NostrEvent) -> String {
return str
}
func decrypt_dm(_ privkey: String?, pubkey: String, content: String) -> String? {
func decrypt_dm(_ privkey: String?, pubkey: String, content: String, encoding: EncEncoding) -> String? {
guard let privkey = privkey else {
return nil
}
guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: pubkey) else {
return nil
}
guard let dat = decode_dm_base64(content) else {
guard let dat = (encoding == .base64 ? decode_dm_base64(content) : decode_dm_bech32(content)) else {
return nil
}
guard let dat = aes_decrypt(data: dat.content, iv: dat.iv, shared_sec: shared_sec) else {
@@ -641,6 +741,13 @@ func decrypt_dm(_ privkey: String?, pubkey: String, content: String) -> String?
return String(data: dat, encoding: .utf8)
}
func decrypt_note(our_privkey: String, their_pubkey: String, enc_note: String, encoding: EncEncoding) -> NostrEvent? {
guard let dec = decrypt_dm(our_privkey, pubkey: their_pubkey, content: enc_note, encoding: encoding) else {
return nil
}
return decode_nostr_event_json(json: dec)
}
func get_shared_secret(privkey: String, pubkey: String) -> [UInt8]? {
guard let privkey_bytes = try? privkey.bytes else {
@@ -686,6 +793,39 @@ struct DirectMessageBase64 {
let iv: [UInt8]
}
func encode_dm_bech32(content: [UInt8], iv: [UInt8]) -> String {
let content_bech32 = bech32_encode(hrp: "pzap", content)
let iv_bech32 = bech32_encode(hrp: "iv", iv)
return content_bech32 + "_" + iv_bech32
}
func decode_dm_bech32(_ all: String) -> DirectMessageBase64? {
let parts = all.split(separator: "_")
guard parts.count == 2 else {
return nil
}
let content_bech32 = String(parts[0])
let iv_bech32 = String(parts[1])
guard let content_tup = try? bech32_decode(content_bech32) else {
return nil
}
guard let iv_tup = try? bech32_decode(iv_bech32) else {
return nil
}
guard content_tup.hrp == "pzap" else {
return nil
}
guard iv_tup.hrp == "iv" else {
return nil
}
return DirectMessageBase64(content: content_tup.data.bytes, iv: iv_tup.data.bytes)
}
func encode_dm_base64(content: [UInt8], iv: [UInt8]) -> String {
let content_b64 = base64_encode(content)
let iv_b64 = base64_encode(iv)
+6 -2
View File
@@ -37,13 +37,17 @@ struct NostrFilter: Codable, Equatable {
}
public static func filter_hashtag(_ htags: [String]) -> NostrFilter {
return NostrFilter(ids: nil, kinds: nil, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil, hashtag: htags)
return NostrFilter(ids: nil, kinds: nil, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil, hashtag: htags.map { $0.lowercased() })
}
public static var filter_text: NostrFilter {
return filter_kinds([1])
}
public static func filter_ids(_ ids: [String]) -> NostrFilter {
return NostrFilter(ids: ids, kinds: nil, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil, hashtag: nil)
}
public static var filter_profiles: NostrFilter {
return filter_kinds([0])
}
+4 -1
View File
@@ -127,6 +127,9 @@ func decode_nostr_uri(_ s: String) -> NostrLink? {
var uri = s.replacingOccurrences(of: "nostr://", with: "")
uri = uri.replacingOccurrences(of: "nostr:", with: "")
uri = uri.replacingOccurrences(of: "damus://", with: "")
uri = uri.replacingOccurrences(of: "damus:", with: "")
let parts = uri.split(separator: ":")
.reduce(into: Array<String>()) { acc, str in
guard let decoded = str.removingPercentEncoding else {
@@ -137,7 +140,7 @@ func decode_nostr_uri(_ s: String) -> NostrLink? {
}
if tag_is_hashtag(parts) {
return .filter(NostrFilter.filter_hashtag([parts[1].lowercased()]))
return .filter(NostrFilter.filter_hashtag([parts[1]]))
}
if let rid = tag_to_refid(parts) {
+66
View File
@@ -0,0 +1,66 @@
//
// DisplayName.swift
// damus
//
// Created by William Casarin on 2023-03-14.
//
import Foundation
struct BothNames {
let username: String
let display_name: String
}
enum DisplayName {
case both(BothNames)
case one(String)
var display_name: String {
switch self {
case .one(let one):
return one
case .both(let b):
return b.display_name
}
}
var username: String {
switch self {
case .one(let one):
return one
case .both(let b):
return b.username
}
}
}
func parse_display_name(profile: Profile?, pubkey: String) -> DisplayName {
if pubkey == "anon" {
return .one("Anonymous")
}
guard let profile else {
return .one(abbrev_bech32_pubkey(pubkey: pubkey))
}
let name = profile.name?.isEmpty == false ? profile.name : nil
let disp_name = profile.display_name?.isEmpty == false ? profile.display_name : nil
if let name, let disp_name, name != disp_name {
return .both(BothNames(username: name, display_name: disp_name))
}
if let one = name ?? disp_name {
return .one(one)
}
return .one(abbrev_bech32_pubkey(pubkey: pubkey))
}
func abbrev_bech32_pubkey(pubkey: String) -> String {
let pk = bech32_nopre_pubkey(pubkey) ?? pubkey
return abbrev_pubkey(pk)
}
+68 -3
View File
@@ -5,10 +5,74 @@
// Created by William Casarin on 2023-02-21.
//
import Combine
import Foundation
import UIKit
class EventCache {
private var events: [String: NostrEvent]
private var events: [String: NostrEvent] = [:]
private var replies = ReplyMap()
private var cancellable: AnyCancellable?
//private var thread_latest: [String: Int64]
init() {
cancellable = NotificationCenter.default.publisher(
for: UIApplication.didReceiveMemoryWarningNotification
).sink { [weak self] _ in
self?.prune()
}
}
func parent_events(event: NostrEvent) -> [NostrEvent] {
var parents: [NostrEvent] = []
var ev = event
while true {
guard let direct_reply = ev.direct_replies(nil).first else {
break
}
guard let next_ev = lookup(direct_reply.ref_id), next_ev != ev else {
break
}
parents.append(next_ev)
ev = next_ev
}
return parents.reversed()
}
func add_replies(ev: NostrEvent) {
for reply in ev.direct_replies(nil) {
replies.add(id: reply.ref_id, reply_id: ev.id)
}
}
func child_events(event: NostrEvent) -> [NostrEvent] {
guard let xs = replies.lookup(event.id) else {
return []
}
let evs: [NostrEvent] = xs.reduce(into: [], { evs, evid in
guard let ev = self.lookup(evid) else {
return
}
evs.append(ev)
}).sorted(by: { $0.created_at < $1.created_at })
return evs
}
func upsert(_ ev: NostrEvent) -> NostrEvent {
if let found = lookup(ev.id) {
return found
}
insert(ev)
return ev
}
func lookup(_ evid: String) -> NostrEvent? {
return events[evid]
@@ -21,7 +85,8 @@ class EventCache {
events[ev.id] = ev
}
init() {
self.events = [:]
private func prune() {
events = [:]
replies.replies = [:]
}
}
+5 -1
View File
@@ -8,12 +8,16 @@
import Foundation
/// Used for holding back events until they're ready to be displayed
class EventHolder: ObservableObject {
class EventHolder: ObservableObject, ScrollQueue {
private var has_event: Set<String>
@Published var events: [NostrEvent]
@Published var incoming: [NostrEvent]
var should_queue: Bool
func set_should_queue(_ val: Bool) {
self.should_queue = val
}
var queued: Int {
return incoming.count
}
+49
View File
@@ -0,0 +1,49 @@
//
// Binding+.swift
// damus
//
// Created by Oleg Abalonski on 3/5/23.
// Ref: https://josephduffy.co.uk/posts/mapping-optional-binding-to-bool
import os.log
import SwiftUI
extension Binding where Value == Bool {
/// Creates a binding by mapping an optional value to a `Bool` that is
/// `true` when the value is non-`nil` and `false` when the value is `nil`.
///
/// When the value of the produced binding is set to `false` the value
/// of `bindingToOptional`'s `wrappedValue` is set to `nil`.
///
/// Setting the value of the produce binding to `true` does nothing and
/// will log an error.
///
/// - parameter bindingToOptional: A `Binding` to an optional value, used to calculate the `wrappedValue`.
public init<Wrapped>(mappedTo bindingToOptional: Binding<Wrapped?>) {
self.init(
get: { bindingToOptional.wrappedValue != nil },
set: { newValue in
if !newValue {
bindingToOptional.wrappedValue = nil
} else {
os_log(
.error,
"Optional binding mapped to optional has been set to `true`, which will have no effect. Current value: %@",
String(describing: bindingToOptional.wrappedValue)
)
}
}
)
}
}
extension Binding {
/// Returns a binding by mapping this binding's value to a `Bool` that is
/// `true` when the value is non-`nil` and `false` when the value is `nil`.
///
/// When the value of the produced binding is set to `false` this binding's value
/// is set to `nil`.
public func mappedToBool<Wrapped>() -> Binding<Bool> where Value == Wrapped? {
return Binding<Bool>(mappedTo: self)
}
}
@@ -14,19 +14,18 @@ extension KFOptionSetter {
options.callbackQueue = .dispatch(.global(qos: .background))
options.processingQueue = .dispatch(.global(qos: .background))
options.downloader = CustomImageDownloader.shared
options.backgroundDecode = true
options.cacheOriginalImage = true
options.scaleFactor = UIScreen.main.scale
options.processor = CustomImageProcessor(
maxSize: imageContext.maxMebibyteSize(),
downsampleSize: imageContext.downsampleSize()
)
options.cacheSerializer = CustomCacheSerializer(
maxSize: imageContext.maxMebibyteSize(),
downsampleSize: imageContext.downsampleSize()
)
options.backgroundDecode = true
options.cacheOriginalImage = true
options.scaleFactor = UIScreen.main.scale
options.onlyLoadFirstFrame = should_disable_image_animation()
return self
}
+6
View File
@@ -15,3 +15,9 @@ func bundleForLocale(locale: Locale?) -> Bundle {
let path = Bundle.main.path(forResource: locale!.identifier, ofType: "lproj")
return path != nil ? (Bundle(path: path!) ?? Bundle.main) : Bundle.main
}
func localizedStringFormat(key: String, locale: Locale?) -> String {
let bundle = bundleForLocale(locale: locale)
let fallback = bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: key, value: nil, table: nil)
return bundle.localizedString(forKey: key, value: fallback, table: nil)
}
-3
View File
@@ -101,9 +101,6 @@ extension Notification.Name {
static var update_stats: Notification.Name {
return Notification.Name("update_stats")
}
static var update_bookmarks: Notification.Name {
return Notification.Name("update_bookmarks")
}
static var zapping: Notification.Name {
return Notification.Name("zapping")
}
+8
View File
@@ -0,0 +1,8 @@
//
// PostBox.swift
// damus
//
// Created by William Casarin on 2023-03-20.
//
import Foundation
+30 -16
View File
@@ -7,12 +7,6 @@
import Foundation
enum ZapSource {
case author(String)
// TODO: anonymous
//case anonymous
}
public struct NoteZapTarget: Equatable {
public let note_id: String
public let author: String
@@ -55,8 +49,10 @@ struct Zap {
public let zapper: String /// zap authorizer
public let target: ZapTarget
public let request: ZapRequest
public let is_anon: Bool
public let private_request: NostrEvent?
public static func from_zap_event(zap_ev: NostrEvent, zapper: 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
guard zapper == zap_ev.pubkey else {
return nil
@@ -83,14 +79,26 @@ struct Zap {
guard let desc = get_zap_description(zap_ev, inv_desc: zap_invoice.description) else {
return nil
}
guard let zap_req = decode_nostr_event_json(desc) else {
return nil
}
guard validate_event(ev: zap_req) == .ok else {
return nil
}
guard let target = determine_zap_target(zap_req) else {
return nil
}
return Zap(event: zap_ev, invoice: zap_invoice, zapper: zapper, target: target, request: ZapRequest(ev: zap_req))
let private_request = our_privkey.flatMap {
decrypt_private_zap(our_privkey: $0, zapreq: zap_req, target: target)
}
let is_anon = private_request == nil && event_is_anonymous(ev: zap_req)
return Zap(event: zap_ev, invoice: zap_invoice, zapper: zapper, target: target, request: ZapRequest(ev: zap_req), is_anon: is_anon, private_request: private_request)
}
}
@@ -285,7 +293,7 @@ func fetch_static_payreq(_ lnurl: String) async -> LNUrlPayRequest? {
return endpoint
}
func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int, zap_type: ZapType, comment: String?) async -> String? {
func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent?, sats: Int, zap_type: ZapType, comment: String?) async -> String? {
guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else {
return nil
}
@@ -295,15 +303,13 @@ func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int,
var query = [URLQueryItem(name: "amount", value: "\(amount)")]
if zappable && zap_type != .non_zap {
if let json = encode_json(zapreq) {
print("zapreq json: \(json)")
query.append(URLQueryItem(name: "nostr", value: json))
}
if let zapreq, zappable && zap_type != .non_zap, let json = encode_json(zapreq) {
print("zapreq json: \(json)")
query.append(URLQueryItem(name: "nostr", value: json))
}
// add a lud12 comment as well if we have it
if let comment, let limit = payreq.commentAllowed, limit != 0 {
if zap_type != .priv, let comment, let limit = payreq.commentAllowed, limit != 0 {
let limited_comment = String(comment.prefix(limit))
query.append(URLQueryItem(name: "comment", value: limited_comment))
}
@@ -316,7 +322,15 @@ func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int,
print("url \(url)")
guard let ret = try? await URLSession.shared.data(from: url) else {
var ret: (Data, URLResponse)? = nil
do {
ret = try await URLSession.shared.data(from: url)
} catch {
print(error.localizedDescription)
return nil
}
guard let ret else {
return nil
}
+49 -16
View File
@@ -26,11 +26,13 @@ struct EventActionBar: View {
// just used for previews
@State var sheet: ActionBarSheet? = nil
@State var confirm_boost: Bool = false
@State var show_share_sheet: Bool = false
@State var show_share_action: Bool = false
@ObservedObject var bar: ActionBarModel
@Environment(\.colorScheme) var colorScheme
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel? = nil, test_lnurl: String? = nil) {
self.damus_state = damus_state
self.event = event
@@ -56,8 +58,8 @@ struct EventActionBar: View {
EventActionButton(img: "arrow.2.squarepath", col: bar.boosted ? Color.green : nil) {
if bar.boosted {
notify(.delete, bar.our_boost)
} else if damus_state.is_privkey_user {
self.confirm_boost = true
} else {
send_boost()
}
}
.accessibilityLabel(NSLocalizedString("Boosts", comment: "Accessibility label for boosts button"))
@@ -88,10 +90,23 @@ struct EventActionBar: View {
Spacer()
EventActionButton(img: "square.and.arrow.up", col: Color.gray) {
show_share_sheet = true
show_share_action = true
}
.accessibilityLabel(NSLocalizedString("Share", comment: "Button to share a post"))
}
.sheet(isPresented: $show_share_action) {
if #available(iOS 16.0, *) {
ShareAction(event: event, bookmarks: damus_state.bookmarks, show_share_sheet: $show_share_sheet, show_share_action: $show_share_action)
.presentationDetents([.height(300)])
.presentationDragIndicator(.visible)
} else {
if let note_id = bech32_note_id(event.id) {
if let url = URL(string: "https://damus.io/" + note_id) {
ShareSheet(activityItems: [url])
}
}
}
}
.sheet(isPresented: $show_share_sheet) {
if let note_id = bech32_note_id(event.id) {
if let url = URL(string: "https://damus.io/" + note_id) {
@@ -99,16 +114,6 @@ struct EventActionBar: View {
}
}
}
.alert(NSLocalizedString("Repost", comment: "Title of alert for confirming to repost a post."), isPresented: $confirm_boost) {
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of reposting a post.")) {
confirm_boost = false
}
Button(NSLocalizedString("Repost", comment: "Button to confirm reposting a post.")) {
send_boost()
}
} message: {
Text("Are you sure you want to repost this?", comment: "Alert message to ask if user wants to repost a post.")
}
.onReceive(handle_notify(.update_stats)) { n in
let target = n.object as! String
guard target == self.event.id else { return }
@@ -135,7 +140,7 @@ struct EventActionBar: View {
self.bar.our_boost = boost
damus_state.pool.send(.event(boost))
notify(.boost, boost)
}
func send_like() {
@@ -167,13 +172,41 @@ struct LikeButton: View {
let action: () -> ()
@Environment(\.colorScheme) var colorScheme
// Following four are Shaka animation properties
let timer = Timer.publish(every: 0.10, on: .main, in: .common).autoconnect()
@State private var shouldAnimate = false
@State private var rotationAngle = 0.0
@State private var amountOfAngleIncrease: Double = 0.0
var body: some View {
Button(action: action) {
Button(action: {
withAnimation(Animation.easeOut(duration: 0.15)) {
self.action()
shouldAnimate = true
amountOfAngleIncrease = 20.0
}
}) {
Image(liked ? "shaka-full" : "shaka-line")
.foregroundColor(liked ? .accentColor : .gray)
}
.accessibilityLabel(NSLocalizedString("Like", comment: "Accessibility Label for Like button"))
.rotationEffect(Angle(degrees: shouldAnimate ? rotationAngle : 0))
.onReceive(self.timer) { _ in
// Shaka animation logic
rotationAngle = amountOfAngleIncrease
if amountOfAngleIncrease == 0 {
timer.upstream.connect().cancel()
return
}
amountOfAngleIncrease = -amountOfAngleIncrease
if amountOfAngleIncrease < 0 {
amountOfAngleIncrease += 2.5
} else {
amountOfAngleIncrease -= 2.5
}
}
}
}
+6 -6
View File
@@ -53,18 +53,18 @@ struct EventDetailBar: View {
}
func repostsCountString(_ count: Int, locale: Locale = Locale.current) -> String {
let bundle = bundleForLocale(locale: locale)
return String(format: bundle.localizedString(forKey: "reposts_count", value: nil, table: nil), locale: locale, count)
let format = localizedStringFormat(key: "reposts_count", locale: locale)
return String(format: format, locale: locale, count)
}
func reactionsCountString(_ count: Int, locale: Locale = Locale.current) -> String {
let bundle = bundleForLocale(locale: locale)
return String(format: bundle.localizedString(forKey: "reactions_count", value: nil, table: nil), locale: locale, count)
let format = localizedStringFormat(key: "reactions_count", locale: locale)
return String(format: format, locale: locale, count)
}
func zapsCountString(_ count: Int, locale: Locale = Locale.current) -> String {
let bundle = bundleForLocale(locale: locale)
return String(format: bundle.localizedString(forKey: "zaps_count", value: nil, table: nil), locale: locale, count)
let format = localizedStringFormat(key: "zaps_count", locale: locale)
return String(format: format, locale: locale, count)
}
struct EventDetailBar_Previews: PreviewProvider {
+110
View File
@@ -0,0 +1,110 @@
//
// ShareAction.swift
// damus
//
// Created by eric on 3/8/23.
//
import SwiftUI
struct ShareAction: View {
let event: NostrEvent
let bookmarks: BookmarksManager
@State private var isBookmarked: Bool = false
@Binding var show_share_sheet: Bool
@Binding var show_share_action: Bool
@Environment(\.colorScheme) var colorScheme
init(event: NostrEvent, bookmarks: BookmarksManager, show_share_sheet: Binding<Bool>, show_share_action: Binding<Bool>) {
let bookmarked = bookmarks.isBookmarked(event)
self._isBookmarked = State(initialValue: bookmarked)
self.bookmarks = bookmarks
self.event = event
self._show_share_sheet = show_share_sheet
self._show_share_action = show_share_action
}
var body: some View {
let col = colorScheme == .light ? Color("DamusMediumGrey") : Color("DamusWhite")
VStack {
Text("Share Note", comment: "Title text to indicate that the buttons below are meant to be used to share a note with others.")
.padding()
.font(.system(size: 17, weight: .bold))
Spacer()
HStack(alignment: .top, spacing: 25) {
ShareActionButton(img: "link", text: NSLocalizedString("Copy Link", comment: "Button to copy link to note"), col: col) {
show_share_action = false
UIPasteboard.general.string = "https://damus.io/" + (bech32_note_id(event.id) ?? event.id)
}
let bookmarkImg = isBookmarked ? "bookmark.slash" : "bookmark"
let bookmarkTxt = isBookmarked ? "Remove\nBookmark" : "Bookmark"
let boomarkCol = isBookmarked ? Color(.red) : col
ShareActionButton(img: bookmarkImg, text: NSLocalizedString(bookmarkTxt, comment: "Button to bookmark to note"), col: boomarkCol) {
show_share_action = false
self.bookmarks.updateBookmark(event)
isBookmarked = self.bookmarks.isBookmarked(event)
}
ShareActionButton(img: "globe", text: NSLocalizedString("Broadcast", comment: "Button to broadcast note to all your relays"), col: col) {
show_share_action = false
NotificationCenter.default.post(name: .broadcast_event, object: event)
}
ShareActionButton(img: "square.and.arrow.up", text: NSLocalizedString("Share Via...", comment: "Button to present iOS share sheet"), col: col) {
show_share_action = false
show_share_sheet = true
}
}
Spacer()
HStack {
Button(action: {
show_share_action = false
}) {
Text(NSLocalizedString("Cancel", comment: "Button to cancel a repost."))
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .center)
.foregroundColor(colorScheme == .light ? Color("DamusBlack") : Color("DamusWhite"))
.overlay {
RoundedRectangle(cornerRadius: 24)
.stroke(colorScheme == .light ? Color("DamusMediumGrey") : Color("DamusWhite"), lineWidth: 1)
}
.padding(EdgeInsets(top: 10, leading: 50, bottom: 25, trailing: 50))
}
}
}
}
}
func ShareActionButton(img: String, text: String, col: Color, action: @escaping () -> ()) -> some View {
Button(action: action) {
VStack() {
Image(systemName: img)
.foregroundColor(col)
.font(.system(size: 23, weight: .bold))
.overlay {
Circle()
.stroke(col, lineWidth: 1)
.frame(width: 55.0, height: 55.0)
}
.frame(height: 25)
Text(verbatim: text)
.foregroundColor(col)
.font(.footnote)
.multilineTextAlignment(.center)
.padding(.top)
}
}
}
+218
View File
@@ -0,0 +1,218 @@
//
// AttachMediaUtility.swift
// damus
//
// Created by Swift on 2/17/23.
//
import SwiftUI
import UIKit
import CoreGraphics
import UniformTypeIdentifiers
enum ImageUploadResult {
case success(String)
case failed(Error?)
}
fileprivate func create_upload_body(imageDataKey: Data, boundary: String, imageUploader: ImageUploader) -> Data {
let body = NSMutableData();
let contentType = "image/jpg"
body.appendString(string: "Content-Type: multipart/form-data; boundary=\(boundary)\r\n\r\n")
body.appendString(string: "--\(boundary)\r\n")
body.appendString(string: "Content-Disposition: form-data; name=\(imageUploader.nameParam); filename=\"damus_generic_filename.jpg\"\r\n")
body.appendString(string: "Content-Type: \(contentType)\r\n\r\n")
body.append(imageDataKey as Data)
body.appendString(string: "\r\n")
body.appendString(string: "--\(boundary)--\r\n")
return body as Data
}
func create_image_upload_request(imageToUpload: UIImage, imageUploader: ImageUploader, progress: URLSessionTaskDelegate) async -> ImageUploadResult {
guard let url = URL(string: imageUploader.postAPI) else {
return .failed(nil)
}
var request = URLRequest(url: url)
request.httpMethod = "POST";
let boundary = "Boundary-\(UUID().description)"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
// otherwise convert to jpg
guard let jpegData = imageToUpload.jpegData(compressionQuality: 0.8) else {
// somehow failed, just return original
return .failed(nil)
}
request.httpBody = create_upload_body(imageDataKey: jpegData, boundary: boundary, imageUploader: imageUploader)
do {
let (data, _) = try await URLSession.shared.data(for: request, delegate: progress)
guard let responseString = String(data: data, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue)) else {
print("Upload failed getting response string")
return .failed(nil)
}
guard let url = imageUploader.getImageURL(from: responseString) else {
print("Upload failed getting image url")
return .failed(nil)
}
return .success(url)
} catch {
return .failed(error)
}
}
extension PostView {
struct ImagePicker: UIViewControllerRepresentable {
@Environment(\.presentationMode)
private var presentationMode
let sourceType: UIImagePickerController.SourceType
let onImagePicked: (UIImage) -> Void
final class Coordinator: NSObject,
UINavigationControllerDelegate,
UIImagePickerControllerDelegate {
@Binding
private var presentationMode: PresentationMode
private let sourceType: UIImagePickerController.SourceType
private let onImagePicked: (UIImage) -> Void
init(presentationMode: Binding<PresentationMode>,
sourceType: UIImagePickerController.SourceType,
onImagePicked: @escaping (UIImage) -> Void) {
_presentationMode = presentationMode
self.sourceType = sourceType
self.onImagePicked = onImagePicked
}
func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let uiImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage
onImagePicked(uiImage)
presentationMode.dismiss()
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
presentationMode.dismiss()
}
}
func makeCoordinator() -> Coordinator {
return Coordinator(presentationMode: presentationMode,
sourceType: sourceType,
onImagePicked: onImagePicked)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.sourceType = sourceType
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController,
context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}
}
extension NSMutableData {
func appendString(string: String) {
guard let data = string.data(using: String.Encoding.utf8, allowLossyConversion: true) else {
return
}
append(data)
}
}
enum ImageUploader: String, CaseIterable, Identifiable {
var id: String { self.rawValue }
case nostrBuild
case nostrImg
var nameParam: String {
switch self {
case .nostrBuild:
return "\"fileToUpload\""
case .nostrImg:
return "\"image\""
}
}
var displayImageUploaderName: String {
switch self {
case .nostrBuild:
return "NostrBuild"
case .nostrImg:
return "NostrImg"
}
}
struct Model: Identifiable, Hashable {
var id: String { self.tag }
var index: Int
var tag: String
var displayName : String
}
var model: Model {
switch self {
case .nostrBuild:
return .init(index: -1, tag: "nostrBuild", displayName: NSLocalizedString("NostrBuild", comment: "Dropdown option label for system default for NostrBuild image uploader."))
case .nostrImg:
return .init(index: 0, tag: "nostrImg", displayName: NSLocalizedString("NostrImg", comment: "Dropdown option label for system default for NostrImg image uploader."))
}
}
var postAPI: String {
switch self {
case .nostrBuild:
return "https://nostr.build/upload.php"
case .nostrImg:
return "https://nostrimg.com/api/upload"
}
}
func getImageURL(from responseString: String) -> String? {
switch self {
case .nostrBuild:
guard let startIndex = responseString.range(of: "nostr.build_")?.lowerBound else {
return nil
}
let stringContainingName = responseString[startIndex..<responseString.endIndex]
guard let endIndex = stringContainingName.range(of: "<")?.lowerBound else {
return nil
}
let nostrBuildImageName = responseString[startIndex..<endIndex]
let nostrBuildURL = "https://nostr.build/i/\(nostrBuildImageName)"
return nostrBuildURL
case .nostrImg:
guard let startIndex = responseString.range(of: "https://i.nostrimg.com/")?.lowerBound else {
return nil
}
let stringContainingName = responseString[startIndex..<responseString.endIndex]
guard let endIndex = stringContainingName.range(of: "\"")?.lowerBound else {
return nil
}
let nostrBuildImageName = responseString[startIndex..<endIndex]
let nostrBuildURL = "\(nostrBuildImageName)"
return nostrBuildURL
}
}
}
+11 -19
View File
@@ -12,15 +12,20 @@ struct BookmarksView: View {
private let noneFilter: (NostrEvent) -> Bool = { _ in true }
private let bookmarksTitle = NSLocalizedString("Bookmarks", comment: "Title of bookmarks view")
@State private var bookmarkEvents: [NostrEvent] = []
@ObservedObject var manager: BookmarksManager
init(state: DamusState) {
self.state = state
self._manager = ObservedObject(initialValue: state.bookmarks)
}
var bookmarks: [NostrEvent] {
manager.bookmarks
}
var body: some View {
Group {
if bookmarkEvents.isEmpty {
if bookmarks.isEmpty {
VStack {
Image(systemName: "bookmark")
.resizable()
@@ -28,12 +33,9 @@ struct BookmarksView: View {
.frame(width: 32.0, height: 32.0)
Text(NSLocalizedString("You have no bookmarks yet, add them in the context menu", comment: "Text indicating that there are no bookmarks to be viewed"))
}
.task {
updateBookmarks()
}
} else {
ScrollView {
InnerTimelineView(events: EventHolder(events: bookmarkEvents, incoming: []), damus: state, show_friend_icon: true, filter: noneFilter)
InnerTimelineView(events: EventHolder(events: bookmarks, incoming: []), damus: state, show_friend_icon: true, filter: noneFilter)
}
}
@@ -41,22 +43,12 @@ struct BookmarksView: View {
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(bookmarksTitle)
.toolbar {
if !bookmarkEvents.isEmpty {
if !bookmarks.isEmpty {
Button(NSLocalizedString("Clear All", comment: "Button for clearing bookmarks data.")) {
BookmarksManager(pubkey: state.pubkey).clearAll()
bookmarkEvents = []
}
manager.clearAll()
}
}
}
.onReceive(handle_notify(.update_bookmarks)) { _ in
updateBookmarks()
}
}
private func updateBookmarks() {
bookmarkEvents = BookmarksManager(pubkey: state.pubkey).bookmarks.compactMap { bookmark_json in
event_from_json(dat: bookmark_json)
}
}
}
+6 -5
View File
@@ -35,14 +35,14 @@ struct ChatView: View {
}
if let rep = thread.replies.lookup(event.id) {
return rep == prev.id
return rep.contains(prev.id)
}
return false
}
var is_active: Bool {
return thread.initial_event.id == event.id
return thread.event.id == event.id
}
func prev_reply_is_same() -> String? {
@@ -108,12 +108,13 @@ struct ChatView: View {
}
}
let show_images = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
let show_images = should_show_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
NoteContentView(damus_state: damus_state,
event: event,
show_images: show_images,
size: .normal,
artifacts: .just_content(event.content))
artifacts: .just_content(event.content),
options: [])
if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey {
let bar = make_actionbar_model(ev: event.id, damus: damus_state)
@@ -160,7 +161,7 @@ func prev_reply_is_same(event: NostrEvent, prev_ev: NostrEvent?, replies: ReplyM
if let prev_reply_id = replies.lookup(prev.id) {
if let cur_reply_id = replies.lookup(event.id) {
if prev_reply_id != cur_reply_id {
return cur_reply_id
return cur_reply_id.first
}
}
}
+8 -5
View File
@@ -7,6 +7,7 @@
import SwiftUI
/*
struct ChatroomView: View {
@EnvironmentObject var thread: ThreadModel
@Environment(\.dismiss) var dismiss
@@ -24,9 +25,9 @@ struct ChatroomView: View {
next_ev: ind == count-1 ? nil : thread.events[ind+1],
damus_state: damus
)
.event_context_menu(ev, keypair: damus.keypair, target_pubkey: ev.pubkey)
.contextMenu{MenuItems(event: ev, keypair: damus.keypair, target_pubkey: ev.pubkey, bookmarks: damus.bookmarks)}
.onTapGesture {
if thread.initial_event.id == ev.id {
if thread.event.id == ev.id {
//dismiss()
toggle_thread_view()
} else {
@@ -44,7 +45,7 @@ struct ChatroomView: View {
}
.onReceive(NotificationCenter.default.publisher(for: .select_quote)) { notif in
let ev = notif.object as! NostrEvent
if ev.id != thread.initial_event.id {
if ev.id != thread.event.id {
thread.set_active_event(ev, privkey: damus.keypair.privkey)
}
scroll_to_event(scroller: scroller, id: ev.id, delay: 0, animate: true)
@@ -57,7 +58,7 @@ struct ChatroomView: View {
once = true
}
.onAppear() {
scroll_to_event(scroller: scroller, id: thread.initial_event.id, delay: 0.1, animate: false)
scroll_to_event(scroller: scroller, id: thread.event.id, delay: 0.1, animate: false)
}
}
}
@@ -76,7 +77,9 @@ struct ChatroomView_Previews: PreviewProvider {
static var previews: some View {
let state = test_damus_state()
ChatroomView(damus: state)
.environmentObject(ThreadModel(evid: "&849ab9bb263ed2819db06e05f1a1a3b72878464e8c7146718a2fc1bf1912f893", damus_state: state))
.environmentObject(ThreadModel(event: test_event, damus_state: state))
}
}
*/
+72 -14
View File
@@ -12,8 +12,10 @@ import Combine
struct ConfigView: View {
let state: DamusState
@Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) var dismiss
@State var confirm_logout: Bool = false
@State var delete_account_warning: Bool = false
@State var confirm_delete_account: Bool = false
@State var show_privkey: Bool = false
@State var has_authenticated_locally: Bool = false
@@ -35,6 +37,10 @@ struct ConfigView: View {
_privkey = State(initialValue: self.state.keypair.privkey_bech32 ?? "")
_settings = ObservedObject(initialValue: state.settings)
}
func textColor() -> Color {
colorScheme == .light ? Color("DamusBlack") : Color("DamusWhite")
}
func authenticateLocally(completion: @escaping (Bool) -> Void) {
// Need to authenticate only once while ConfigView is presented
@@ -66,12 +72,18 @@ struct ConfigView: View {
self.pubkey_copied = is_pk
generator.impactOccurred()
}
if has_authenticated_locally {
if is_pk {
// When trying to copy npub
copyKey()
} else {
authenticateLocally { success in
if success {
copyKey()
// When trying to copy nsec
if has_authenticated_locally {
copyKey()
} else {
authenticateLocally { success in
if success {
copyKey()
}
}
}
}
@@ -133,9 +145,9 @@ struct ConfigView: View {
TextField(String("1000"), text: $default_zap_amount)
.keyboardType(.numberPad)
.onReceive(Just(default_zap_amount)) { newValue in
if let parsed = handle_string_amount(new_value: newValue) {
self.default_zap_amount = String(parsed)
set_default_zap_amount(pubkey: self.state.pubkey, amount: parsed)
}
}
}
@@ -187,23 +199,53 @@ struct ConfigView: View {
}
}
Section(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen")) {
Section(NSLocalizedString("Miscellaneous", comment: "Section header for miscellaneous user configuration")) {
Toggle(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen"), isOn: $settings.left_handed)
.toggleStyle(.switch)
Toggle(NSLocalizedString("Zap Vibration", comment: "Setting to enable vibration on zap"), isOn: $settings.zap_vibration)
.toggleStyle(.switch)
}
Section(NSLocalizedString("Clear Cache", comment: "Section title for clearing cached data.")) {
Button(NSLocalizedString("Clear", comment: "Button for clearing cached data.")) {
KingfisherManager.shared.cache.clearMemoryCache()
KingfisherManager.shared.cache.clearDiskCache()
KingfisherManager.shared.cache.cleanExpiredDiskCache()
Section(NSLocalizedString("Images", comment: "Section title for images configuration.")) {
Toggle(NSLocalizedString("Disable animations", comment: "Button to disable image animation"), isOn: $settings.disable_animation)
.toggleStyle(.switch)
.onChange(of: settings.disable_animation) { _ in
clear_kingfisher_cache()
}
Toggle(NSLocalizedString("Always show images", comment: "Setting to always show and never blur images"), isOn: $settings.always_show_images)
.toggleStyle(.switch)
Button(NSLocalizedString("Clear Cache", comment: "Button to clear image cache.")) {
clear_kingfisher_cache()
}
Picker(NSLocalizedString("Select image uploader", comment: "Prompt selection of user's image uploader"),
selection: $settings.default_image_uploader) {
ForEach(ImageUploader.allCases, id: \.self) { uploader in
Text(uploader.model.displayName)
.tag(uploader.model.tag)
}
}
}
Section(NSLocalizedString("Sign Out", comment: "Section title for signing out")) {
Button(action: {
if state.keypair.privkey == nil {
notify(.logout, ())
} else {
confirm_logout = true
}
}, label: {
Label(NSLocalizedString("Sign out", comment: "Sidebar menu label to sign out of the account."), systemImage: "pip.exit")
.foregroundColor(textColor())
.frame(maxWidth: .infinity, alignment: .leading)
})
}
if state.is_privkey_user {
Section(NSLocalizedString("Delete", comment: "Section title for deleting the user")) {
Section(NSLocalizedString("Permanently Delete Account", comment: "Section title for deleting the user")) {
Button(NSLocalizedString("Delete Account", comment: "Button to delete the user's account."), role: .destructive) {
confirm_delete_account = true
delete_account_warning = true
}
}
}
@@ -217,6 +259,15 @@ struct ConfigView: View {
}
.navigationTitle(NSLocalizedString("Settings", comment: "Navigation title for Settings view."))
.navigationBarTitleDisplayMode(.large)
.alert(NSLocalizedString("WARNING:\n\nTHIS WILL SIGN AN EVENT THAT DELETES THIS ACCOUNT.\n\nYOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.\n\n ARE YOU SURE YOU WANT TO CONTINUE?", comment: "Alert for deleting the users account."), isPresented: $delete_account_warning) {
Button(NSLocalizedString("Cancel", comment: "Cancel deleting the user."), role: .cancel) {
delete_account_warning = false
}
Button(NSLocalizedString("Continue", comment: "Continue with deleting the user.")) {
confirm_delete_account = true
}
}
.alert(NSLocalizedString("Permanently Delete Account", comment: "Alert for deleting the users account."), isPresented: $confirm_delete_account) {
TextField(NSLocalizedString("Type DELETE to delete", comment: "Text field prompt asking user to type the word DELETE to confirm that they want to proceed with deleting their account. The all caps lock DELETE word should not be translated. Everything else should."), text: $delete_text)
Button(NSLocalizedString("Cancel", comment: "Cancel deleting the user."), role: .cancel) {
@@ -337,7 +388,8 @@ struct ConfigView_Previews: PreviewProvider {
func handle_string_amount(new_value: String) -> Int? {
let filtered = new_value.filter { Set("0123456789").contains($0) }
let digits = Set("0123456789")
let filtered = new_value.filter { digits.contains($0) }
if filtered == "" {
return nil
@@ -349,3 +401,9 @@ func handle_string_amount(new_value: String) -> Int? {
return amt
}
func clear_kingfisher_cache() -> Void {
KingfisherManager.shared.cache.clearMemoryCache()
KingfisherManager.shared.cache.clearDiskCache()
KingfisherManager.shared.cache.cleanExpiredDiskCache()
}
+26 -8
View File
@@ -19,7 +19,7 @@ struct DMChatView: View {
VStack(alignment: .leading) {
ForEach(Array(zip(dms.events, dms.events.indices)), id: \.0.id) { (ev, ind) in
DMView(event: dms.events[ind], damus_state: damus_state)
.event_context_menu(ev, keypair: damus_state.keypair, target_pubkey: ev.pubkey)
.contextMenu{MenuItems(event: ev, keypair: damus_state.keypair, target_pubkey: ev.pubkey, bookmarks: damus_state.bookmarks)}
}
EndBlock(height: 80)
}
@@ -181,13 +181,12 @@ struct DMChatView_Previews: PreviewProvider {
}
}
enum EncEncoding {
case base64
case bech32
}
func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keypair, created_at: Int64? = nil) -> NostrEvent?
{
guard let privkey = keypair.privkey else {
return nil
}
func encrypt_message(message: String, privkey: String, to_pk: String, encoding: EncEncoding = .base64) -> String? {
let iv = random_bytes(count: 16).bytes
guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: to_pk) else {
return nil
@@ -196,7 +195,26 @@ func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keyp
guard let enc_message = aes_encrypt(data: utf8_message, iv: iv, shared_sec: shared_sec) else {
return nil
}
let enc_content = encode_dm_base64(content: enc_message.bytes, iv: iv)
switch encoding {
case .base64:
return encode_dm_base64(content: enc_message.bytes, iv: iv)
case .bech32:
return encode_dm_bech32(content: enc_message.bytes, iv: iv)
}
}
func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keypair, created_at: Int64? = nil) -> NostrEvent?
{
guard let privkey = keypair.privkey else {
return nil
}
guard let enc_content = encrypt_message(message: message, privkey: privkey, to_pk: to_pk) else {
return nil
}
let created = created_at ?? Int64(Date().timeIntervalSince1970)
let ev = NostrEvent(content: enc_content, pubkey: keypair.pubkey, kind: 4, tags: tags, createdAt: created)
+12 -5
View File
@@ -21,14 +21,21 @@ struct DMView: View {
Spacer(minLength: UIScreen.main.bounds.width * 0.2)
}
let should_show_img = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
let should_show_img = should_show_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: .normal, artifacts: .just_content(event.get_content(damus_state.keypair.privkey)))
.foregroundColor(is_ours ? Color.white : Color.primary)
.padding(10)
.background(is_ours ? Color.accentColor : Color.secondary.opacity(0.15))
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: .normal, artifacts: .just_content(event.get_content(damus_state.keypair.privkey)), options: [])
.padding([.top, .leading, .trailing], 10)
.padding([.bottom], 25)
.background(VisualEffectView(effect: UIBlurEffect(style: .prominent))
.background(is_ours ? Color.accentColor.opacity(0.9) : Color.secondary.opacity(0.15))
)
.cornerRadius(8.0)
.tint(is_ours ? Color.white : Color.accentColor)
.overlay(Text(format_relative_time(event.created_at))
.font(.footnote)
.foregroundColor(.gray)
.opacity(0.8)
.offset(x: -10, y: -5), alignment: .bottomTrailing)
if !is_ours {
Spacer(minLength: UIScreen.main.bounds.width * 0.2)
}
+23 -3
View File
@@ -6,6 +6,7 @@
//
import SwiftUI
import Combine
let PPM_SIZE: CGFloat = 80.0
let BANNER_HEIGHT: CGFloat = 150.0;
@@ -65,6 +66,8 @@ struct EditMetadataView: View {
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
@State var confirm_ln_address: Bool = false
init (damus_state: DamusState) {
self.damus_state = damus_state
@@ -103,6 +106,10 @@ struct EditMetadataView: View {
damus_state.pool.send(.event(metadata_ev))
}
}
func is_ln_valid(ln: String) -> Bool {
return ln.contains("@") || ln.lowercased().starts(with: "lnurl")
}
var nip05_parts: NIP05? {
return NIP05.parse(nip05)
@@ -190,6 +197,9 @@ struct EditMetadataView: View {
TextField(NSLocalizedString("jb55@jb55.com", comment: "Placeholder example text for identifier used for NIP-05 verification."), text: $nip05)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
.onReceive(Just(nip05)) { newValue in
self.nip05 = newValue.trimmingCharacters(in: .whitespaces)
}
}, header: {
Text("NIP-05 Verification", comment: "Label for NIP-05 Verification section of user profile form.")
}, footer: {
@@ -201,12 +211,22 @@ struct EditMetadataView: View {
})
Button(NSLocalizedString("Save", comment: "Button for saving profile.")) {
save()
dismiss()
if !ln.isEmpty && !is_ln_valid(ln: ln) {
confirm_ln_address = true
} else {
save()
dismiss()
}
}
.alert(NSLocalizedString("Invalid Tip Address", comment: "Title of alerting as invalid tip address."), isPresented: $confirm_ln_address) {
Button(NSLocalizedString("Ok", comment: "Button to dismiss the alert.")) {
}
} message: {
Text("The address should either begin with LNURL or should look like an email address.", comment: "Giving the description of the alert message.")
}
}
}
.ignoresSafeArea()
.ignoresSafeArea(edges: .top)
}
}
+1 -1
View File
@@ -16,7 +16,7 @@ struct EventDetailView: View {
func scroll_after_load(thread: ThreadModel, proxy: ScrollViewProxy) {
if !thread.loading {
let id = thread.initial_event.id
let id = thread.event.id
scroll_to_event(scroller: proxy, id: id, delay: 0.1, animate: false)
}
}
+16 -26
View File
@@ -29,29 +29,29 @@ func eventviewsize_to_font(_ size: EventViewKind) -> Font {
struct EventView: View {
let event: NostrEvent
let has_action_bar: Bool
let options: EventViewOptions
let damus: DamusState
let pubkey: String
@EnvironmentObject var action_bar: ActionBarModel
init(damus: DamusState, event: NostrEvent, has_action_bar: Bool) {
init(damus: DamusState, event: NostrEvent, options: EventViewOptions) {
self.event = event
self.has_action_bar = has_action_bar
self.options = options
self.damus = damus
self.pubkey = event.pubkey
}
init(damus: DamusState, event: NostrEvent) {
self.event = event
self.has_action_bar = false
self.options = []
self.damus = damus
self.pubkey = event.pubkey
}
init(damus: DamusState, event: NostrEvent, pubkey: String) {
self.event = event
self.has_action_bar = false
self.options = [.no_action_bar]
self.damus = damus
self.pubkey = pubkey
}
@@ -60,17 +60,7 @@ struct EventView: View {
VStack {
if event.known_kind == .boost {
if let inner_ev = event.inner_event {
VStack(alignment: .leading) {
let prof = damus.profiles.lookup(id: event.pubkey)
let booster_profile = ProfileView(damus_state: damus, pubkey: event.pubkey)
NavigationLink(destination: booster_profile) {
Reposted(damus: damus, pubkey: event.pubkey, profile: prof)
}
.buttonStyle(PlainButtonStyle())
TextEvent(damus: damus, event: inner_ev, pubkey: inner_ev.pubkey, has_action_bar: has_action_bar, booster_pubkey: event.pubkey)
.padding([.top], 1)
}
RepostedEvent(damus: damus, event: event, inner_ev: inner_ev, options: options)
} else {
EmptyView()
}
@@ -81,15 +71,19 @@ struct EventView: View {
EmptyView()
}
} else {
TextEvent(damus: damus, event: event, pubkey: pubkey, has_action_bar: has_action_bar, booster_pubkey: nil)
.padding([.top], 6)
TextEvent(damus: damus, event: event, pubkey: pubkey, options: options)
//.padding([.top], 6)
}
}
}
}
// blame the porn bots for this code
func should_show_images(contacts: Contacts, ev: NostrEvent, our_pubkey: String, booster_pubkey: String? = nil) -> Bool {
func should_show_images(settings: UserSettingsStore, contacts: Contacts, ev: NostrEvent, our_pubkey: String, booster_pubkey: String? = nil) -> Bool {
if settings.always_show_images {
return true
}
if ev.pubkey == our_pubkey {
return true
}
@@ -126,9 +120,9 @@ extension View {
}
}
func event_context_menu(_ event: NostrEvent, keypair: Keypair, target_pubkey: String) -> some View {
func event_context_menu(_ event: NostrEvent, keypair: Keypair, target_pubkey: String, bookmarks: BookmarksManager) -> some View {
return self.contextMenu {
EventMenuContext(event: event, keypair: keypair, target_pubkey: target_pubkey)
EventMenuContext(event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks)
}
}
@@ -176,11 +170,7 @@ struct EventView_Previews: PreviewProvider {
EventView(damus: test_damus_state(), event: NostrEvent(content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool", pubkey: "pk"), show_friend_icon: true, size: .big)
*/
EventView(
damus: test_damus_state(),
event: test_event,
has_action_bar: true
)
EventView( damus: test_damus_state(), event: test_event )
}
.padding()
}
+19 -1
View File
@@ -13,6 +13,19 @@ struct BuilderEventView: View {
@State var event: NostrEvent?
@State var subscription_uuid: String = UUID().description
init(damus: DamusState, event: NostrEvent) {
_event = State(initialValue: event)
self.damus = damus
self.event_id = event.id
}
init(damus: DamusState, event_id: String) {
let event = damus.events.lookup(event_id)
self.event_id = event_id
self.damus = damus
_event = State(initialValue: event)
}
func unsubscribe() {
damus.pool.unsubscribe(sub_id: subscription_uuid)
}
@@ -62,7 +75,9 @@ struct BuilderEventView: View {
VStack {
if let event = event {
let ev = event.inner_event ?? event
NavigationLink(destination: BuildThreadV2View(damus: damus, event_id: ev.id)) {
let thread = ThreadModel(event: ev, damus_state: damus)
let dest = ThreadView(state: damus, thread: thread)
NavigationLink(destination: dest) {
EmbeddedEventView(damus_state: damus, event: event)
.padding(8)
}.buttonStyle(.plain)
@@ -76,6 +91,9 @@ struct BuilderEventView: View {
.stroke(Color.gray.opacity(0.2), lineWidth: 1.0)
)
.onAppear {
guard event == nil else {
return
}
self.load()
}
}
+12 -4
View File
@@ -18,12 +18,20 @@ struct EmbeddedEventView: View {
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)
.padding([.bottom], 4)
}
.minimumScaleFactor(0.75)
.lineLimit(1)
EventProfile(damus_state: damus_state, pubkey: pubkey, profile: profile, size: .small)
EventBody(damus_state: damus_state, event: event, size: .small)
EventBody(damus_state: damus_state, event: event, size: .small, options: [.truncate_content])
}
.event_context_menu(event, keypair: damus_state.keypair, target_pubkey: pubkey)
}
}
+6 -8
View File
@@ -12,12 +12,14 @@ struct EventBody: View {
let event: NostrEvent
let size: EventViewKind
let should_show_img: Bool
let options: EventViewOptions
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind, should_show_img: Bool? = nil) {
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind, should_show_img: Bool? = nil, options: EventViewOptions) {
self.damus_state = damus_state
self.event = event
self.size = size
self.should_show_img = should_show_img ?? should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
self.options = options
self.should_show_img = should_show_img ?? should_show_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
}
var content: String {
@@ -25,17 +27,13 @@ struct EventBody: View {
}
var body: some View {
if event_is_reply(event, privkey: damus_state.keypair.privkey) {
ReplyDescription(event: event, profiles: damus_state.profiles)
}
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: size, artifacts: .just_content(content))
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: size, artifacts: .just_content(content), options: options)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
struct EventBody_Previews: PreviewProvider {
static var previews: some View {
EventBody(damus_state: test_damus_state(), event: test_event, size: .normal)
EventBody(damus_state: test_damus_state(), event: test_event, size: .normal, options: [])
}
}
+86 -56
View File
@@ -11,71 +11,101 @@ struct EventMenuContext: View {
let event: NostrEvent
let keypair: Keypair
let target_pubkey: String
let bookmarks: BookmarksManager
@Environment(\.colorScheme) var colorScheme
var body: some View {
HStack {
Menu {
MenuItems(event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks)
} label: {
Label("", systemImage: "ellipsis")
.foregroundColor(Color.gray)
}
}
.contentShape(Rectangle())
.onTapGesture {}
}
}
struct MenuItems: View {
let event: NostrEvent
let keypair: Keypair
let target_pubkey: String
let bookmarks: BookmarksManager
@State private var isBookmarked: Bool = false
var body: some View {
Button {
UIPasteboard.general.string = event.get_content(keypair.privkey)
} label: {
Label(NSLocalizedString("Copy Text", comment: "Context menu option for copying the text from an note."), systemImage: "doc.on.doc")
}
Button {
UIPasteboard.general.string = bech32_pubkey(target_pubkey)
} label: {
Label(NSLocalizedString("Copy User Pubkey", comment: "Context menu option for copying the ID of the user who created the note."), systemImage: "person")
}
Button {
UIPasteboard.general.string = bech32_note_id(event.id) ?? event.id
} label: {
Label(NSLocalizedString("Copy Note ID", comment: "Context menu option for copying the ID of the note."), systemImage: "note.text")
}
Button {
UIPasteboard.general.string = event_to_json(ev: event)
} label: {
Label(NSLocalizedString("Copy Note JSON", comment: "Context menu option for copying the JSON text from the note."), systemImage: "square.on.square")
}
init(event: NostrEvent, keypair: Keypair, target_pubkey: String, bookmarks: BookmarksManager) {
let bookmarked = bookmarks.isBookmarked(event)
self._isBookmarked = State(initialValue: bookmarked)
Button {
let event_json = event_to_json(ev: event)
BookmarksManager(pubkey: keypair.pubkey).updateBookmark(event_json)
isBookmarked = BookmarksManager(pubkey: keypair.pubkey).isBookmarked(event_json)
notify(.update_bookmarks, event)
} label: {
let imageName = isBookmarked ? "bookmark.fill" : "bookmark"
let removeBookmarkString = NSLocalizedString("Remove Bookmark", comment: "Context menu option for removing a note bookmark.")
let addBookmarkString = NSLocalizedString("Add Bookmark", comment: "Context menu option for adding a note bookmark.")
Label(isBookmarked ? removeBookmarkString : addBookmarkString, systemImage: imageName)
}
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
isBookmarked = BookmarksManager(pubkey: keypair.pubkey).isBookmarked(event_to_json(ev: event))
}
}
self.bookmarks = bookmarks
self.event = event
self.keypair = keypair
self.target_pubkey = target_pubkey
}
var body: some View {
Button {
NotificationCenter.default.post(name: .broadcast_event, object: event)
} label: {
Label(NSLocalizedString("Broadcast", comment: "Context menu option for broadcasting the user's note to all of the user's connected relay servers."), systemImage: "globe")
}
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
if keypair.pubkey != target_pubkey && keypair.privkey != nil {
Button(role: .destructive) {
let target: ReportTarget = .note(ReportNoteTarget(pubkey: target_pubkey, note_id: event.id))
notify(.report, target)
Group {
Button {
UIPasteboard.general.string = event.get_content(keypair.privkey)
} label: {
Label(NSLocalizedString("Report", comment: "Context menu option for reporting content."), systemImage: "exclamationmark.bubble")
Label(NSLocalizedString("Copy Text", comment: "Context menu option for copying the text from an note."), systemImage: "doc.on.doc")
}
Button(role: .destructive) {
notify(.block, target_pubkey)
Button {
UIPasteboard.general.string = bech32_pubkey(target_pubkey)
} label: {
Label(NSLocalizedString("Block", comment: "Context menu option for blocking users."), systemImage: "exclamationmark.octagon")
Label(NSLocalizedString("Copy User Pubkey", comment: "Context menu option for copying the ID of the user who created the note."), systemImage: "person")
}
Button {
UIPasteboard.general.string = bech32_note_id(event.id) ?? event.id
} label: {
Label(NSLocalizedString("Copy Note ID", comment: "Context menu option for copying the ID of the note."), systemImage: "note.text")
}
Button {
UIPasteboard.general.string = event_to_json(ev: event)
} label: {
Label(NSLocalizedString("Copy Note JSON", comment: "Context menu option for copying the JSON text from the note."), systemImage: "square.on.square")
}
Button {
self.bookmarks.updateBookmark(event)
isBookmarked = self.bookmarks.isBookmarked(event)
} label: {
let imageName = isBookmarked ? "bookmark.fill" : "bookmark"
let removeBookmarkString = NSLocalizedString("Remove Bookmark", comment: "Context menu option for removing a note bookmark.")
let addBookmarkString = NSLocalizedString("Add Bookmark", comment: "Context menu option for adding a note bookmark.")
Label(isBookmarked ? removeBookmarkString : addBookmarkString, systemImage: imageName)
}
Button {
NotificationCenter.default.post(name: .broadcast_event, object: event)
} label: {
Label(NSLocalizedString("Broadcast", comment: "Context menu option for broadcasting the user's note to all of the user's connected relay servers."), systemImage: "globe")
}
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
if keypair.pubkey != target_pubkey && keypair.privkey != nil {
Button(role: .destructive) {
let target: ReportTarget = .note(ReportNoteTarget(pubkey: target_pubkey, note_id: event.id))
notify(.report, target)
} label: {
Label(NSLocalizedString("Report", comment: "Context menu option for reporting content."), systemImage: "exclamationmark.bubble")
}
Button(role: .destructive) {
notify(.block, target_pubkey)
} label: {
Label(NSLocalizedString("Block", comment: "Context menu option for blocking users."), systemImage: "exclamationmark.octagon")
}
}
}
}
+5 -17
View File
@@ -13,18 +13,14 @@ struct MutedEventView: View {
let scroller: ScrollViewProxy?
let selected: Bool
@Binding var nav_target: String?
@Binding var navigating: Bool
@State var shown: Bool
@Environment(\.colorScheme) var colorScheme
init(damus_state: DamusState, event: NostrEvent, scroller: ScrollViewProxy?, nav_target: Binding<String?>, navigating: Binding<Bool>, selected: Bool) {
init(damus_state: DamusState, event: NostrEvent, scroller: ScrollViewProxy?, selected: Bool) {
self.damus_state = damus_state
self.event = event
self.scroller = scroller
self.selected = selected
self._nav_target = nav_target
self._navigating = navigating
self._shown = State(initialValue: should_show_event(contacts: damus_state.contacts, ev: event))
}
@@ -55,17 +51,9 @@ struct MutedEventView: View {
var Event: some View {
Group {
if selected {
SelectedEventView(damus: damus_state, event: event)
SelectedEventView(damus: damus_state, event: event, size: .selected)
} else {
EventView(damus: damus_state, event: event, has_action_bar: true)
.onTapGesture {
nav_target = event.id
navigating = true
}
.onAppear {
// TODO: find another solution to prevent layout shifting and layout blocking on large responses
scroller?.scrollTo("main", anchor: .bottom)
}
EventView(damus: damus_state, event: event)
}
}
}
@@ -101,12 +89,12 @@ struct MutedEventView: View {
}
struct MutedEventView_Previews: PreviewProvider {
@State static var nav_target: String? = nil
@State static var nav_target: NostrEvent = test_event
@State static var navigating: Bool = false
static var previews: some View {
MutedEventView(damus_state: test_damus_state(), event: test_event, scroller: nil, nav_target: $nav_target, navigating: $navigating, selected: false)
MutedEventView(damus_state: test_damus_state(), event: test_event, scroller: nil, selected: false)
.frame(width: .infinity, height: 50)
}
}
+7 -5
View File
@@ -39,19 +39,21 @@ func reply_desc(profiles: Profiles, event: NostrEvent, locale: Locale = Locale.c
let names: [String] = pubkeys.map {
let prof = profiles.lookup(id: $0)
return Profile.displayName(profile: prof, pubkey: $0)
return Profile.displayName(profile: prof, pubkey: $0).username
}
let uniqueNames = NSOrderedSet(array: names).array as! [String]
if names.count > 1 {
if uniqueNames.count > 1 {
let othersCount = n - pubkeys.count
if othersCount == 0 {
return String(format: NSLocalizedString("Replying to %@ & %@", bundle: bundle, comment: "Label to indicate that the user is replying to 2 users."), locale: locale, names[0], names[1])
return String(format: NSLocalizedString("Replying to %@ & %@", bundle: bundle, comment: "Label to indicate that the user is replying to 2 users."), locale: locale, uniqueNames[0], uniqueNames[1])
} else {
return String(format: bundle.localizedString(forKey: "replying_to_two_and_others", value: nil, table: nil), locale: locale, othersCount, names[0], names[1])
return String(format: localizedStringFormat(key: "replying_to_two_and_others", locale: locale), locale: locale, othersCount, uniqueNames[0], uniqueNames[1])
}
}
return String(format: NSLocalizedString("Replying to %@", bundle: bundle, comment: "Label to indicate that the user is replying to 1 user."), locale: locale, names[0])
return String(format: NSLocalizedString("Replying to %@", bundle: bundle, comment: "Label to indicate that the user is replying to 1 user."), locale: locale, uniqueNames[0])
}
+18 -5
View File
@@ -10,6 +10,7 @@ import SwiftUI
struct SelectedEventView: View {
let damus: DamusState
let event: NostrEvent
let size: EventViewKind
var pubkey: String {
event.pubkey
@@ -17,9 +18,10 @@ struct SelectedEventView: View {
@StateObject var bar: ActionBarModel
init(damus: DamusState, event: NostrEvent) {
init(damus: DamusState, event: NostrEvent, size: EventViewKind) {
self.damus = damus
self.event = event
self.size = size
self._bar = StateObject(wrappedValue: make_actionbar_model(ev: event.id, damus: damus))
}
@@ -28,8 +30,19 @@ struct SelectedEventView: View {
let profile = damus.profiles.lookup(id: pubkey)
VStack(alignment: .leading) {
EventProfile(damus_state: damus, pubkey: pubkey, profile: profile, size: .normal)
EventBody(damus_state: damus, event: event, size: .selected)
HStack {
EventProfile(damus_state: damus, pubkey: pubkey, profile: profile, size: .normal)
Spacer()
EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks)
.padding([.bottom], 4)
}
.minimumScaleFactor(0.75)
.lineLimit(1)
EventBody(damus_state: damus, event: event, size: size, options: [])
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
BuilderEventView(damus: damus, event_id: mention.ref.id)
@@ -60,14 +73,14 @@ struct SelectedEventView: View {
self.bar.update(damus: self.damus, evid: target)
}
.padding([.leading], 2)
.event_context_menu(event, keypair: damus.keypair, target_pubkey: event.pubkey)
.compositingGroup()
}
}
}
struct SelectedEventView_Previews: PreviewProvider {
static var previews: some View {
SelectedEventView(damus: test_damus_state(), event: test_event)
SelectedEventView(damus: test_damus_state(), event: test_event, size: .selected)
.padding()
}
}
+153 -41
View File
@@ -7,62 +7,161 @@
import SwiftUI
struct EventViewOptions: OptionSet {
let rawValue: UInt8
static let no_action_bar = EventViewOptions(rawValue: 1 << 0)
static let no_replying_to = EventViewOptions(rawValue: 1 << 1)
static let no_images = EventViewOptions(rawValue: 1 << 2)
static let wide = EventViewOptions(rawValue: 1 << 3)
static let truncate_content = EventViewOptions(rawValue: 1 << 4)
static let pad_content = EventViewOptions(rawValue: 1 << 5)
}
struct TextEvent: View {
let damus: DamusState
let event: NostrEvent
let pubkey: String
let has_action_bar: Bool
let booster_pubkey: String?
let options: EventViewOptions
var has_action_bar: Bool {
!options.contains(.no_action_bar)
}
var body: some View {
HStack(alignment: .top) {
let profile = damus.profiles.lookup(id: pubkey)
let is_anon = event_is_anonymous(ev: event)
VStack {
MaybeAnonPfpView(state: damus, is_anon: is_anon, pubkey: pubkey)
Spacer()
Group {
if options.contains(.wide) {
WideStyle
} else {
ThreadedStyle
}
VStack(alignment: .leading) {
HStack(alignment: .center) {
let pk = is_anon ? "anon" : pubkey
EventProfileName(pubkey: pk, profile: profile, damus: damus, show_friend_confirmed: true, size: .normal)
Text(verbatim: "\(format_relative_time(event.created_at))")
.foregroundColor(.gray)
Spacer()
}
EventBody(damus_state: damus, event: event, size: .normal)
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
BuilderEventView(damus: damus, event_id: mention.ref.id)
}
if has_action_bar {
Rectangle().frame(height: 2).opacity(0)
EventActionBar(damus_state: damus, event: event)
.padding([.top], 4)
}
}
.padding([.leading], 2)
}
.contentShape(Rectangle())
.background(event_validity_color(event.validity))
.id(event.id)
.frame(maxWidth: .infinity, minHeight: PFP_SIZE)
.padding([.bottom], 2)
.event_context_menu(event, keypair: damus.keypair, target_pubkey: pubkey)
}
}
func Pfp(is_anon: Bool) -> some View {
MaybeAnonPfpView(state: damus, is_anon: is_anon, pubkey: pubkey)
}
func TopPart(is_anon: Bool) -> some View {
HStack(alignment: .center, spacing: 0) {
ProfileName(is_anon: is_anon)
TimeDot
Time
Spacer()
ContextButton
}
.lineLimit(1)
}
var ReplyPart: some View {
Group {
if event_is_reply(event, privkey: damus.keypair.privkey) {
ReplyDescription(event: event, profiles: damus.profiles)
} else {
EmptyView()
}
}
}
var WideStyle: some View {
VStack(alignment: .leading) {
let is_anon = event_is_anonymous(ev: event)
HStack(spacing: 10) {
Pfp(is_anon: is_anon)
VStack {
TopPart(is_anon: is_anon)
ReplyPart
}
}
.padding(.horizontal)
struct TextEvent_Previews: PreviewProvider {
static var previews: some View {
TextEvent(damus: test_damus_state(), event: test_event, pubkey: "pk", has_action_bar: true, booster_pubkey: nil)
EvBody(options: [.truncate_content, .pad_content])
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
Mention(mention)
.padding(.horizontal)
}
if has_action_bar {
//EmptyRect
ActionBar
.padding(.horizontal)
}
}
}
var TimeDot: some View {
Text(verbatim: "")
.font(.footnote)
.foregroundColor(.gray)
}
var Time: some View {
Text(verbatim: "\(format_relative_time(event.created_at))")
.font(.system(size: 16))
.foregroundColor(.gray)
}
var ContextButton: some View {
EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks)
.padding([.bottom], 4)
}
func ProfileName(is_anon: Bool) -> some View {
let profile = damus.profiles.lookup(id: pubkey)
let pk = is_anon ? "anon" : pubkey
return EventProfileName(pubkey: pk, profile: profile, damus: damus, show_friend_confirmed: true, size: .normal)
}
func EvBody(options: EventViewOptions) -> some View {
return EventBody(damus_state: damus, event: event, size: .normal, options: options)
}
func Mention(_ mention: Mention) -> some View {
return BuilderEventView(damus: damus, event_id: mention.ref.id)
}
var ActionBar: some View {
return EventActionBar(damus_state: damus, event: event)
.padding([.top], 4)
}
var EmptyRect: some View {
return Rectangle().frame(height: 2).opacity(0)
}
var ThreadedStyle: some View {
HStack(alignment: .top) {
let is_anon = event_is_anonymous(ev: event)
VStack {
Pfp(is_anon: is_anon)
Spacer()
}
VStack(alignment: .leading) {
TopPart(is_anon: is_anon)
ReplyPart
EvBody(options: [])
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
Mention(mention)
}
if has_action_bar {
EmptyRect
ActionBar
}
}
.padding([.leading], 2)
}
}
}
@@ -80,3 +179,16 @@ func event_has_tag(ev: NostrEvent, tag: String) -> Bool {
func event_is_anonymous(ev: NostrEvent) -> Bool {
return ev.known_kind == .zap_request && event_has_tag(ev: ev, tag: "anon")
}
struct TextEvent_Previews: PreviewProvider {
static var previews: some View {
VStack(spacing: 20) {
TextEvent(damus: test_damus_state(), event: test_event, pubkey: "pk", options: [])
.frame(height: 400)
TextEvent(damus: test_damus_state(), event: test_event, pubkey: "pk", options: [.wide])
.frame(height: 400)
}
}
}
+22
View File
@@ -0,0 +1,22 @@
//
// WideEventView.swift
// damus
//
// Created by William Casarin on 2023-03-23.
//
import SwiftUI
struct WideEventView: View {
let event: NostrEvent
var body: some View {
EmptyView()
}
}
struct WideEventView_Previews: PreviewProvider {
static var previews: some View {
WideEventView(event: test_event)
}
}
+32 -9
View File
@@ -13,21 +13,44 @@ struct ZapEvent: View {
var body: some View {
VStack(alignment: .leading) {
Text("⚡️ \(format_msats(zap.invoice.amount))", comment: "Text indicating the zap amount. i.e. number of satoshis that were tipped to a user")
.font(.headline)
.padding([.top], 2)
HStack(alignment: .center) {
Text("⚡️ \(format_msats(zap.invoice.amount))", comment: "Text indicating the zap amount. i.e. number of satoshis that were tipped to a user")
.font(.headline)
.padding([.top], 2)
if zap.private_request != nil {
Image(systemName: "lock.fill")
.foregroundColor(Color("DamusGreen"))
.help(NSLocalizedString("Only you can see this message and who sent it.", comment: "Help text on green lock icon that explains that only the current user can see the message of a zap event and who sent the zap."))
}
}
TextEvent(damus: damus, event: zap.request.ev, pubkey: zap.request.ev.pubkey, has_action_bar: false, booster_pubkey: nil)
.padding([.top], 1)
if let priv = zap.private_request {
TextEvent(damus: damus, event: priv, pubkey: priv.pubkey, options: [.no_action_bar, .no_replying_to])
.padding([.top], 1)
} else {
TextEvent(damus: damus, event: zap.request.ev, pubkey: zap.request.ev.pubkey, options: [.no_action_bar, .no_replying_to])
.padding([.top], 1)
}
}
}
}
/*
let test_zap_invoice = ZapInvoice(description: .description("description"), amount: 10000, string: "lnbc1", expiry: 1000000, payment_hash: Data(), created_at: 1000000)
let test_zap_request_ev = NostrEvent(content: "hi", pubkey: "pk", kind: 9734)
let test_zap_request = ZapRequest(ev: test_zap_request_ev)
let test_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), request: test_zap_request, is_anon: false, private_request: nil)
let test_private_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), request: test_zap_request, is_anon: false, private_request: test_event)
struct ZapEvent_Previews: PreviewProvider {
static var previews: some View {
ZapEvent()
VStack {
ZapEvent(damus: test_damus_state(), zap: test_zap)
ZapEvent(damus: test_damus_state(), zap: test_private_zap)
}
}
}
*/
@@ -0,0 +1,51 @@
//
// CarouselImageContainerView.swift
// damus
//
// Created by William Casarin on 2023-03-23.
//
import SwiftUI
import Kingfisher
// lots of overlap between this and ImageContainerView
struct ImageContainerView: View {
let url: URL?
@State private var image: UIImage?
@State private var showShareSheet = false
private struct ImageHandler: ImageModifier {
@Binding var handler: UIImage?
func modify(_ image: UIImage) -> UIImage {
handler = image
return image
}
}
var body: some View {
KFAnimatedImage(url)
.imageContext(.note)
.configure { view in
view.framePreloadCount = 3
}
.imageModifier(ImageHandler(handler: $image))
.clipped()
.modifier(ImageContextMenuModifier(url: url, image: image, showShareSheet: $showShareSheet))
.sheet(isPresented: $showShareSheet) {
ShareSheet(activityItems: [url])
}
}
}
let test_image_url = URL(string: "https://jb55.com/red-me.jpg")!
struct ImageContainerView_Previews: PreviewProvider {
static var previews: some View {
ImageContainerView(url: test_image_url)
}
}
@@ -0,0 +1,43 @@
//
// ImageContextMenuModifier.swift
// damus
//
// Created by William Casarin on 2023-03-23.
//
import Foundation
import SwiftUI
import UIKit
struct ImageContextMenuModifier: ViewModifier {
let url: URL?
let image: UIImage?
@Binding var showShareSheet: Bool
func body(content: Content) -> some View {
return content.contextMenu {
Button {
UIPasteboard.general.url = url
} label: {
Label(NSLocalizedString("Copy Image URL", comment: "Context menu option to copy the URL of an image into clipboard."), systemImage: "doc.on.doc")
}
if let someImage = image {
Button {
UIPasteboard.general.image = someImage
} label: {
Label(NSLocalizedString("Copy Image", comment: "Context menu option to copy an image into clipboard."), systemImage: "photo.on.rectangle")
}
Button {
UIImageWriteToSavedPhotosAlbum(someImage, nil, nil, nil)
} label: {
Label(NSLocalizedString("Save Image", comment: "Context menu option to save an image."), systemImage: "square.and.arrow.down")
}
}
Button {
showShareSheet = true
} label: {
Label(NSLocalizedString("Share", comment: "Button to share an image."), systemImage: "square.and.arrow.up")
}
}
}
}
+102
View File
@@ -0,0 +1,102 @@
//
// ImageView.swift
// damus
//
// Created by William Casarin on 2023-03-23.
//
import SwiftUI
struct ImageView: View {
let urls: [URL?]
@Environment(\.presentationMode) var presentationMode
@State private var selectedIndex = 0
@State var showMenu = true
var navBarView: some View {
VStack {
HStack {
/*
Text(urls[selectedIndex]?.lastPathComponent ?? "")
.bold()
*/
Spacer()
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Image(systemName: "xmark")
})
}
.padding()
}
}
var tabViewIndicator: some View {
HStack(spacing: 10) {
ForEach(urls.indices, id: \.self) { index in
Capsule()
.fill(index == selectedIndex ? Color(UIColor.label) : Color.secondary)
.frame(width: 7, height: 7)
}
}
.padding()
.background(.regularMaterial)
.clipShape(Capsule())
}
var body: some View {
ZStack {
Color(.systemBackground)
.ignoresSafeArea()
TabView(selection: $selectedIndex) {
ForEach(urls.indices, id: \.self) { index in
ZoomableScrollView {
ImageContainerView(url: urls[index])
.aspectRatio(contentMode: .fit)
.padding(.top, Theme.safeAreaInsets?.top)
.padding(.bottom, Theme.safeAreaInsets?.bottom)
}
.modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
presentationMode.wrappedValue.dismiss()
}))
.ignoresSafeArea()
.tag(index)
}
}
.ignoresSafeArea()
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.gesture(TapGesture(count: 2).onEnded {
// Prevents menu from hiding on double tap
})
.gesture(TapGesture(count: 1).onEnded {
showMenu.toggle()
})
.overlay(
VStack {
if showMenu {
navBarView
Spacer()
if (urls.count > 1) {
tabViewIndicator
}
}
}
.animation(.easeInOut, value: showMenu)
.padding(.bottom, Theme.safeAreaInsets?.bottom)
)
}
}
}
struct ImageView_Previews: PreviewProvider {
static var previews: some View {
ImageView(urls: [URL(string: "https://jb55.com/red-me.jpg")])
}
}
+116 -38
View File
@@ -28,47 +28,42 @@ struct NoteContentView: View {
let show_images: Bool
let size: EventViewKind
let preview_height: CGFloat?
let options: EventViewOptions
@State var artifacts: NoteArtifacts
@State var preview: LinkViewRepresentable?
init(damus_state: DamusState, event: NostrEvent, show_images: Bool, size: EventViewKind, artifacts: NoteArtifacts) {
init(damus_state: DamusState, event: NostrEvent, show_images: Bool, size: EventViewKind, artifacts: NoteArtifacts, options: EventViewOptions) {
self.damus_state = damus_state
self.event = event
self.show_images = show_images
self.size = size
self.options = options
self._artifacts = State(initialValue: artifacts)
self.preview_height = lookup_cached_preview_size(previews: damus_state.previews, evid: event.id)
self._preview = State(initialValue: load_cached_preview(previews: damus_state.previews, evid: event.id))
self._artifacts = State(initialValue: render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey))
}
func MainContent() -> some View {
return VStack(alignment: .leading) {
if size == .selected {
SelectableText(attributedString: artifacts.content)
TranslateView(damus_state: damus_state, event: event)
} else {
Text(artifacts.content)
.font(eventviewsize_to_font(size))
.fixedSize(horizontal: false, vertical: true)
}
if show_images && artifacts.images.count > 0 {
ImageCarousel(urls: artifacts.images)
} else if !show_images && artifacts.images.count > 0 {
ZStack {
ImageCarousel(urls: artifacts.images)
Blur()
.disabled(true)
}
.cornerRadius(10)
}
if artifacts.invoices.count > 0 {
InvoicesView(our_pubkey: damus_state.keypair.pubkey, invoices: artifacts.invoices)
}
var truncate: Bool {
return options.contains(.truncate_content)
}
var with_padding: Bool {
return options.contains(.pad_content)
}
var truncatedText: some View {
TruncatedText(text: artifacts.content, maxChars: (truncate ? 280 : nil))
.font(eventviewsize_to_font(size))
}
var invoicesView: some View {
InvoicesView(our_pubkey: damus_state.keypair.pubkey, invoices: artifacts.invoices)
}
var previewView: some View {
Group {
if let preview = self.preview, show_images {
if let preview_height {
preview
@@ -83,8 +78,51 @@ struct NoteContentView: View {
}
}
var MainContent: some View {
VStack(alignment: .leading) {
if size == .selected {
SelectableText(attributedString: artifacts.content)
TranslateView(damus_state: damus_state, event: event)
} else {
if with_padding {
truncatedText
.padding(.horizontal)
} else {
truncatedText
}
}
if show_images && artifacts.images.count > 0 {
ImageCarousel(urls: artifacts.images)
} else if !show_images && artifacts.images.count > 0 {
ZStack {
ImageCarousel(urls: artifacts.images)
Blur()
.disabled(true)
}
//.cornerRadius(10)
}
if artifacts.invoices.count > 0 {
if with_padding {
invoicesView
.padding(.horizontal)
} else {
invoicesView
}
}
if with_padding {
previewView.padding(.horizontal)
} else {
previewView
}
}
}
var body: some View {
MainContent()
MainContent
.onReceive(handle_notify(.profile_updated)) { notif in
let profile = notif.object as! ProfileUpdate
let blocks = event.blocks(damus_state.keypair.privkey)
@@ -139,15 +177,15 @@ struct NoteContentView: View {
func hashtag_str(_ htag: String) -> AttributedString {
var attributedString = AttributedString(stringLiteral: "#\(htag)")
attributedString.link = URL(string: "nostr:t:\(htag)")
attributedString.foregroundColor = .purple
attributedString.link = URL(string: "damus:t:\(htag)")
attributedString.foregroundColor = Color("DamusPurple")
return attributedString
}
func url_str(_ url: URL) -> AttributedString {
var attributedString = AttributedString(stringLiteral: url.absoluteString)
attributedString.link = url
attributedString.foregroundColor = .purple
attributedString.foregroundColor = Color("DamusPurple")
return attributedString
}
@@ -156,16 +194,16 @@ func mention_str(_ m: Mention, profiles: Profiles) -> AttributedString {
case .pubkey:
let pk = m.ref.ref_id
let profile = profiles.lookup(id: pk)
let disp = Profile.displayName(profile: profile, pubkey: pk)
let disp = Profile.displayName(profile: profile, pubkey: pk).username
var attributedString = AttributedString(stringLiteral: "@\(disp)")
attributedString.link = URL(string: "nostr:\(encode_pubkey_uri(m.ref))")
attributedString.foregroundColor = .purple
attributedString.link = URL(string: "damus:\(encode_pubkey_uri(m.ref))")
attributedString.foregroundColor = Color("DamusPurple")
return attributedString
case .event:
let bevid = bech32_note_id(m.ref.ref_id) ?? m.ref.ref_id
var attributedString = AttributedString(stringLiteral: "@\(abbrev_pubkey(bevid))")
attributedString.link = URL(string: "nostr:\(encode_event_id_uri(m.ref))")
attributedString.foregroundColor = .purple
attributedString.link = URL(string: "damus:\(encode_event_id_uri(m.ref))")
attributedString.foregroundColor = Color("DamusPurple")
return attributedString
}
}
@@ -175,7 +213,7 @@ struct NoteContentView_Previews: PreviewProvider {
let state = test_damus_state()
let content = "hi there ¯\\_(ツ)_/¯ https://jb55.com/s/Oct12-150217.png 5739a762ef6124dd.jpg"
let artifacts = NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: [])
NoteContentView(damus_state: state, event: NostrEvent(content: content, pubkey: "pk"), show_images: true, size: .normal, artifacts: artifacts)
NoteContentView(damus_state: state, event: NostrEvent(content: content, pubkey: "pk"), show_images: true, size: .normal, artifacts: artifacts, options: [])
}
}
@@ -202,6 +240,7 @@ struct NoteArtifacts {
func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -> NoteArtifacts {
let blocks = ev.blocks(privkey)
return render_blocks(blocks: blocks, profiles: profiles, privkey: privkey)
}
@@ -209,9 +248,17 @@ func render_blocks(blocks: [Block], profiles: Profiles, privkey: String?) -> Not
var invoices: [Invoice] = []
var img_urls: [URL] = []
var link_urls: [URL] = []
let one_note_ref = blocks
.filter({ $0.is_note_mention })
.count == 1
let txt: AttributedString = blocks.reduce("") { str, block in
switch block {
case .mention(let m):
if m.type == .event && one_note_ref {
return str
}
return str + mention_str(m, profiles: profiles)
case .text(let txt):
return str + AttributedString(stringLiteral: txt)
@@ -238,7 +285,8 @@ func render_blocks(blocks: [Block], profiles: Profiles, privkey: String?) -> Not
func is_image_url(_ url: URL) -> Bool {
let str = url.lastPathComponent.lowercased()
return str.hasSuffix("png") || str.hasSuffix("jpg") || str.hasSuffix("jpeg") || str.hasSuffix("gif")
let isUrl = str.hasSuffix(".png") || str.hasSuffix(".jpg") || str.hasSuffix(".jpeg") || str.hasSuffix(".gif")
return isUrl
}
func lookup_cached_preview_size(previews: PreviewCache, evid: String) -> CGFloat? {
@@ -261,3 +309,33 @@ func load_cached_preview(previews: PreviewCache, evid: String) -> LinkViewRepres
return LinkViewRepresentable(meta: .linkmeta(meta))
}
struct TruncatedText: View {
let text: AttributedString
let maxChars: Int?
var body: some View {
let truncatedAttributedString: AttributedString? = getTruncatedString()
Text(truncatedAttributedString ?? text)
.fixedSize(horizontal: false, vertical: true)
if truncatedAttributedString != nil {
Spacer()
Button(NSLocalizedString("Show more", comment: "Button to show entire note.")) { }
.allowsHitTesting(false)
}
}
func getTruncatedString() -> AttributedString? {
guard let maxChars = maxChars else { return nil }
let nsAttributedString = NSAttributedString(text)
if nsAttributedString.length < maxChars { return nil }
let range = NSRange(location: 0, length: maxChars)
let truncatedAttributedString = nsAttributedString.attributedSubstring(from: range)
return AttributedString(truncatedAttributedString) + "..."
}
}
+51 -18
View File
@@ -14,6 +14,19 @@ enum EventGroupType {
case zap(ZapGroup)
case profile_zap(ZapGroup)
var zap_group: ZapGroup? {
switch self {
case .profile_zap(let grp):
return grp
case .zap(let grp):
return grp
case .reaction:
return nil
case .repost:
return nil
}
}
var events: [NostrEvent] {
switch self {
case .repost(let grp):
@@ -46,10 +59,28 @@ func determine_reacting_to(our_pubkey: String, ev: NostrEvent?) -> ReactingTo {
return .tagged_in
}
func event_author_name(profiles: Profiles, _ ev: NostrEvent) -> String {
let alice_pk = ev.pubkey
let alice_prof = profiles.lookup(id: alice_pk)
return Profile.displayName(profile: alice_prof, pubkey: alice_pk)
func event_author_name(profiles: Profiles, pubkey: String) -> String {
let alice_prof = profiles.lookup(id: pubkey)
return Profile.displayName(profile: alice_prof, pubkey: pubkey).username
}
func event_group_author_name(profiles: Profiles, ind: Int, group: EventGroupType) -> String {
if let zapgrp = group.zap_group {
let zap = zapgrp.zaps[ind]
if let privzap = zap.private_request {
return event_author_name(profiles: profiles, pubkey: privzap.pubkey)
}
if zap.is_anon {
return "Anonymous"
}
return event_author_name(profiles: profiles, pubkey: zap.request.ev.pubkey)
} else {
let ev = group.events[ind]
return event_author_name(profiles: profiles, pubkey: ev.pubkey)
}
}
/**
@@ -90,30 +121,30 @@ func event_author_name(profiles: Profiles, _ ev: NostrEvent) -> String {
"zapped_your_profile_3" - returned when 3 or more zaps occurred to the current user's profile
*/
func reacting_to_text(profiles: Profiles, our_pubkey: String, group: EventGroupType, ev: NostrEvent?, locale: Locale? = nil) -> String {
if group.events.count == 0 {
return "??"
}
let verb = reacting_to_verb(group: group)
let reacting_to = determine_reacting_to(our_pubkey: our_pubkey, ev: ev)
let localization_key = "\(verb)_\(reacting_to)_\(min(group.events.count, 3))"
let bundle = bundleForLocale(locale: locale)
let format = localizedStringFormat(key: localization_key, locale: locale)
switch group.events.count {
case 0:
return NSLocalizedString("??", comment: "")
case 1:
let ev = group.events.first!
let profile = profiles.lookup(id: ev.pubkey)
let display_name = Profile.displayName(profile: profile, pubkey: ev.pubkey)
let display_name = event_group_author_name(profiles: profiles, ind: 0, group: group)
return String(format: bundle.localizedString(forKey: localization_key, value: bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: localization_key, value: nil, table: nil), table: nil), locale: locale, display_name)
return String(format: format, locale: locale, display_name)
case 2:
let alice_name = event_author_name(profiles: profiles, group.events[0])
let bob_name = event_author_name(profiles: profiles, group.events[1])
let alice_name = event_group_author_name(profiles: profiles, ind: 0, group: group)
let bob_name = event_group_author_name(profiles: profiles, ind: 1, group: group)
return String(format: bundle.localizedString(forKey: localization_key, value: bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: localization_key, value: nil, table: nil), table: nil), locale: locale, alice_name, bob_name)
return String(format: format, locale: locale, alice_name, bob_name)
default:
let alice_name = event_author_name(profiles: profiles, group.events.first!)
let alice_name = event_group_author_name(profiles: profiles, ind: 0, group: group)
let count = group.events.count - 1
return String(format: bundle.localizedString(forKey: localization_key, value: bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: localization_key, value: nil, table: nil), table: nil), locale: locale, count, alice_name)
return String(format: format, locale: locale, count, alice_name)
}
}
@@ -178,8 +209,10 @@ struct EventGroupView: View {
GroupDescription
if let event {
NavigationLink(destination: BuildThreadV2View(damus: state, event_id: event.id)) {
Text(event.content)
let thread = ThreadModel(event: event, damus_state: state)
let dest = ThreadView(state: state, thread: thread)
NavigationLink(destination: dest) {
Text(render_note_content(ev: event, profiles: state.profiles, privkey: state.keypair.privkey).content)
.padding([.top], 1)
.foregroundColor(.gray)
}
@@ -51,8 +51,8 @@ struct NotificationItemView: View {
EventGroupView(state: state, event: ev, group: .reaction(evgrp))
case .reply(let ev):
NavigationLink(destination: BuildThreadV2View(damus: state, event_id: ev.id)) {
EventView(damus: state, event: ev, has_action_bar: true)
NavigationLink(destination: ThreadView(state: state, thread: ThreadModel(event: ev, damus_state: state))) {
EventView(damus: state, event: ev)
}
.buttonStyle(.plain)
}
@@ -7,23 +7,93 @@
import SwiftUI
enum NotificationFilterState: String {
case all
case zaps
case replies
func filter(_ item: NotificationItem) -> Bool {
switch self {
case .all:
return true
case .replies:
return item.is_reply != nil
case .zaps:
return item.is_zap != nil
}
}
}
struct NotificationsView: View {
let state: DamusState
@ObservedObject var notifications: NotificationsModel
@State var filter_state: NotificationFilterState
@Environment(\.colorScheme) var colorScheme
init(state: DamusState, notifications: NotificationsModel) {
self.state = state
self._notifications = ObservedObject(initialValue: notifications)
self._filter_state = State(initialValue: load_notification_filter_state(pubkey: state.pubkey))
}
var body: some View {
TabView(selection: $filter_state) {
NotificationTab(NotificationFilterState.all)
.tag(NotificationFilterState.all)
.id(NotificationFilterState.all)
NotificationTab(NotificationFilterState.zaps)
.tag(NotificationFilterState.zaps)
.id(NotificationFilterState.zaps)
NotificationTab(NotificationFilterState.replies)
.tag(NotificationFilterState.replies)
.id(NotificationFilterState.replies)
}
.onChange(of: filter_state) { val in
save_notification_filter_state(pubkey: state.pubkey, state: val)
}
.safeAreaInset(edge: .top, spacing: 0) {
VStack(spacing: 0) {
CustomPicker(selection: $filter_state, content: {
Text("All", comment: "Label for filter for all notifications.")
.tag(NotificationFilterState.all)
Text("Zaps", comment: "Label for filter for zap notifications.")
.tag(NotificationFilterState.zaps)
Text("Mentions", comment: "Label for filter for seeing mention notifications (replies, etc).")
.tag(NotificationFilterState.replies)
})
Divider()
.frame(height: 1)
}
.background(colorScheme == .dark ? Color.black : Color.white)
}
}
func NotificationTab(_ filter: NotificationFilterState) -> some View {
ScrollViewReader { scroller in
ScrollView {
LazyVStack(alignment: .leading) {
Color.white.opacity(0)
.id("startblock")
.frame(height: 5)
ForEach(notifications.notifications, id: \.id) { item in
ForEach(notifications.notifications.filter(filter.filter), id: \.id) { item in
NotificationItemView(state: state, item: item)
}
}
.background(GeometryReader { proxy -> Color in
DispatchQueue.main.async {
handle_scroll_queue(proxy, queue: self.notifications)
}
return Color.clear
})
.padding(.horizontal)
}
.coordinateSpace(name: "scroll")
.onReceive(handle_notify(.scroll_to_top)) { notif in
let _ = notifications.flush()
self.notifications.should_queue = false
@@ -41,3 +111,27 @@ struct NotificationsView_Previews: PreviewProvider {
NotificationsView(state: test_damus_state(), notifications: NotificationsModel())
}
}
func notification_filter_state_key(pubkey: String) -> String {
return pk_setting_key(pubkey, key: "notification_filter_state")
}
func load_notification_filter_state(pubkey: String) -> NotificationFilterState {
let key = notification_filter_state_key(pubkey: pubkey)
guard let state_str = UserDefaults.standard.string(forKey: key) else {
return .all
}
guard let state = NotificationFilterState(rawValue: state_str) else {
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)
}
+133 -49
View File
@@ -16,10 +16,13 @@ let POST_PLACEHOLDER = NSLocalizedString("Type your post here...", comment: "Tex
struct PostView: View {
@State var post: NSMutableAttributedString = NSMutableAttributedString()
@FocusState var focus: Bool
@State var showPrivateKeyWarning: Bool = false
@State var attach_media: Bool = false
@State var error: String? = nil
@StateObject var image_upload: ImageUploadModel = ImageUploadModel()
let replying_to: NostrEvent?
let references: [ReferencedId]
let damus_state: DamusState
@@ -38,7 +41,7 @@ struct PostView: View {
func dismiss() {
self.presentationMode.wrappedValue.dismiss()
}
func send_post() {
var kind: NostrKind = .text
if replying_to?.known_kind == .chat {
@@ -68,70 +71,152 @@ struct PostView: View {
var is_post_empty: Bool {
return post.string.allSatisfy { $0.isWhitespace }
}
var ImageButton: some View {
Button(action: {
attach_media = true
}, label: {
Image(systemName: "photo")
})
}
var AttachmentBar: some View {
HStack(alignment: .center) {
ImageButton
.disabled(image_upload.progress != nil)
}
}
var PostButton: some View {
Button(NSLocalizedString("Post", comment: "Button to post a note.")) {
showPrivateKeyWarning = contentContainsPrivateKey(self.post.string)
var body: some View {
if !showPrivateKeyWarning {
self.send_post()
}
}
.font(.system(size: 14, weight: .bold))
.frame(width: 80, height: 30)
.foregroundColor(.white)
.background(LINEAR_GRADIENT)
.clipShape(Capsule())
}
var TextEntry: some View {
ZStack(alignment: .topLeading) {
TextViewWrapper(attributedText: $post)
.focused($focus)
.textInputAutocapitalization(.sentences)
.onChange(of: post) { _ in
if let replying_to {
damus_state.drafts.replies[replying_to] = post
} else {
damus_state.drafts.post = post
}
}
if post.string.isEmpty {
Text(POST_PLACEHOLDER)
.padding(.top, 8)
.padding(.leading, 4)
.foregroundColor(Color(uiColor: .placeholderText))
.allowsHitTesting(false)
}
}
}
var TopBar: some View {
VStack {
HStack {
HStack(spacing: 5.0) {
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of posting a note.")) {
self.cancel()
}
.foregroundColor(.primary)
if let error {
Text(error)
.foregroundColor(.red)
}
Spacer()
if !is_post_empty {
Button(NSLocalizedString("Post", comment: "Button to post a note.")) {
showPrivateKeyWarning = contentContainsPrivateKey(self.post.string)
if !showPrivateKeyWarning {
self.send_post()
}
}
.font(.system(size: 14, weight: .bold))
.frame(width: 80, height: 30)
.foregroundColor(.white)
.background(LINEAR_GRADIENT)
.clipShape(Capsule())
PostButton
}
}
.frame(height: 30)
.padding([.top, .bottom], 4)
if let progress = image_upload.progress {
ProgressView(value: progress, total: 1.0)
.progressViewStyle(.linear)
}
}
.frame(height: 30)
.padding([.top, .bottom], 4)
}
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(image: UIImage) {
let uploader = get_image_uploader(damus_state.pubkey)
Task.init {
let res = await image_upload.start(img: image, uploader: uploader)
switch res {
case .success(let url):
append_url(url)
case .failed(let error):
if let error {
self.error = error.localizedDescription
} else {
self.error = "Error uploading image :("
}
}
}
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
let searching = get_searching_string(post.string)
TopBar
HStack(alignment: .top) {
ProfilePicView(pubkey: damus_state.pubkey, size: 45.0, highlight: .none, profiles: damus_state.profiles)
VStack(alignment: .leading) {
ZStack(alignment: .topLeading) {
TextViewWrapper(attributedText: $post)
.focused($focus)
.textInputAutocapitalization(.sentences)
.onChange(of: post) { _ in
if let replying_to {
damus_state.drafts.replies[replying_to] = post
} else {
damus_state.drafts.post = post
}
}
if post.string.isEmpty {
Text(POST_PLACEHOLDER)
.padding(.top, 8)
.padding(.leading, 4)
.foregroundColor(Color(uiColor: .placeholderText))
.allowsHitTesting(false)
}
}
}
TextEntry
}
.frame(maxHeight: searching == nil ? .infinity : 50)
// This if-block observes @ for tagging
if let searching = get_searching_string(post.string) {
VStack {
Spacer()
UserSearch(damus_state: damus_state, search: searching, post: $post)
}.zIndex(1)
if let searching {
UserSearch(damus_state: damus_state, search: searching, post: $post)
.frame(maxHeight: .infinity)
} else {
Divider()
.padding([.bottom], 10)
AttachmentBar
}
}
.padding()
.sheet(isPresented: $attach_media) {
ImagePicker(sourceType: .photoLibrary) { img in
handle_upload(image: img)
}
}
.onAppear() {
@@ -157,7 +242,6 @@ struct PostView: View {
damus_state.drafts.post = NSMutableAttributedString(string : "")
}
}
.padding()
.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: {
Button(NSLocalizedString("No", comment: "Button to cancel out of posting a note after being alerted that it looks like they might be posting a private key."), role: .cancel) {
showPrivateKeyWarning = false
+64 -28
View File
@@ -25,10 +25,50 @@ struct UserSearch: View {
var users: [SearchedUser] {
guard let contacts = damus_state.contacts.event else {
return []
return search_profiles(profiles: damus_state.profiles, search: search)
}
return search_users(profiles: damus_state.profiles, tags: contacts.tags, search: search)
return search_users_for_autocomplete(profiles: damus_state.profiles, tags: contacts.tags, search: search)
}
func on_user_tapped(user: SearchedUser) {
guard let pk = bech32_pubkey(user.pubkey) else {
return
}
// Remove all characters after the last '@'
removeCharactersAfterLastAtSymbol()
// Create and append the user tag
let tagAttributedString = createUserTag(for: user, with: pk)
appendUserTag(tagAttributedString)
}
private func removeCharactersAfterLastAtSymbol() {
while post.string.last != "@" {
post.deleteCharacters(in: NSRange(location: post.length - 1, length: 1))
}
post.deleteCharacters(in: NSRange(location: post.length - 1, length: 1))
}
private func createUserTag(for user: SearchedUser, with pk: String) -> NSMutableAttributedString {
let name = Profile.displayName(profile: user.profile, pubkey: pk).username
let tagString = "@\(name)\u{200B} "
let tagAttributedString = NSMutableAttributedString(string: tagString,
attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18.0),
NSAttributedString.Key.link: "@\(pk)"])
tagAttributedString.removeAttribute(.link, range: NSRange(location: tagAttributedString.length - 2, length: 2))
tagAttributedString.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.label], range: NSRange(location: tagAttributedString.length - 2, length: 2))
return tagAttributedString
}
private func appendUserTag(_ tagAttributedString: NSMutableAttributedString) {
let mutableString = NSMutableAttributedString()
mutableString.append(post)
mutableString.append(tagAttributedString)
post = mutableString
}
var body: some View {
@@ -37,29 +77,7 @@ struct UserSearch: View {
ForEach(users) { user in
UserView(damus_state: damus_state, pubkey: user.pubkey)
.onTapGesture {
guard let pk = bech32_pubkey(user.pubkey) else {
return
}
while post.string.last != "@" {
post.deleteCharacters(in: NSRange(location: post.length - 1, length: 1))
}
post.deleteCharacters(in: NSRange(location: post.length - 1, length: 1))
var tagString = ""
if let name = user.profile?.name {
tagString = "@\(name)\u{200B} "
}
let tagAttributedString = NSMutableAttributedString(string: tagString,
attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18.0),
NSAttributedString.Key.link: "@\(pk)"])
tagAttributedString.removeAttribute(.link, range: NSRange(location: tagAttributedString.length - 2, length: 2))
tagAttributedString.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.label], range: NSRange(location: tagAttributedString.length - 2, length: 2))
let mutableString = NSMutableAttributedString()
mutableString.append(post)
mutableString.append(tagAttributedString)
post = mutableString
on_user_tapped(user: user)
}
}
}
@@ -77,11 +95,11 @@ struct UserSearch_Previews: PreviewProvider {
}
func search_users(profiles: Profiles, tags: [[String]], search _search: String) -> [SearchedUser] {
func search_users_for_autocomplete(profiles: Profiles, tags: [[String]], search _search: String) -> [SearchedUser] {
var seen_user = Set<String>()
let search = _search.lowercased()
return tags.reduce(into: Array<SearchedUser>()) { arr, tag in
var matches = tags.reduce(into: Array<SearchedUser>()) { arr, tag in
guard tag.count >= 2 && tag[0] == "p" else {
return
}
@@ -99,11 +117,29 @@ func search_users(profiles: Profiles, tags: [[String]], search _search: String)
let profile = profiles.lookup(id: pubkey)
guard ((petname?.lowercased().hasPrefix(search) ?? false) || (profile?.name?.lowercased().hasPrefix(search) ?? false)) else {
guard ((petname?.lowercased().hasPrefix(search) ?? false) ||
(profile?.name?.lowercased().hasPrefix(search) ?? false) ||
(profile?.display_name?.lowercased().hasPrefix(search) ?? false)) else {
return
}
let searched_user = SearchedUser(petname: petname, profile: profile, pubkey: pubkey)
arr.append(searched_user)
}
// search profile cache as well
for tup in profiles.profiles.enumerated() {
let pk = tup.element.key
let prof = tup.element.value.profile
guard !seen_user.contains(pk) else {
continue
}
if let match = profile_search_matches(profiles: profiles, profile: prof, pubkey: pk, search: search) {
matches.append(match)
}
}
return matches
}
@@ -0,0 +1,96 @@
//
// EventProfileName.swift
// damus
//
// Created by William Casarin on 2023-03-14.
//
import SwiftUI
/// Profile Name used when displaying an event in the timeline
struct EventProfileName: View {
let damus_state: DamusState
let pubkey: String
let profile: Profile?
let prefix: String
let show_friend_confirmed: Bool
@State var display_name: DisplayName?
@State var nip05: NIP05?
let size: EventViewKind
init(pubkey: String, profile: Profile?, damus: DamusState, show_friend_confirmed: Bool, size: EventViewKind = .normal) {
self.damus_state = damus
self.pubkey = pubkey
self.profile = profile
self.prefix = ""
self.show_friend_confirmed = show_friend_confirmed
self.size = size
}
init(pubkey: String, profile: Profile?, prefix: String, damus: DamusState, show_friend_confirmed: Bool, size: EventViewKind = .normal) {
self.damus_state = damus
self.pubkey = pubkey
self.profile = profile
self.prefix = prefix
self.show_friend_confirmed = show_friend_confirmed
self.size = size
}
var friend_icon: String? {
return get_friend_icon(contacts: damus_state.contacts, pubkey: pubkey, show_confirmed: show_friend_confirmed)
}
var current_nip05: NIP05? {
nip05 ?? damus_state.profiles.is_validated(pubkey)
}
var current_display_name: DisplayName {
return display_name ?? Profile.displayName(profile: profile, pubkey: pubkey)
}
var body: some View {
HStack(spacing: 2) {
switch current_display_name {
case .one(let one):
Text(one)
.font(.body.weight(.bold))
case .both(let both):
Text(both.display_name)
.font(.body.weight(.bold))
Text(verbatim: "@\(both.username)")
.foregroundColor(.gray)
.font(eventviewsize_to_font(size))
}
if let nip05 = current_nip05 {
NIP05Badge(nip05: nip05, pubkey: pubkey, contacts: damus_state.contacts, show_domain: false, clickable: false)
}
if let frend = friend_icon, current_nip05 == nil {
Label("", systemImage: frend)
.foregroundColor(.gray)
.font(.footnote)
}
}
.onReceive(handle_notify(.profile_updated)) { notif in
let update = notif.object as! ProfileUpdate
if update.pubkey != pubkey {
return
}
display_name = Profile.displayName(profile: update.profile, pubkey: pubkey)
nip05 = damus_state.profiles.is_validated(pubkey)
}
}
}
struct EventProfileName_Previews: PreviewProvider {
static var previews: some View {
EventProfileName(pubkey: "pk", profile: nil, damus: test_damus_state(), show_friend_confirmed: true)
}
}
+105
View File
@@ -0,0 +1,105 @@
//
// ProfileName.swift
// damus
//
// Created by William Casarin on 2022-04-16.
//
import SwiftUI
func get_friend_icon(contacts: Contacts, pubkey: String, show_confirmed: Bool) -> String? {
if !show_confirmed {
return nil
}
if contacts.is_friend_or_self(pubkey) {
return "person.fill.checkmark"
}
if contacts.is_friend_of_friend(pubkey) {
return "person.fill.and.arrow.left.and.arrow.right"
}
return nil
}
struct ProfileName: View {
let damus_state: DamusState
let pubkey: String
let profile: Profile?
let prefix: String
let show_friend_confirmed: Bool
let show_nip5_domain: Bool
@State var display_name: DisplayName?
@State var nip05: NIP05?
init(pubkey: String, profile: Profile?, damus: DamusState, show_friend_confirmed: Bool, show_nip5_domain: Bool = true) {
self.pubkey = pubkey
self.profile = profile
self.prefix = ""
self.show_friend_confirmed = show_friend_confirmed
self.show_nip5_domain = show_nip5_domain
self.damus_state = damus
}
init(pubkey: String, profile: Profile?, prefix: String, damus: DamusState, show_friend_confirmed: Bool, show_nip5_domain: Bool = true) {
self.pubkey = pubkey
self.profile = profile
self.prefix = prefix
self.damus_state = damus
self.show_friend_confirmed = show_friend_confirmed
self.show_nip5_domain = show_nip5_domain
}
var friend_icon: String? {
return get_friend_icon(contacts: damus_state.contacts, pubkey: pubkey, show_confirmed: show_friend_confirmed)
}
var current_nip05: NIP05? {
nip05 ?? damus_state.profiles.is_validated(pubkey)
}
var nip05_color: Color {
return get_nip05_color(pubkey: pubkey, contacts: damus_state.contacts)
}
var current_display_name: DisplayName {
return display_name ?? Profile.displayName(profile: profile, pubkey: pubkey)
}
var name_choice: String {
return prefix == "@" ? current_display_name.username : current_display_name.display_name
}
var body: some View {
HStack(spacing: 2) {
Text(verbatim: "\(prefix)\(name_choice)")
.font(.body)
.fontWeight(prefix == "@" ? .none : .bold)
if let nip05 = current_nip05 {
NIP05Badge(nip05: nip05, pubkey: pubkey, contacts: damus_state.contacts, show_domain: show_nip5_domain, clickable: true)
}
if let friend = friend_icon, current_nip05 == nil {
Image(systemName: friend)
.foregroundColor(.gray)
}
}
.onReceive(handle_notify(.profile_updated)) { notif in
let update = notif.object as! ProfileUpdate
if update.pubkey != pubkey {
return
}
display_name = Profile.displayName(profile: update.profile, pubkey: pubkey)
nip05 = damus_state.profiles.is_validated(pubkey)
}
}
}
struct ProfileName_Previews: PreviewProvider {
static var previews: some View {
ProfileName(pubkey:
test_damus_state().pubkey, profile: make_test_profile(), damus: test_damus_state(), show_friend_confirmed: true)
}
}
+18 -18
View File
@@ -17,10 +17,20 @@ struct ProfileNameView: View {
var body: some View {
Group {
if let real_name = profile?.display_name {
VStack(alignment: .leading, spacing: 0) {
Text(real_name)
VStack(alignment: .leading) {
switch Profile.displayName(profile: profile, pubkey: pubkey) {
case .one:
HStack(alignment: .center, spacing: spacing) {
ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true)
.font(.title3.weight(.bold))
if follows_you {
FollowsYou()
}
}
case .both(let both):
Text(both.display_name)
.font(.title3.weight(.bold))
HStack(alignment: .center, spacing: spacing) {
ProfileName(pubkey: pubkey, profile: profile, prefix: "@", damus: damus, show_friend_confirmed: true)
.font(.callout)
@@ -30,22 +40,12 @@ struct ProfileNameView: View {
FollowsYou()
}
}
Spacer()
KeyView(pubkey: pubkey)
.pubkey_context_menu(bech32_pubkey: pubkey)
}
} else {
VStack(alignment: .leading) {
HStack(alignment: .center, spacing: spacing) {
ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true)
.font(.title3.weight(.bold))
if follows_you {
FollowsYou()
}
}
KeyView(pubkey: pubkey)
.pubkey_context_menu(bech32_pubkey: pubkey)
}
Spacer()
KeyView(pubkey: pubkey)
.pubkey_context_menu(bech32_pubkey: pubkey)
}
}
}
@@ -66,6 +66,7 @@ struct InnerProfilePicView: View {
.placeholder { _ in
Placeholder
}
.scaledToFill()
}
.frame(width: size, height: size)
.clipShape(Circle())
@@ -50,13 +50,18 @@ func follow_btn_enabled_state(_ fs: FollowState) -> Bool {
}
func followersCountString(_ count: Int, locale: Locale = Locale.current) -> String {
let bundle = bundleForLocale(locale: locale)
return String(format: bundle.localizedString(forKey: "followers_count", value: nil, table: nil), locale: locale, count)
let format = localizedStringFormat(key: "followers_count", locale: locale)
return String(format: format, locale: locale, count)
}
func followingCountString(_ count: Int, locale: Locale = Locale.current) -> String {
let format = localizedStringFormat(key: "following_count", locale: locale)
return String(format: format, locale: locale, count)
}
func relaysCountString(_ count: Int, locale: Locale = Locale.current) -> String {
let bundle = bundleForLocale(locale: locale)
return String(format: bundle.localizedString(forKey: "relays_count", value: nil, table: nil), locale: locale, count)
let format = localizedStringFormat(key: "relays_count", locale: locale)
return String(format: format, locale: locale, count)
}
struct EditButton: View {
@@ -179,6 +184,7 @@ struct ProfileView: View {
}
.frame(height: bannerHeight)
.allowsHitTesting(false)
}
var navbarHeight: CGFloat {
@@ -328,7 +334,7 @@ struct ProfileView: View {
actionSection(profile_data: profile_data)
}
let follows_you = profile.follows(pubkey: damus_state.pubkey)
let follows_you = profile.pubkey != damus_state.pubkey && profile.follows(pubkey: damus_state.pubkey)
ProfileNameView(pubkey: profile.pubkey, profile: profile_data, follows_you: follows_you, damus: damus_state)
}
}
@@ -367,7 +373,7 @@ struct ProfileView: View {
let following_model = FollowingModel(damus_state: damus_state, contacts: contacts)
NavigationLink(destination: FollowingView(damus_state: damus_state, following: following_model, whos: profile.pubkey)) {
HStack {
let noun_text = Text("Following", comment: "Text on the user profile page next to the number of accounts a user is following.").font(.subheadline).foregroundColor(.gray)
let noun_text = Text(verbatim: "\(followingCountString(profile.following))").font(.subheadline).foregroundColor(.gray)
Text("\(Text("\(profile.following)").font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.")
}
}
@@ -515,26 +521,25 @@ struct KeyView: View {
let bech32 = bech32_pubkey(pubkey) ?? pubkey
HStack {
RoundedRectangle(cornerRadius: 11)
.frame(height: 22)
.foregroundColor(fillColor())
.overlay(
HStack {
Button {
copyPubkey(bech32)
} label: {
Label(NSLocalizedString("Public Key", comment: "Label indicating that the text is a user's public account key."), systemImage: "key.fill")
.font(.custom("key", size: 12.0))
.labelStyle(IconOnlyLabelStyle())
.foregroundStyle(hex_to_rgb(pubkey))
.symbolRenderingMode(.palette)
}
.padding(.leading,4)
Text(verbatim: "\(abbrev_pubkey(bech32, amount: 16))")
.font(.footnote)
.foregroundColor(keyColor())
}
)
HStack {
Button {
copyPubkey(bech32)
} label: {
Label(NSLocalizedString("Public Key", comment: "Label indicating that the text is a user's public account key."), systemImage: "key.fill")
.font(.custom("key", size: 12.0))
.labelStyle(IconOnlyLabelStyle())
.foregroundStyle(hex_to_rgb(pubkey))
.symbolRenderingMode(.palette)
}
.padding(.trailing, 2)
Text(verbatim: "\(abbrev_pubkey(bech32, amount: 16))")
.font(.footnote)
.foregroundColor(keyColor())
}
.padding(2)
.padding([.leading, .trailing], 3)
.background(RoundedRectangle(cornerRadius: 11).foregroundColor(fillColor()))
if isCopied != true {
Button {
copyPubkey(bech32)
-168
View File
@@ -1,168 +0,0 @@
//
// ProfileName.swift
// damus
//
// Created by William Casarin on 2022-04-16.
//
import SwiftUI
func get_friend_icon(contacts: Contacts, pubkey: String, show_confirmed: Bool) -> String? {
if !show_confirmed {
return nil
}
if contacts.is_friend_or_self(pubkey) {
return "person.fill.checkmark"
}
if contacts.is_friend_of_friend(pubkey) {
return "person.fill.and.arrow.left.and.arrow.right"
}
return nil
}
struct ProfileName: View {
let damus_state: DamusState
let pubkey: String
let profile: Profile?
let prefix: String
let show_friend_confirmed: Bool
let show_nip5_domain: Bool
@State var display_name: String?
@State var nip05: NIP05?
init(pubkey: String, profile: Profile?, damus: DamusState, show_friend_confirmed: Bool, show_nip5_domain: Bool = true) {
self.pubkey = pubkey
self.profile = profile
self.prefix = ""
self.show_friend_confirmed = show_friend_confirmed
self.show_nip5_domain = show_nip5_domain
self.damus_state = damus
}
init(pubkey: String, profile: Profile?, prefix: String, damus: DamusState, show_friend_confirmed: Bool, show_nip5_domain: Bool = true) {
self.pubkey = pubkey
self.profile = profile
self.prefix = prefix
self.damus_state = damus
self.show_friend_confirmed = show_friend_confirmed
self.show_nip5_domain = show_nip5_domain
}
var friend_icon: String? {
return get_friend_icon(contacts: damus_state.contacts, pubkey: pubkey, show_confirmed: show_friend_confirmed)
}
var current_nip05: NIP05? {
nip05 ?? damus_state.profiles.is_validated(pubkey)
}
var nip05_color: Color {
return get_nip05_color(pubkey: pubkey, contacts: damus_state.contacts)
}
var body: some View {
HStack(spacing: 2) {
Text(verbatim: "\(prefix)\(String(display_name ?? Profile.displayName(profile: profile, pubkey: pubkey)))")
.font(.body)
.fontWeight(prefix == "@" ? .none : .bold)
if let nip05 = current_nip05 {
NIP05Badge(nip05: nip05, pubkey: pubkey, contacts: damus_state.contacts, show_domain: show_nip5_domain, clickable: true)
}
if let friend = friend_icon, current_nip05 == nil {
Image(systemName: friend)
.foregroundColor(.gray)
}
}
.onReceive(handle_notify(.profile_updated)) { notif in
let update = notif.object as! ProfileUpdate
if update.pubkey != pubkey {
return
}
display_name = Profile.displayName(profile: update.profile, pubkey: pubkey)
nip05 = damus_state.profiles.is_validated(pubkey)
}
}
}
/// Profile Name used when displaying an event in the timeline
struct EventProfileName: View {
let damus_state: DamusState
let pubkey: String
let profile: Profile?
let prefix: String
let show_friend_confirmed: Bool
@State var display_name: String?
@State var nip05: NIP05?
let size: EventViewKind
init(pubkey: String, profile: Profile?, damus: DamusState, show_friend_confirmed: Bool, size: EventViewKind = .normal) {
self.damus_state = damus
self.pubkey = pubkey
self.profile = profile
self.prefix = ""
self.show_friend_confirmed = show_friend_confirmed
self.size = size
}
init(pubkey: String, profile: Profile?, prefix: String, damus: DamusState, show_friend_confirmed: Bool, size: EventViewKind = .normal) {
self.damus_state = damus
self.pubkey = pubkey
self.profile = profile
self.prefix = prefix
self.show_friend_confirmed = show_friend_confirmed
self.size = size
}
var friend_icon: String? {
return get_friend_icon(contacts: damus_state.contacts, pubkey: pubkey, show_confirmed: show_friend_confirmed)
}
var current_nip05: NIP05? {
nip05 ?? damus_state.profiles.is_validated(pubkey)
}
var body: some View {
HStack(spacing: 2) {
if let real_name = profile?.display_name {
Text(real_name)
.font(.body.weight(.bold))
.padding([.trailing], 2)
Text(verbatim: "@\(display_name ?? Profile.displayName(profile: profile, pubkey: pubkey))")
.foregroundColor(Color("DamusMediumGrey"))
.font(eventviewsize_to_font(size))
} else {
Text(verbatim: "\(display_name ?? Profile.displayName(profile: profile, pubkey: pubkey))")
.font(eventviewsize_to_font(size))
.fontWeight(.bold)
}
if let nip05 = current_nip05 {
NIP05Badge(nip05: nip05, pubkey: pubkey, contacts: damus_state.contacts, show_domain: false, clickable: false)
}
if let frend = friend_icon, current_nip05 == nil {
Label("", systemImage: frend)
.foregroundColor(.gray)
.font(.footnote)
}
}
.onReceive(handle_notify(.profile_updated)) { notif in
let update = notif.object as! ProfileUpdate
if update.pubkey != pubkey {
return
}
display_name = Profile.displayName(profile: update.profile, pubkey: pubkey)
nip05 = damus_state.profiles.is_validated(pubkey)
}
}
}
+2 -2
View File
@@ -7,7 +7,7 @@
import SwiftUI
import Kingfisher
private struct ImageContainerView: View {
struct ProfileImageContainerView: View {
let url: URL?
@@ -68,7 +68,7 @@ struct ProfileZoomView: View {
.ignoresSafeArea()
ZoomableScrollView {
ImageContainerView(url: get_profile_url(picture: nil, pubkey: pubkey, profiles: profiles))
ProfileImageContainerView(url: get_profile_url(picture: nil, pubkey: pubkey, profiles: profiles))
.aspectRatio(contentMode: .fit)
.padding(.top, Theme.safeAreaInsets?.top)
.padding(.bottom, Theme.safeAreaInsets?.bottom)
+38 -14
View File
@@ -25,24 +25,48 @@ struct RecommendedRelayView: View {
}
var body: some View {
HStack {
Text(relay)
Spacer()
if add_button {
if let privkey = damus.keypair.privkey {
Button(NSLocalizedString("Add", comment: "Button to add recommended relay server.")) {
guard let ev_before_add = damus.contacts.event else {
return
}
guard let ev_after_add = add_relay(ev: ev_before_add, privkey: privkey, current_relays: damus.pool.descriptors, relay: relay, info: .rw) else {
return
}
process_contact_event(state: damus, ev: ev_after_add)
damus.pool.send(.event(ev_after_add))
ZStack {
HStack {
RelayType(is_paid: damus.relay_metadata.lookup(relay_id: relay)?.is_paid ?? false)
Text(relay).layoutPriority(1)
if let meta = damus.relay_metadata.lookup(relay_id: relay) {
NavigationLink ( destination:
RelayDetailView(state: damus, relay: relay, nip11: meta)
){
EmptyView()
}
.opacity(0.0)
Spacer()
Image(systemName: "info.circle")
.foregroundColor(Color.accentColor)
}
}
}
.swipeActions {
if add_button {
if let privkey = damus.keypair.privkey {
AddAction(privkey: privkey)
}
}
}
}
func AddAction(privkey: String) -> some View {
Button {
guard let ev_before_add = damus.contacts.event else {
return
}
guard let ev_after_add = add_relay(ev: ev_before_add, privkey: privkey, current_relays: damus.pool.descriptors, relay: relay, info: .rw) else {
return
}
process_contact_event(state: damus, ev: ev_after_add)
damus.pool.send(.event(ev_after_add))
} label: {
Label(NSLocalizedString("Add Relay", comment: "Button to add recommended relay server."), systemImage: "plus.circle")
}
.tint(.accentColor)
}
}
+15 -5
View File
@@ -26,12 +26,13 @@ struct ReplyView: View {
var body: some View {
VStack {
Text("Replying to:", comment: "Indicating that the user is replying to the following listed people.")
HStack(alignment: .top) {
let names = references.pRefs
.map { pubkey in
let pk = pubkey.ref_id
let prof = damus.profiles.lookup(id: pk)
return Profile.displayName(profile: prof, pubkey: pk)
return Profile.displayName(profile: prof, pubkey: pk).username
}
.joined(separator: ", ")
Text(names)
@@ -44,16 +45,25 @@ struct ReplyView: View {
.sheet(isPresented: $participantsShown) {
ParticipantsView(damus_state: damus, references: $references, originalReferences: $originalReferences)
}
ScrollView {
EventView(damus: damus, event: replying_to, has_action_bar: false)
ScrollViewReader { scroller in
ScrollView {
EventView(damus: damus, event: replying_to, options: [.no_action_bar])
PostView(replying_to: replying_to, references: references, damus_state: damus)
.frame(minHeight: 500, maxHeight: .infinity)
.id("post")
}
.frame(maxHeight: .infinity)
.onAppear {
scroll_to_event(scroller: scroller, id: "post", delay: 1.0, animate: true, anchor: .top)
}
}
PostView(replying_to: replying_to, references: references, damus_state: damus)
}
.onAppear {
references = gather_reply_ids(our_pubkey: damus.pubkey, from: replying_to)
originalReferences = references
}
.padding()
}
+37
View File
@@ -0,0 +1,37 @@
//
// RepostedEvent.swift
// damus
//
// Created by William Casarin on 2023-03-23.
//
import SwiftUI
struct RepostedEvent: View {
let damus: DamusState
let event: NostrEvent
let inner_ev: NostrEvent
let options: EventViewOptions
var body: some View {
VStack(alignment: .leading) {
let prof = damus.profiles.lookup(id: event.pubkey)
let booster_profile = ProfileView(damus_state: damus, pubkey: event.pubkey)
NavigationLink(destination: booster_profile) {
Reposted(damus: damus, pubkey: event.pubkey, profile: prof)
.padding(.horizontal)
}
.buttonStyle(PlainButtonStyle())
//SelectedEventView(damus: damus, event: inner_ev, size: .normal)
TextEvent(damus: damus, event: inner_ev, pubkey: inner_ev.pubkey, options: options)
}
}
}
struct RepostedEvent_Previews: PreviewProvider {
static var previews: some View {
RepostedEvent(damus: test_damus_state(), event: test_event, inner_ev: test_event, options: [])
}
}
+113
View File
@@ -0,0 +1,113 @@
//
// SearchingEventView.swift
// damus
//
// Created by William Casarin on 2023-03-05.
//
import SwiftUI
enum SearchState {
case searching
case found(NostrEvent)
case found_profile(String)
case not_found
}
enum SearchType {
case event
case profile
}
struct SearchingEventView: View {
let state: DamusState
let evid: String
let search_type: SearchType
@State var search_state: SearchState = .searching
var bech32_evid: String {
guard let bytes = hex_decode(evid) else {
return evid
}
let noteid = bech32_encode(hrp: "note", bytes)
return abbrev_pubkey(noteid)
}
var search_name: String {
switch search_type {
case .profile:
return "profile"
case .event:
return "note"
}
}
var body: some View {
Group {
switch search_state {
case .searching:
HStack(spacing: 10) {
Text("Looking for \(search_name)...", comment: "Label that appears when searching for note or profile")
ProgressView()
.progressViewStyle(.circular)
}
case .found(let ev):
NavigationLink(destination: ThreadView(state: state, thread: ThreadModel(event: ev, damus_state: state))) {
EventView(damus: state, event: ev)
}
.buttonStyle(PlainButtonStyle())
case .found_profile(let pk):
NavigationLink(destination: ProfileView(damus_state: state, pubkey: pk)) {
FollowUserView(target: .pubkey(pk), damus_state: state)
}
.buttonStyle(PlainButtonStyle())
case .not_found:
Text("\(search_name.capitalized) not found", comment: "When a note or profile is not found when searching for it via its note id")
}
}
.onAppear {
switch search_type {
case .event:
if let ev = state.events.lookup(evid) {
self.search_state = .found(ev)
return
}
find_event(state: state, evid: evid, search_type: search_type, find_from: nil) { ev in
if let ev {
self.search_state = .found(ev)
} else {
self.search_state = .not_found
}
}
case .profile:
find_event(state: state, evid: evid, search_type: search_type, find_from: nil) { _ in
if state.profiles.lookup(id: evid) != nil {
self.search_state = .found_profile(evid)
return
} else {
self.search_state = .not_found
}
}
}
}
}
}
struct SearchingEventView_Previews: PreviewProvider {
static var previews: some View {
let state = test_damus_state()
SearchingEventView(state: state, evid: test_event.id, search_type: .event)
}
}
enum EventSearchState {
case searching
case not_found
case found(NostrEvent)
case found_profile(String)
}
@@ -0,0 +1,20 @@
//
// SearchingProfileView.swift
// damus
//
// Created by William Casarin on 2023-03-05.
//
import SwiftUI
struct SearchingProfileView: View {
var body: some View {
Text(verbatim: /*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
}
}
struct SearchingProfileView_Previews: PreviewProvider {
static var previews: some View {
SearchingProfileView()
}
}
+13 -15
View File
@@ -12,30 +12,31 @@ struct SearchHomeView: View {
let damus_state: DamusState
@StateObject var model: SearchHomeModel
@State var search: String = ""
@FocusState private var isFocused: Bool
var SearchInput: some View {
ZStack(alignment: .leading) {
HStack {
HStack{
Image(systemName: "magnifyingglass")
.foregroundColor(.gray)
TextField(NSLocalizedString("Search...", comment: "Placeholder text to prompt entry of search query."), text: $search)
.padding(8)
.padding(.leading, 35)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
.focused($isFocused)
}
.padding(10)
.background(.secondary.opacity(0.2))
.cornerRadius(20)
if(!search.isEmpty) {
Text("Cancel", comment: "Cancel out of search view.")
.foregroundColor(.blue)
.foregroundColor(.accentColor)
.padding(EdgeInsets(top: 0.0, leading: 0.0, bottom: 0.0, trailing: 10.0))
.opacity((search == "") ? 0.0 : 1.0)
.onTapGesture {
self.search = ""
isFocused = false
}
}
Label("", systemImage: "magnifyingglass")
.padding(.leading, 10)
}
.background {
RoundedRectangle(cornerRadius: 8)
.foregroundColor(.secondary.opacity(0.2))
}
}
@@ -83,9 +84,6 @@ struct SearchHomeView: View {
}
.background(colorScheme == .dark ? Color.black : Color.white)
}
.onChange(of: search) { s in
print("search change 1")
}
.onReceive(handle_notify(.new_mutes)) { _ in
self.model.filter_muted()
}
+81 -86
View File
@@ -8,7 +8,7 @@
import SwiftUI
enum Search {
case profiles([(String, Profile)])
case profiles([SearchedUser])
case hashtag(String)
case profile(String)
case note(String)
@@ -18,9 +18,10 @@ enum Search {
struct SearchResultsView: View {
let damus_state: DamusState
@Binding var search: String
@State var result: Search? = nil
func ProfileSearchResult(pk: String, res: Profile) -> some View {
func ProfileSearchResult(pk: String) -> some View {
FollowUserView(target: .pubkey(pk), damus_state: damus_state)
}
@@ -30,8 +31,8 @@ struct SearchResultsView: View {
switch result {
case .profiles(let results):
LazyVStack {
ForEach(results, id: \.0) { prof in
ProfileSearchResult(pk: prof.0, res: prof.1)
ForEach(results) { prof in
ProfileSearchResult(pk: prof.pubkey)
}
}
case .hashtag(let ht):
@@ -43,39 +44,23 @@ struct SearchResultsView: View {
case .profile(let prof):
let decoded = try? bech32_decode(prof)
let hex = hex_encode(decoded!.data)
let prof_model = ProfileModel(pubkey: hex, damus: damus_state)
let f = FollowersModel(damus_state: damus_state, target: prof)
let dst = ProfileView(damus_state: damus_state, profile: prof_model, followers: f)
NavigationLink(destination: dst) {
Text("Goto profile \(prof)", comment: "Navigation link to go to profile.")
}
case .hex(let h):
let prof_model = ProfileModel(pubkey: h, damus: damus_state)
let f = FollowersModel(damus_state: damus_state, target: h)
let prof_view = ProfileView(damus_state: damus_state, profile: prof_model, followers: f)
let ev_view = BuildThreadV2View(
damus: damus_state,
event_id: h
)
VStack(spacing: 50) {
NavigationLink(destination: prof_view) {
Text("Goto profile \(h)", comment: "Navigation link to go to profile referenced by hex code.")
}
NavigationLink(destination: ev_view) {
Text("Goto post \(h)", comment: "Navigation link to go to post referenced by hex code.")
}
SearchingEventView(state: damus_state, evid: hex, search_type: .profile)
case .hex(let h):
//let prof_view = ProfileView(damus_state: damus_state, pubkey: h)
//let ev_view = ThreadView(damus: damus_state, event_id: h)
VStack(spacing: 10) {
SearchingEventView(state: damus_state, evid: h, search_type: .event)
SearchingEventView(state: damus_state, evid: h, search_type: .profile)
}
case .note(let nid):
let decoded = try? bech32_decode(nid)
let hex = hex_encode(decoded!.data)
let ev_view = BuildThreadV2View(
damus: damus_state,
event_id: hex
)
NavigationLink(destination: ev_view) {
Text("Goto post \(nid)", comment: "Navigation link to go to post referenced by note ID.")
}
SearchingEventView(state: damus_state, evid: hex, search_type: .event)
case .none:
Text("none", comment: "No search results.")
}
@@ -84,66 +69,14 @@ struct SearchResultsView: View {
}
}
func search_changed(_ new: String) {
guard new.count != 0 else {
return
}
if new.first! == "#" {
let ht = String(new.dropFirst())
self.result = .hashtag(ht)
return
}
if hex_decode(new) != nil, new.count == 64 {
self.result = .hex(new)
return
}
if new.starts(with: "npub") {
if (try? bech32_decode(new)) != nil {
self.result = .profile(new)
return
}
}
if new.starts(with: "note") {
if (try? bech32_decode(new)) != nil {
self.result = .note(new)
return
}
}
let profs = damus_state.profiles.profiles.enumerated()
let results: [(String, Profile)] = profs.reduce(into: []) { acc, els in
let pk = els.element.key
let prof = els.element.value.profile
let lowname = prof.name.map { $0.lowercased() }
let lownip05 = damus_state.profiles.is_validated(pk).map { $0.host.lowercased() }
let lowdisp = prof.display_name.map { $0.lowercased() }
let ok = new.count == 1 ?
((lowname?.starts(with: new) ?? false) ||
(lownip05?.starts(with: new) ?? false) ||
(lowdisp?.starts(with: new) ?? false)) : (pk.starts(with: new) || String(new.dropFirst()) == pk
|| lowname?.contains(new) ?? false
|| lownip05?.contains(new) ?? false
|| lowdisp?.contains(new) ?? false)
if ok {
acc.append((pk, prof))
}
}
self.result = .profiles(results)
}
var body: some View {
MainContent
.frame(maxHeight: .infinity)
.onAppear {
search_changed(search)
self.result = search_for_string(profiles: damus_state.profiles, search)
}
.onChange(of: search) { new in
search_changed(new)
self.result = search_for_string(profiles: damus_state.profiles, new)
}
}
}
@@ -155,3 +88,65 @@ struct SearchResultsView_Previews: PreviewProvider {
}
}
*/
func search_for_string(profiles: Profiles, _ new: String) -> Search? {
guard new.count != 0 else {
return nil
}
if new.first! == "#" {
let ht = String(new.dropFirst().filter{$0 != " "})
return .hashtag(ht)
}
if hex_decode(new) != nil, new.count == 64 {
return .hex(new)
}
if new.starts(with: "npub") {
if (try? bech32_decode(new)) != nil {
return .profile(new)
}
}
if new.starts(with: "note") {
if (try? bech32_decode(new)) != nil {
return .note(new)
}
}
return .profiles(search_profiles(profiles: profiles, search: new))
}
func search_profiles(profiles: Profiles, search: String) -> [SearchedUser] {
let new = search.lowercased()
return profiles.profiles.enumerated().reduce(into: []) { acc, els in
let pk = els.element.key
let prof = els.element.value.profile
if let searched = profile_search_matches(profiles: profiles, profile: prof, pubkey: pk, search: new) {
acc.append(searched)
}
}
}
func profile_search_matches(profiles: Profiles, profile prof: Profile, pubkey pk: String, search new: String) -> SearchedUser? {
let lowname = prof.name.map { $0.lowercased() }
let lownip05 = profiles.is_validated(pk).map { $0.host.lowercased() }
let lowdisp = prof.display_name.map { $0.lowercased() }
let ok = new.count == 1 ?
((lowname?.starts(with: new) ?? false) ||
(lownip05?.starts(with: new) ?? false) ||
(lowdisp?.starts(with: new) ?? false)) : (pk.starts(with: new) || String(new.dropFirst()) == pk
|| lowname?.contains(new) ?? false
|| lownip05?.contains(new) ?? false
|| lowdisp?.contains(new) ?? false)
if ok {
return SearchedUser(petname: nil, profile: prof, pubkey: pk)
}
return nil
}
+9 -4
View File
@@ -13,16 +13,21 @@ struct TextViewWrapper: UIViewRepresentable {
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.delegate = context.coordinator
textView.font = UIFont.systemFont(ofSize: 18)
textView.textColor = UIColor.label
TextViewWrapper.setTextProperties(textView)
return textView
}
static func setTextProperties(_ uiView: UITextView) {
uiView.textColor = UIColor.label
uiView.font = UIFont.preferredFont(forTextStyle: .body)
let linkAttributes: [NSAttributedString.Key : Any] = [
NSAttributedString.Key.foregroundColor: UIColor(Color.accentColor)]
textView.linkTextAttributes = linkAttributes
return textView
uiView.linkTextAttributes = linkAttributes
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.attributedText = attributedText
TextViewWrapper.setTextProperties(uiView)
}
func makeCoordinator() -> Coordinator {
-336
View File
@@ -1,336 +0,0 @@
//
// ThreadV2View.swift
// damus
//
// Created by Thomas Tastet on 25/12/2022.
//
import SwiftUI
struct ThreadV2 {
var parentEvents: [NostrEvent]
var current: NostrEvent
var childEvents: [NostrEvent]
mutating func clean() {
// remove duplicates
self.parentEvents = Array(Set(self.parentEvents))
self.childEvents = Array(Set(self.childEvents))
// remove empty contents
self.parentEvents = self.parentEvents.filter { event in
return !event.content.isEmpty
}
self.childEvents = self.childEvents.filter { event in
return !event.content.isEmpty
}
// sort events by publication date
self.parentEvents = self.parentEvents.sorted { event1, event2 in
return event1 < event2
}
self.childEvents = self.childEvents.sorted { event1, event2 in
return event1 < event2
}
}
}
struct BuildThreadV2View: View {
let damus: DamusState
@State var parents_ids: [String] = []
let event_id: String
@State var current_event: NostrEvent? = nil
@State var thread: ThreadV2? = nil
@State var current_events_uuid: String = ""
@State var extra_events_uuid: String = ""
@State var childs_events_uuid: String = ""
@State var parents_events_uuids: [String] = []
@State var subscriptions_uuids: [String] = []
@Environment(\.dismiss) var dismiss
init(damus: DamusState, event_id: String) {
self.damus = damus
self.event_id = event_id
}
func unsubscribe_all() {
print("ThreadV2View: Unsubscribe all..")
for subscriptions in subscriptions_uuids {
unsubscribe(subscriptions)
}
}
func unsubscribe(_ sub_id: String) {
if subscriptions_uuids.contains(sub_id) {
damus.pool.unsubscribe(sub_id: sub_id)
subscriptions_uuids.remove(at: subscriptions_uuids.firstIndex(of: sub_id)!)
}
}
func subscribe(filters: [NostrFilter], sub_id: String = UUID().description) -> String {
damus.pool.register_handler(sub_id: sub_id, handler: handle_event)
damus.pool.send(.subscribe(.init(filters: filters, sub_id: sub_id)))
subscriptions_uuids.append(sub_id)
return sub_id
}
func handle_current_events(ev: NostrEvent) {
if current_event != nil {
return
}
current_event = ev
thread = ThreadV2(
parentEvents: [],
current: current_event!,
childEvents: []
)
// Get parents
parents_ids = current_event!.tags.enumerated().filter { (index, tag) in
return tag.count >= 2 && tag[0] == "e" && !current_event!.content.contains("#[\(index)]")
}.map { tag in
return tag.1[1]
}
print("ThreadV2View: Parents list: (\(parents_ids)")
if parents_ids.count > 0 {
// Ask for parents
let parents_events = NostrFilter(
ids: parents_ids,
limit: UInt32(parents_ids.count)
)
let uuid = subscribe(filters: [parents_events])
parents_events_uuids.append(uuid)
print("ThreadV2View: Ask for parents (\(uuid)) (\(parents_events))")
}
// Ask for children
let childs_events = NostrFilter(
kinds: [1],
referenced_ids: [self.event_id],
limit: 50
)
childs_events_uuid = subscribe(filters: [childs_events])
print("ThreadV2View: Ask for children (\(childs_events) (\(childs_events_uuid))")
}
func handle_parent_events(sub_id: String, nostr_event: NostrEvent) {
// We are filtering this later
thread!.parentEvents.append(nostr_event)
// Get parents of parents
let local_parents_ids = nostr_event.tags.enumerated().filter { (index, tag) in
return tag.count >= 2 && tag[0] == "e" && !nostr_event.content.contains("#[\(index)]")
}.map { tag in
return tag.1[1]
}.filter { tag_id in
return !parents_ids.contains(tag_id)
}
print("ThreadV2View: Sub Parents list: (\(local_parents_ids))")
// Expand new parents id
parents_ids.append(contentsOf: local_parents_ids)
if local_parents_ids.count > 0 {
// Ask for parents
let parents_events = NostrFilter(
ids: local_parents_ids,
limit: UInt32(local_parents_ids.count)
)
let uuid = subscribe(filters: [parents_events])
parents_events_uuids.append(uuid)
print("ThreadV2View: Ask for sub_parents (\(local_parents_ids)) \(uuid)")
}
thread!.clean()
unsubscribe(sub_id)
return
}
func handle_event(relay_id: String, ev: NostrConnectionEvent) {
guard case .nostr_event(let nostr_response) = ev else {
return
}
guard case .event(let id, let nostr_event) = nostr_response else {
return
}
// Is current event
if id == current_events_uuid {
handle_current_events(ev: nostr_event)
return
}
if parents_events_uuids.contains(id) {
handle_parent_events(sub_id: id, nostr_event: nostr_event)
return
}
if id == childs_events_uuid {
// We are filtering this later
thread!.childEvents.append(nostr_event)
thread!.clean()
return
}
}
func reload() {
self.unsubscribe_all()
print("ThreadV2View: Reload!")
var extra = NostrFilter.filter_kinds([9735, 6, 7])
extra.referenced_ids = [ self.event_id ]
// Get the current event
current_events_uuid = subscribe(filters: [
NostrFilter(ids: [self.event_id], limit: 1)
])
extra_events_uuid = subscribe(filters: [extra])
print("subscribing to threadV2 \(event_id) with sub_id \(current_events_uuid)")
}
var body: some View {
VStack {
if thread == nil {
ProgressView()
} else {
ThreadV2View(damus: damus, thread: thread!)
}
}
.onAppear {
if self.thread == nil {
self.reload()
}
}
.onDisappear {
self.unsubscribe_all()
}
.onReceive(handle_notify(.switched_timeline)) { n in
dismiss()
}
}
}
struct ThreadV2View: View {
let damus: DamusState
let thread: ThreadV2
@State var nav_target: String? = nil
@State var navigating: Bool = false
var MaybeBuildThreadView: some View {
Group {
if let evid = nav_target {
BuildThreadV2View(damus: damus, event_id: evid)
} else {
EmptyView()
}
}
}
var body: some View {
NavigationLink(destination: MaybeBuildThreadView, isActive: $navigating) {
EmptyView()
}
ScrollViewReader { reader in
ScrollView {
VStack {
// MARK: - Parents events view
VStack {
ForEach(thread.parentEvents, id: \.id) { event in
MutedEventView(damus_state: damus,
event: event,
scroller: reader,
nav_target: $nav_target,
navigating: $navigating,
selected: false
)
Divider()
.padding(.top, 4)
.padding(.leading, 25 * 2)
}
}.background(GeometryReader { geometry in
// get the height and width of the EventView view
let eventHeight = geometry.frame(in: .global).height
// let eventWidth = geometry.frame(in: .global).width
// vertical gray line in the background
Rectangle()
.fill(Color.gray.opacity(0.25))
.frame(width: 2, height: eventHeight)
.offset(x: 25, y: 40)
})
// MARK: - Actual event view
MutedEventView(
damus_state: damus,
event: thread.current,
scroller: reader,
nav_target: $nav_target,
navigating: $navigating,
selected: true
).id("main")
// MARK: - Responses of the actual event view
LazyVStack {
ForEach(thread.childEvents, id: \.id) { event in
MutedEventView(
damus_state: damus,
event: event,
scroller: nil,
nav_target: $nav_target,
navigating: $navigating,
selected: false
)
Divider()
.padding([.top], 4)
}
}
}.padding()
}.navigationBarTitle(NSLocalizedString("Thread", comment: "Navigation bar title for note thread."))
}
}
}
struct ThreadV2View_Previews: PreviewProvider {
static var previews: some View {
BuildThreadV2View(damus: test_damus_state(), event_id: "ac9fd97b53b0c1d22b3aea2a3d62e11ae393960f5f91ee1791987d60151339a7")
ThreadV2View(
damus: test_damus_state(),
thread: ThreadV2(
parentEvents: [
NostrEvent(id: "1", content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool 4", pubkey: "916b7aca250f43b9f842faccc831db4d155088632a8c27c0d140f2043331ba57"),
NostrEvent(id: "2", content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool 4", pubkey: "916b7aca250f43b9f842faccc831db4d155088632a8c27c0d140f2043331ba57"),
NostrEvent(id: "3", content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool 4", pubkey: "916b7aca250f43b9f842faccc831db4d155088632a8c27c0d140f2043331ba57"),
],
current: NostrEvent(id: "4", content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool 4", pubkey: "916b7aca250f43b9f842faccc831db4d155088632a8c27c0d140f2043331ba57"),
childEvents: [
NostrEvent(id: "5", content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool 4", pubkey: "916b7aca250f43b9f842faccc831db4d155088632a8c27c0d140f2043331ba57"),
NostrEvent(id: "6", content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool 4", pubkey: "916b7aca250f43b9f842faccc831db4d155088632a8c27c0d140f2043331ba57"),
]
)
)
}
}
+101
View File
@@ -0,0 +1,101 @@
//
// ThreadV2View.swift
// damus
//
// Created by Thomas Tastet on 25/12/2022.
//
import SwiftUI
struct ThreadView: View {
let state: DamusState
@StateObject var thread: ThreadModel
@Environment(\.dismiss) var dismiss
var parent_events: [NostrEvent] {
state.events.parent_events(event: thread.event)
}
var child_events: [NostrEvent] {
state.events.child_events(event: thread.event)
}
var body: some View {
ScrollViewReader { reader in
ScrollView {
LazyVStack {
// MARK: - Parents events view
ForEach(parent_events, id: \.id) { parent_event in
MutedEventView(damus_state: state,
event: parent_event,
scroller: reader,
selected: false
)
.onTapGesture {
thread.set_active_event(parent_event, privkey: state.keypair.privkey)
scroll_to_event(scroller: reader, id: parent_event.id, delay: 0.1, animate: false)
}
Divider()
.padding(.top, 4)
.padding(.leading, 25 * 2)
}.background(GeometryReader { geometry in
// get the height and width of the EventView view
let eventHeight = geometry.frame(in: .global).height
// let eventWidth = geometry.frame(in: .global).width
// vertical gray line in the background
Rectangle()
.fill(Color.gray.opacity(0.25))
.frame(width: 2, height: eventHeight)
.offset(x: 25, y: 40)
})
// MARK: - Actual event view
MutedEventView(
damus_state: state,
event: self.thread.event,
scroller: reader,
selected: true
)
.id(self.thread.event.id)
ForEach(child_events, id: \.id) { child_event in
MutedEventView(
damus_state: state,
event: child_event,
scroller: nil,
selected: false
)
.onTapGesture {
thread.set_active_event(child_event, privkey: state.keypair.privkey)
scroll_to_event(scroller: reader, id: child_event.id, delay: 0.1, animate: false)
}
Divider()
.padding([.top], 4)
}
}.padding()
}.navigationBarTitle(NSLocalizedString("Thread", comment: "Navigation bar title for note thread."))
.onAppear {
thread.subscribe()
scroll_to_event(scroller: reader, id: self.thread.event.id, delay: 0.0, animate: false)
}
.onDisappear {
thread.unsubscribe()
}
.onReceive(handle_notify(.switched_timeline)) { notif in
dismiss()
}
}
}
}
struct ThreadView_Previews: PreviewProvider {
static var previews: some View {
let state = test_damus_state()
let thread = ThreadModel(event: test_event, damus_state: state)
ThreadView(state: state, thread: thread)
}
}
+17 -16
View File
@@ -13,21 +13,22 @@ struct InnerTimelineView: View {
let damus: DamusState
let show_friend_icon: Bool
let filter: (NostrEvent) -> Bool
@State var nav_target: NostrEvent? = nil
@State var nav_target: NostrEvent
@State var navigating: Bool = false
var MaybeBuildThreadView: some View {
Group {
if let ev = nav_target {
BuildThreadV2View(damus: damus, event_id: (ev.inner_event ?? ev).id)
} else {
EmptyView()
}
}
init(events: EventHolder, damus: DamusState, show_friend_icon: Bool, filter: @escaping (NostrEvent) -> Bool) {
self.events = events
self.damus = damus
self.show_friend_icon = show_friend_icon
self.filter = filter
// dummy event to avoid MaybeThreadView
self._nav_target = State(initialValue: test_event)
}
var body: some View {
NavigationLink(destination: MaybeBuildThreadView, isActive: $navigating) {
let thread = ThreadModel(event: nav_target, damus_state: damus)
let dest = ThreadView(state: damus, thread: thread)
NavigationLink(destination: dest, isActive: $navigating) {
EmptyView()
}
LazyVStack(spacing: 0) {
@@ -36,26 +37,26 @@ struct InnerTimelineView: View {
EmptyTimelineView()
} else {
ForEach(events.filter(filter), id: \.id) { (ev: NostrEvent) in
EventView(damus: damus, event: ev, has_action_bar: true)
EventView(damus: damus, event: ev, options: [.wide])
.onTapGesture {
nav_target = ev
nav_target = ev.inner_event ?? ev
navigating = true
}
.padding(.top, 10)
.padding(.top, 7)
Divider()
.padding([.top], 10)
.padding([.top], 7)
}
}
}
.padding(.horizontal)
//.padding(.horizontal)
}
}
struct InnerTimelineView_Previews: PreviewProvider {
static var previews: some View {
InnerTimelineView(events: test_event_holder, damus: test_damus_state(), show_friend_icon: true, filter: { _ in true }, nav_target: nil, navigating: false)
InnerTimelineView(events: test_event_holder, damus: test_damus_state(), show_friend_icon: true, filter: { _ in true })
.frame(width: 300, height: 500)
.border(Color.red)
}
+16 -12
View File
@@ -27,17 +27,6 @@ struct TimelineView: View {
MainContent
}
func handle_scroll(_ proxy: GeometryProxy) {
let offset = -proxy.frame(in: .named("scroll")).origin.y
guard offset >= 0 else {
return
}
let val = offset > 0
if self.events.should_queue != val {
self.events.should_queue = val
}
}
var realtime_bar_opacity: Double {
colorScheme == .dark ? 0.2 : 0.1
}
@@ -55,7 +44,7 @@ struct TimelineView: View {
.disabled(loading)
.background(GeometryReader { proxy -> Color in
DispatchQueue.main.async {
handle_scroll(proxy)
handle_scroll_queue(proxy, queue: self.events)
}
return Color.clear
})
@@ -82,3 +71,18 @@ struct TimelineView_Previews: PreviewProvider {
}
protocol ScrollQueue {
var should_queue: Bool { get }
func set_should_queue(_ val: Bool)
}
func handle_scroll_queue(_ proxy: GeometryProxy, queue: ScrollQueue) {
let offset = -proxy.frame(in: .named("scroll")).origin.y
guard offset >= 0 else {
return
}
let val = offset > 0
if queue.should_queue != val {
queue.set_should_queue(val)
}
}
+59 -31
View File
@@ -11,6 +11,7 @@ import Combine
enum ZapType {
case pub
case anon
case priv
case non_zap
}
@@ -80,13 +81,30 @@ struct CustomizeZapView: View {
self.state = state
}
var zap_type_desc: String {
switch zap_type {
case .pub:
return NSLocalizedString("Everyone on can see that you zapped", comment: "Description of public zap type where the zap is sent publicly and identifies the user who sent it.")
case .anon:
return NSLocalizedString("No one can see that you zapped", comment: "Description of anonymous zap type where the zap is sent anonymously and does not identify the user who sent it.")
case .priv:
let pk = event.pubkey
let prof = state.profiles.lookup(id: pk)
let name = Profile.displayName(profile: prof, pubkey: pk).username
return String.localizedStringWithFormat(NSLocalizedString("private_zap_description", value: "Only '%@' can see that you zapped them", comment: "Description of private zap type where the zap is sent privately and does not identify the user to the public."), name)
case .non_zap:
return NSLocalizedString("No zaps are sent, only a lightning payment.", comment: "Description of non-zap type where sats are sent to the user's wallet as a regular Lightning payment, not as a zap.")
}
}
var ZapTypePicker: some View {
Picker(NSLocalizedString("Zap Type", comment: "Header text to indicate that the picker below it is to choose the type of zap to send."), selection: $zap_type) {
Text("Public", comment: "Picker option to indicate that a zap should be sent publicly and identify the user as who sent it.").tag(ZapType.pub)
Text("Private", comment: "Picker option to indicate that a zap should be sent privately and not identify the user to the public.").tag(ZapType.priv)
Text("Anonymous", comment: "Picker option to indicate that a zap should be sent anonymously and not identify the user as who sent it.").tag(ZapType.anon)
Text("Non-Zap", comment: "Picker option to indicate that sats should be sent to the user's wallet as a regular Lightning payment, not as a zap.").tag(ZapType.non_zap)
Text(verbatim: NSLocalizedString("none_zap_type", value: "None", comment: "Picker option to indicate that sats should be sent to the user's wallet as a regular Lightning payment, not as a zap.")).tag(ZapType.non_zap)
}
.pickerStyle(.segmented)
.pickerStyle(.menu)
}
var AmountPicker: some View {
@@ -120,9 +138,9 @@ struct CustomizeZapView: View {
case .failed(let err):
switch err {
case .fetching_invoice:
self.error = "Error fetching lightning invoice"
self.error = NSLocalizedString("Error fetching lightning invoice", comment: "Message to display when there was an error fetching a lightning invoice while attempting to zap.")
case .bad_lnurl:
self.error = "Invalid lightning address"
self.error = NSLocalizedString("Invalid lightning address", comment: "Message to display when there was an error attempting to zap due to an invalid lightning address.")
}
break
case .got_zap_invoice(let inv):
@@ -130,6 +148,7 @@ struct CustomizeZapView: View {
self.invoice = inv
self.showing_wallet_selector = true
} else {
end_editing()
open_with_wallet(wallet: get_default_wallet(state.pubkey).model, invoice: inv)
self.showing_wallet_selector = false
dismiss()
@@ -150,11 +169,13 @@ struct CustomizeZapView: View {
.ignoresSafeArea()
}
var MainContent: some View {
VStack(alignment: .leading) {
Form {
var TheForm: some View {
Form {
Group {
Section(content: {
AmountPicker
.frame(height: 120)
}, header: {
Text("Zap Amount in sats", comment: "Header text to indicate that the picker below it is to choose a pre-defined amount of sats to zap.")
})
@@ -172,39 +193,46 @@ struct CustomizeZapView: View {
}, header: {
Text("Custom Zap Amount", comment: "Header text to indicate that the text field below it is to enter a custom zap amount.")
})
.dismissKeyboardOnTap()
Section(content: {
TextField(NSLocalizedString("Awesome post!", comment: "Placeholder text for a comment to send as part of a zap to the user."), text: $comment)
}, header: {
Text("Comment", comment: "Header text to indicate that the text field below it is a comment that will be used to send as part of a zap to the user.")
})
.dismissKeyboardOnTap()
Section(content: {
ZapTypePicker
}, header: {
Text("Zap Type", comment: "Header text to indicate that the picker below it is to choose the type of zap to send.")
})
if zapping {
Text("Zapping...", comment: "Text to indicate that the app is in the process of sending a zap.")
} else {
Button(NSLocalizedString("Zap", comment: "Button to send a zap.")) {
let amount = custom_amount_sats ?? selected_amount.amount
send_zap(damus_state: state, event: event, lnurl: lnurl, is_custom: true, comment: comment, amount_sats: amount, zap_type: zap_type)
self.zapping = true
}
.zIndex(16)
}
if let error {
Text(error)
.foregroundColor(.red)
}
}
.dismissKeyboardOnTap()
Section(content: {
ZapTypePicker
}, header: {
Text("Zap Type", comment: "Header text to indicate that the picker below it is to choose the type of zap to send.")
}, footer: {
Text(zap_type_desc)
})
if zapping {
Text("Zapping...", comment: "Text to indicate that the app is in the process of sending a zap.")
} else {
Button(NSLocalizedString("Zap", comment: "Button to send a zap.")) {
let amount = custom_amount_sats ?? selected_amount.amount
send_zap(damus_state: state, event: event, lnurl: lnurl, is_custom: true, comment: comment, amount_sats: amount, zap_type: zap_type)
self.zapping = true
}
.zIndex(16)
}
if let error {
Text(error)
.foregroundColor(.red)
}
}
}
var MainContent: some View {
TheForm
}
}
struct CustomizeZapView_Previews: PreviewProvider {
+1 -1
View File
@@ -21,7 +21,7 @@ struct ZapsView: View {
LazyVStack {
ForEach(model.zaps, id: \.event.id) { zap in
ZapEvent(damus: state, zap: zap)
.padding()
.padding([.horizontal])
}
}
}
Binary file not shown.
+240 -24
View File
@@ -50,6 +50,102 @@
<string>المتابِعون</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>zero</key>
<string>المتابَعون</string>
<key>one</key>
<string>المتابَعون</string>
<key>two</key>
<string>المتابَعون</string>
<key>few</key>
<string>المتابَعون</string>
<key>many</key>
<string>المتابَعون</string>
<key>other</key>
<string>المتابَعون</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>zero</key>
<string>%2$@ و %1$d مستخدم آخر تفاعل مع منشور تمت الإشارة لك فيه</string>
<key>one</key>
<string>%2$@ و %1$d مستخدم آخر تفاعل مع منشور تمت الإشارة لك فيه</string>
<key>two</key>
<string>%2$@ و %1$d آخران تفاعلوا مع منشور تمت الإشارة لك فيه</string>
<key>few</key>
<string>%2$@ و %1$d آخرون تفاعلوا مع منشور تمت الإشارة لك فيه</string>
<key>many</key>
<string>%2$@ و %1$d مستخدم آخر تفاعلوا مع منشور تمت الإشارة لك فيه</string>
<key>other</key>
<string>%2$@ و %1$d مستخدم آخر تفاعلوا مع منشور تمت الإشارة لك فيه</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>zero</key>
<string>%2$@ و %1$d مستخدم آخر تفاعل مع منشورك</string>
<key>one</key>
<string>%2$@ و %1$d مستخدم آخر تفاعل مع منشورك</string>
<key>two</key>
<string>%2$@ و %1$d آخران تفاعلا مع منشورك</string>
<key>few</key>
<string>%2$@ و %1$d آخرون تفاعلوا مع منشورك</string>
<key>many</key>
<string>%2$@ و %1$d مستخدم آخر تفاعلوا مع منشورك</string>
<key>other</key>
<string>%2$@ و %1$d مستخدم آخر تفاعلوا مع منشورك</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>zero</key>
<string>%2$@ و %1$d مستخدم آخر تفاعل مع حسابك</string>
<key>one</key>
<string>%2$@ و %1$d مستخدم آخر تفاعل مع حسابك</string>
<key>two</key>
<string>%2$@ و %1$d مستخدمان آخران تفاعلوا مع حسابك</string>
<key>few</key>
<string>%2$@ و %1$d آخرون تفاعلوا مع حسابك</string>
<key>many</key>
<string>%2$@ و %1$d مستخدم آخر تفاعلوا مع حسابك</string>
<key>other</key>
<string>%2$@ و %1$d مستخدم آخر تفاعلوا مع حسابك</string>
</dict>
</dict>
<key>reactions_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
@@ -98,30 +194,6 @@
<string>موصّل</string>
</dict>
</dict>
<key>replying_to_one_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>zero</key>
<string>رد على %2$@</string>
<key>one</key>
<string>الرد على %2$@ &amp; %1$d آخر</string>
<key>two</key>
<string>الرد على %2$@ &amp; %1$d آخرين</string>
<key>few</key>
<string>الرد على %2$@ &amp; %1$d آخرين</string>
<key>many</key>
<string>الرد على %2$@ &amp; %1$d آخرين</string>
<key>other</key>
<string>الرد على %2$@ &amp; %1$d آخرين</string>
</dict>
</dict>
<key>replying_to_two_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
@@ -146,6 +218,78 @@
<string>الرد على %2$@, %3$@ &amp; %1$d آخرين</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>zero</key>
<string>%2$@ و %1$d مستخدم آخر نشر منشورا تمت الإشارة لك فيه</string>
<key>one</key>
<string>%2$@ و %1$d مستخدم آخر نشر منشورا تمت الإشارة لك فيه</string>
<key>two</key>
<string>%2$@ و %1$d مستخدمان آخران نشروا منشورا تمت الإشارة لك فيه</string>
<key>few</key>
<string>%2$@ و %1$d آخرون نشروا منشورا تمت الإشارة لك فيه</string>
<key>many</key>
<string>%2$@ و %1$d مستخدم آخر نشروا منشورا تمت الإشارة لك فيه</string>
<key>other</key>
<string>%2$@ و %1$d مستخدم آخر نشروا منشورا تمت الإشارة لك فيه</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>zero</key>
<string>%2$@ و %1$d مستخدم آخر نشر منشورك</string>
<key>one</key>
<string>%2$@ و %1$d مستخدم آخر نشر منشورك</string>
<key>two</key>
<string>%2$@ و %1$d مستخدمان آخران نشروا منشورك</string>
<key>few</key>
<string>%2$@ و %1$d آخرون نشروا منشورك</string>
<key>many</key>
<string>%2$@ و %1$d مستخدم آخر نشروا منشورك</string>
<key>other</key>
<string>%2$@ و %1$d مستخدم آخر نشروا منشورك</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>zero</key>
<string>%2$@ و %1$d مستخدم آخر نشر حسابك</string>
<key>one</key>
<string>%2$@ و %1$d مستخدم آخر نشر حسابك</string>
<key>two</key>
<string>%2$@ و %1$d مستخدمان آخران نشروا حسابك</string>
<key>few</key>
<string>%2$@ و %1$d آخرون نشروا حسابك</string>
<key>many</key>
<string>%2$@ و %1$d مستخدم آخر نشروا حسابك</string>
<key>other</key>
<string>%2$@ و %1$d مستخدم آخر نشروا حسابك</string>
</dict>
</dict>
<key>reposts_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
@@ -194,6 +338,78 @@
<string>%2$@ ساتوشي</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>zero</key>
<string>%2$@ و %1$d مستخدم آخر ومّض منشورا تمت الإشارة لك فيه</string>
<key>one</key>
<string>%2$@ و %1$d مستخدم آخر ومّض منشورا تمت الإشارة لك فيه</string>
<key>two</key>
<string>%2$@ و %1$d مستخدمان آخران ومّضوا منشورا تمت الإشارة لك فيه</string>
<key>few</key>
<string>%2$@ و %1$d آخررن ومّضوا منشورا تمت الإشارة لك فيه</string>
<key>many</key>
<string>%2$@ و %1$d مستخدم آخر ومّضوا منشورا تمت الإشارة لك فيه</string>
<key>other</key>
<string>%2$@ و %1$d مستخدم آخر ومّضوا منشورا تمت الإشارة لك فيه</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>zero</key>
<string>%2$@ و %1$d مستخدم آخر ومّض منشورك</string>
<key>one</key>
<string>%2$@ و %1$d مستخدم آخر ومّض منشورك</string>
<key>two</key>
<string>%2$@ و %1$d مستخدمان آخران ومّضوا منشورك</string>
<key>few</key>
<string>%2$@ و %1$d آخرون ومّضوا منشورك</string>
<key>many</key>
<string>%2$@ و %1$d مستخدم آخر ومّضوا منشورك</string>
<key>other</key>
<string>%2$@ و %1$d مستخدم آخر ومّضوا منشورك</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>zero</key>
<string>%2$@ و %1$d مستخدم آخر ومّض حسابك</string>
<key>one</key>
<string>%2$@ و %1$d مستخدم آخر ومّض حسابك</string>
<key>two</key>
<string>%2$@ و %1$d مستخدمان آخران ومّضوا حسابك</string>
<key>few</key>
<string>%2$@ و %1$d آخرون ومّضوا حسابك</string>
<key>many</key>
<string>%2$@ و %1$d مستخدم آخر ومّضوا حسابك</string>
<key>other</key>
<string>%2$@ و %1$d مستخدم آخر ومّضوا حسابك</string>
</dict>
</dict>
<key>zaps_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
Binary file not shown.
Binary file not shown.
+278
View File
@@ -0,0 +1,278 @@
<?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 друга бележка ...</string>
<key>other</key>
<string>... %d други бележки ...</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>Последовател</string>
<key>other</key>
<string>Последователи</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$@ и %1$d друг реагираха на бележка, в която ти бе споменат </string>
<key>other</key>
<string>%2$@ и %1$d други реагираха на бележка, в която ти бе споменат</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$@ и %1$d друг реагираха на твоята бележка</string>
<key>other</key>
<string>%2$@ и %1$d други реагираха на твоята бележка</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$@ и %1$d друг реагираха на твоя профил</string>
<key>other</key>
<string>%2$@ и %1$d други реагираха на твоя профил</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>Реакция</string>
<key>other</key>
<string>Реакции</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>Реле</string>
<key>other</key>
<string>Релета</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>Отговори на %2$@, %3$@ &amp; %1$d друг</string>
<key>other</key>
<string>Отговори на %2$@, %3$@ &amp; %1$d други</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$@ и %1$d друг споделиха бележка, в която ти бе споменат</string>
<key>other</key>
<string>%2$@ и %1$d други споделиха бележка, в която ти бе споменат</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$@ и %1$d друг споделиха твоята бележка</string>
<key>other</key>
<string>%2$@ и %1$d други споделиха твоята бележка</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$@ и %1$d други споделиха твоя профил</string>
<key>other</key>
<string>%2$@ и %1$d други споделиха твоя профил</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>Споделяне</string>
<key>other</key>
<string>Споделяния</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$@ сатоши</string>
<key>other</key>
<string>%2$@ сатошита</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$@ и %1$d друг пратиха zap на бележка, в която ти бе споменат</string>
<key>other</key>
<string>%2$@ и %1$d други пратиха zap на бележка, в която ти бе споменат</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$@ и %1$d друг пратиха zap на твоята бележка</string>
<key>other</key>
<string>%2$@ и %1$d други пратиха zap на твоята бележка</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$@ и %1$d друг пратиха zap на вашия профил</string>
<key>other</key>
<string>%2$@ и %1$d други пратих zap на твоя профил</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>other</key>
<string>Zaps</string>
</dict>
</dict>
</dict>
</plist>
Binary file not shown.
+204 -24
View File
@@ -42,6 +42,86 @@
<string>Sledují</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>Sleduje</string>
<key>few</key>
<string>Sleduje</string>
<key>many</key>
<string>Sleduje</string>
<key>other</key>
<string>Sleduje</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$@ a další %1$d reagovali na příspěvek, ve kterém jste byli označeni</string>
<key>few</key>
<string>%2$@ a další %1$d reagovali na příspěvek, ve kterém jste byli označeni</string>
<key>many</key>
<string>%2$@ a další %1$d reagovali na příspěvek, ve kterém jste byli označeni</string>
<key>other</key>
<string>%2$@ a dalších %1$d reagovali na příspěvek, ve kterém jste byli označeni</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$@ a další %1$d zareagovali na váš příspěvek</string>
<key>few</key>
<string>%2$@ a další %1$d zareagovali na váš příspěvek</string>
<key>many</key>
<string>%2$@ a další %1$d zareagovali na váš příspěvek</string>
<key>other</key>
<string>%2$@ a dalších %1$d zareagovali na váš příspěvek</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$@ a další %1$d zareagovali na váš profil</string>
<key>few</key>
<string>%2$@ a další %1$d zareagovali na váš profil</string>
<key>many</key>
<string>%2$@ a další %1$d zareagovali na váš profil</string>
<key>other</key>
<string>%2$@ a dalších %1$d zareagovali na váš profil</string>
</dict>
</dict>
<key>reactions_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
@@ -82,26 +162,6 @@
<string>Relé</string>
</dict>
</dict>
<key>replying_to_one_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>Odpověď na %2$@ a %1$d další</string>
<key>few</key>
<string>Odpověď na %2$@ a %1$d others</string>
<key>many</key>
<string>Odpověď na %2$@ a %1$d others</string>
<key>other</key>
<string>Odpověď na %2$@ a %1$d další</string>
</dict>
</dict>
<key>replying_to_two_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
@@ -113,13 +173,73 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Odpovědět na %2$@, %3$@ &amp; %1$d další</string>
<string>Odpovědět na %2$@, %3$@ &amp; a další %1$d </string>
<key>few</key>
<string>Odpovědět na %2$@, %3$@ &amp; %1$d others</string>
<string>Odpovědět na %2$@, %3$@ &amp; a další %1$d</string>
<key>many</key>
<string>Odpovědět na %2$@, %3$@ &amp; %1$d others</string>
<string>Odpovědět na %2$@, %3$@ &amp; a další %1$d</string>
<key>other</key>
<string>Odpovědět na %2$@, %3$@ &amp; %1$d další</string>
<string>Odpovědět na %2$@, %3$@ &amp; a dalších %1$d</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$@ a další %1$d přesdíleli příspěvek, ve kterém jste byli označeni</string>
<key>few</key>
<string>%2$@ a další %1$d přesdíleli příspěvek, ve kterém jste byli označeni</string>
<key>many</key>
<string>%2$@ a další %1$d přesdíleli příspěvek, ve kterém jste byli označeni</string>
<key>other</key>
<string>%2$@ a dalších %1$d přesdíleli příspěvek, ve kterém jste byli označeni</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$@ a další %1$d přesdíleli váš příspěvek</string>
<key>few</key>
<string>%2$@ a další %1$d přesdíleli váš příspěvek</string>
<key>many</key>
<string>%2$@ a další %1$d přesdíleli váš příspěvek</string>
<key>other</key>
<string>%2$@ a dalších %1$d přesdíleli váš příspěvek</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$@ a další %1$d přesdíleli váš profil</string>
<key>few</key>
<string>%2$@ a další %1$d přesdíleli váš profil</string>
<key>many</key>
<string>%2$@ a další %1$d přesdíleli váš profil</string>
<key>other</key>
<string>%2$@ a dalších %1$d přesdíleli váš profil</string>
</dict>
</dict>
<key>reposts_count</key>
@@ -162,6 +282,66 @@
<string>%2$@ satů</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$@ a další %1$d ZAP-nuli příspěvek, ve kterém jste byli označeni</string>
<key>few</key>
<string>%2$@ a další %1$d ZAP-nuli příspěvek, ve kterém jste byli označeni</string>
<key>many</key>
<string>%2$@ a další %1$d ZAP-nuli příspěvek, ve kterém jste byli označeni</string>
<key>other</key>
<string>%2$@ a dalších %1$d ZAP-nuli příspěvek, ve kterém jste byli označeni</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$@ a další %1$d ZAP-nuli váš příspěvek</string>
<key>few</key>
<string>%2$@ a další %1$d ZAP-nuli váš příspěvek</string>
<key>many</key>
<string>%2$@ a další %1$d ZAP-nuli váš příspěvek</string>
<key>other</key>
<string>%2$@ a dalších %1$d ZAP-nuli váš příspěvek</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$@ a další %1$d ZAP-nuli váš profil</string>
<key>few</key>
<string>%2$@ a další %1$d ZAP-nuli váš profil</string>
<key>many</key>
<string>%2$@ a další %1$d ZAP-nuli váš profil</string>
<key>other</key>
<string>%2$@ a dalších %1$d ZAP-nuli váš profil</string>
</dict>
</dict>
<key>zaps_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
Binary file not shown.
+161 -17
View File
@@ -34,6 +34,70 @@
<string>Follower</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>Folge ich</string>
<key>other</key>
<string>Folge ich</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$@ und %1$d andere:r reagierten auf einen Beitrag in dem Du markiert warst</string>
<key>other</key>
<string>%2$@ und %1$d andere reagierten auf einen Beitrag in dem Du markiert warst</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$@ und %1$d andere:r reagierten auf deinen Beitrag</string>
<key>other</key>
<string>%2$@ und %1$d andere reagierten auf deinen Beitrag</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$@ und %1$d andere:r reagierten auf dein Profil</string>
<key>other</key>
<string>%2$@ und %1$d andere reagierten auf dein Profil</string>
</dict>
</dict>
<key>reactions_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
@@ -66,22 +130,6 @@
<string>Relays</string>
</dict>
</dict>
<key>replying_to_one_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>Antwort an %2$@ &amp; %1$d andere</string>
<key>other</key>
<string>Antwort an %2$@ &amp; %1$d andere</string>
</dict>
</dict>
<key>replying_to_two_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
@@ -93,11 +141,59 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Antwort an %2$@, %3$@ &amp; %1$d andere</string>
<string>Antwort an %2$@, %3$@ &amp; %1$d andere:r</string>
<key>other</key>
<string>Antwort an %2$@, %3$@ &amp; %1$d andere</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$@ und %1$d andere:r teilten einen Beitrag in dem Du markiert warst</string>
<key>other</key>
<string>%2$@ und %1$d andere teilten ein Beitrag in dem Du markiert warst</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$@ und %1$d andere:r teilten deinen Beitrag</string>
<key>other</key>
<string>%2$@ und %1$d andere teilten deinen Beitrag</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$@ und %1$d andere:r teilten dein Profil</string>
<key>other</key>
<string>%2$@ und %1$d andere teilten dein Profil</string>
</dict>
</dict>
<key>reposts_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
@@ -130,6 +226,54 @@
<string>%2$@ sats</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$@ und %1$d andere:r zappten einen Beitrag in dem Du markiert warst</string>
<key>other</key>
<string>%2$@ und %1$d andere zappten einen Beitrag in dem Du markiert warst</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$@ und %1$d andere:r zappten deinen Beitrag</string>
<key>other</key>
<string>%2$@ und %1$d andere zappten deinen Beitrag</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$@ und %1$d andere:r zappten dein Profil</string>
<key>other</key>
<string>%2$@ und %1$d andere zappten dein Profil</string>
</dict>
</dict>
<key>zaps_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
Binary file not shown.

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