Compare commits

...

274 Commits

Author SHA1 Message Date
866e93d338 Add auto-translate setting 2023-03-27 10:31:58 -04:00
f75fc7eebe Add optional language filter on Universe feed 2023-03-27 10:26:18 -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
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
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
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
d5a646f9ce Update Translations 🤖 2023-03-12 18:15:48 +00:00
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
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
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
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
240fda2429 Merge branch 'tyiu/notifications' into tyiu/translations 2023-02-27 10:58:10 +13:00
bacd9b3c38 Add strings for event grouped notifications 2023-02-27 10:47:05 +13:00
0152286859 Fix missing comments on new strings 2023-02-27 10:43:00 +13:00
transifex-integration[bot]
06e9a1b392 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.
2023-02-27 10:27:19 +13:00
transifex-integration[bot]
483730af18 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.
2023-02-27 10:27:19 +13:00
transifex-integration[bot]
23229015a6 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.
2023-02-27 10:27:19 +13:00
transifex-integration[bot]
7ab95583df 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.
2023-02-27 10:27:19 +13:00
transifex-integration[bot]
b7a48a24e9 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.
2023-02-27 10:27:18 +13:00
04028d9cff Fix wording in SaveKeysView to be more mobile-friendly 2023-02-27 10:27:18 +13:00
6918fb46cf Fix localization bug on RelayFilterView 2023-02-27 10:27:18 +13:00
transifex-integration[bot]
2b854ef9b7 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.
2023-02-27 10:27:18 +13:00
transifex-integration[bot]
5eb61f1ac1 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.
2023-02-27 10:27:18 +13:00
transifex-integration[bot]
c3bbf7aa8f 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.
2023-02-27 10:27:18 +13:00
transifex-integration[bot]
1e52d958ac 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.
2023-02-27 10:27:18 +13:00
transifex-integration[bot]
5252e5f5bb 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.
2023-02-27 10:27:17 +13:00
transifex-integration[bot]
71d5625f04 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.
2023-02-27 10:27:17 +13:00
transifex-integration[bot]
990e783c30 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.
2023-02-27 10:27:17 +13:00
transifex-integration[bot]
3602189133 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.
2023-02-27 10:27:17 +13:00
transifex-integration[bot]
3ca9acdf34 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.
2023-02-27 10:27:17 +13:00
transifex-integration[bot]
2036d5843b 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.
2023-02-27 10:27:17 +13:00
transifex-integration[bot]
2d3bd11d56 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.
2023-02-27 10:27:17 +13:00
transifex-integration[bot]
a715987e71 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.
2023-02-27 10:27:16 +13:00
transifex-integration[bot]
0303031445 Apply translations in uk
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'uk' language.
2023-02-27 10:27:16 +13:00
transifex-integration[bot]
d6ae9a5d79 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.
2023-02-27 10:27:16 +13:00
transifex-integration[bot]
356bd06e6a 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.
2023-02-27 10:27:16 +13:00
transifex-integration[bot]
e757bdca90 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.
2023-02-27 10:27:16 +13:00
transifex-integration[bot]
ff1b4d724d 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.
2023-02-27 10:27:16 +13:00
transifex-integration[bot]
b8614f055c 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.
2023-02-27 10:27:16 +13:00
transifex-integration[bot]
63ab151a5e 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.
2023-02-27 10:27:15 +13:00
transifex-integration[bot]
fd9d4deb44 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.
2023-02-27 10:27:15 +13:00
transifex-integration[bot]
a5c719673e Apply translations in zh_CN
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-02-27 10:27:15 +13:00
transifex-integration[bot]
2bf3c6718d 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.
2023-02-27 10:27:15 +13:00
transifex-integration[bot]
3f3e59488a Apply translations in fa
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'fa' language.
2023-02-27 10:27:15 +13:00
transifex-integration[bot]
b1b4b5b6c9 Apply translations in fa
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/InfoPlist.strings'
on the 'fa' language.
2023-02-27 10:27:15 +13:00
transifex-integration[bot]
7455665672 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.
2023-02-27 10:27:15 +13:00
transifex-integration[bot]
9f22234926 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.
2023-02-27 10:27:15 +13:00
transifex-integration[bot]
21a8a4e96f 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.
2023-02-27 10:27:14 +13:00
transifex-integration[bot]
c635f3d77a 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.
2023-02-27 10:27:14 +13:00
transifex-integration[bot]
2e9a4388b9 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.
2023-02-27 10:27:14 +13:00
transifex-integration[bot]
4a1949eeb8 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.
2023-02-27 10:27:14 +13:00
transifex-integration[bot]
b4fcb58bcb 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.
2023-02-27 10:27:14 +13:00
transifex-integration[bot]
7d852eb33b 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.
2023-02-27 10:27:14 +13:00
transifex-integration[bot]
a448f610c0 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.
2023-02-27 10:27:14 +13:00
transifex-integration[bot]
8cc561b8c6 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.
2023-02-27 10:27:13 +13:00
transifex-integration[bot]
01630d0a4c Apply translations in zh_CN
translated for the source file 'damus/en-US.xcloc/Source Contents/damus/en-US.lproj/Localizable.strings'
on the 'zh_CN' language.
2023-02-27 10:27:13 +13:00
16156f4d9a Fix number formatting for Arabic and other languages 2023-02-27 10:27:13 +13:00
f840fe9c80 Update source English strings per feedback from translators 2023-02-27 10:27:13 +13:00
77bcd1b715 Fix localization and add tests for EventGroupView 2023-02-27 10:20:50 +13:00
Swift
75fd8de456 Fix post text in darkmode
Closes: #693
2023-02-26 12:21:04 -08:00
William Casarin
71f7ea47df Customized Zaps
Changelog-Added: Customized zaps
2023-02-26 11:53:29 -08:00
William Casarin
64b1a57918 Notifications
Changelog-Added: Add new Notifications View
2023-02-25 11:33: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
William Casarin
e4dd585754 Fix text color in dark mode on post view 2023-02-24 15:11:09 -08:00
Swift
436d20dfbd Rich tagging
Changelog-Changed: No more inline npubs when tagging users
Closes: #691
2023-02-24 13:35:33 -08:00
OlegAba
810b3e1fa5 Fix mention rounded border
Closes: #670
2023-02-24 13:04:36 -08:00
OlegAba
75fb0d19e2 Fix ImageCarousel corner radius and context menu 2023-02-24 13:04:13 -08:00
OlegAba
a2749eaaaa Fix event dividers 2023-02-24 13:04:11 -08:00
OlegAba
83c9289345 Lazy loading of thread child events
Closes: #679
2023-02-24 12:49:26 -08:00
Joel Klabo
4c3a83772e Update Alignment of Side Menu Labels
Changelog-Fixed: Fix alignment of side menu labels
Closes: #688
2023-02-24 12:45:47 -08:00
5cd4c2d75e Fix localization issues, add tests, import translations, and add zh-CN and zh-TW
Closes: #689
2023-02-24 12:44:54 -08:00
Joel Klabo
85e797a054 Embed in ScrollView 2023-02-24 12:42:49 -08:00
Joel Klabo
9f52e2c246 Fix Identical Participants in ParticipantView
Changelog-Fixed: Fix duplicated participants in reply-to view
Closes: #685
2023-02-24 12:42:18 -08: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
Joel Klabo
8b9958a4ad Add Bookmarking (Local to device)
Changelog-Added: Bookmarking
Closes: #649
2023-02-21 12:33:21 -08:00
William Casarin
87a0bdac94 Load missing profiles in Zaps view
Changelog-Fixed: Load missing profiles in Zaps view
2023-02-21 10:08:15 -08:00
William Casarin
37b964c296 Fix issue where CPU is continuously pegged when scrolling
Since should_queue is continuosly getting set, this is causing the
InnerTimelineView to continuously rerender, pegging the CPU to 100%

This change only updates the var if it changes from the previous value.
2023-02-21 05:07:27 -08:00
William Casarin
b1a2b47116 Eliminate popping when scrolling
This commit makes a few changes:

- Link preview views are no longer cached, only the metadata. This fixes
  a memory leak when preview videos. It will keep playing the video
  forever eventually leading to a crash. This is fixed!

- Cache the intrinsic height of previews, when loading notes it looks
  for the cached height so that things don't pop-in after the fact

- Note artifacts and previews are set in the constructor instead of
  onAppear, this prevents the size from changing and popping after it
  has been loaded into the lazyvstack

Changelog-Fixed: Fix memory leak with inline videos
Changelog-Fixed: Eliminate popping when scrolling
2023-02-21 04:36:01 -08:00
202 changed files with 11839 additions and 2476 deletions

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

View File

@@ -11,6 +11,11 @@
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */; };
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.swift */; };
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; };
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */; };
3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */; };
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */; };
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; };
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */; };
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; };
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
@@ -43,6 +48,11 @@
4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8B28398BC6008A31F1 /* Keys.swift */; };
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */; };
4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */; };
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */; };
4C30AC7429A5680900E2BD5A /* EventGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7329A5680900E2BD5A /* EventGroupView.swift */; };
4C30AC7629A5770900E2BD5A /* NotificationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */; };
4C30AC7829A577AB00E2BD5A /* EventCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7729A577AB00E2BD5A /* EventCache.swift */; };
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7F29A6A53F00E2BD5A /* ProfilePicturesView.swift */; };
4C363A8428233689006E126D /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8328233689006E126D /* Parser.swift */; };
4C363A8828236948006E126D /* BlocksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8728236948006E126D /* BlocksView.swift */; };
4C363A8A28236B57006E126D /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8928236B57006E126D /* MentionView.swift */; };
@@ -93,6 +103,9 @@
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */; };
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C42812B298C848200DBF26F /* TranslateView.swift */; };
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C477C9D282C3A4800033AA3 /* TipCounter.swift */; };
4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0629A540BA003E4487 /* NotificationsModel.swift */; };
4C54AA0A29A55429003E4487 /* EventGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0929A55429003E4487 /* EventGroup.swift */; };
4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0B29A5543C003E4487 /* ZapGroup.swift */; };
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */; };
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */; };
4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5F9113283D694D0052CD1C /* FollowTarget.swift */; };
@@ -122,6 +135,10 @@
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 */; };
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AC298851D000060CEA /* AccountDeletion.swift */; };
4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */; };
@@ -155,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 */; };
@@ -201,25 +222,36 @@
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 */; };
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */; };
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */; };
@@ -255,6 +287,18 @@
3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A2B8B0A296A8982009CC16D /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "en-US"; path = "en-US.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyDescriptionTests.swift; sourceTree = "<group>"; };
3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailBarTests.swift; sourceTree = "<group>"; };
3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationUtil.swift; sourceTree = "<group>"; };
3A3040F229A91366008A0F29 /* ProfileViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewTests.swift; sourceTree = "<group>"; };
3A3040F929A91ED6008A0F29 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A3040FA29A91EFC008A0F29 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A3040FB29A91F03008A0F29 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-HK"; path = "zh-HK.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A3040FC29A91F31008A0F29 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-TW"; path = "zh-TW.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A3040FD29A91F31008A0F29 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-TW"; path = "zh-TW.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A3040FE29A91F31008A0F29 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-TW"; path = "zh-TW.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A3040FF29AB02D1008A0F29 /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-US"; path = "en-US.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGroupViewTests.swift; sourceTree = "<group>"; };
3A41E559299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@@ -287,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>"; };
@@ -299,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>"; };
@@ -328,6 +393,11 @@
4C285C8B28398BC6008A31F1 /* Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keys.swift; sourceTree = "<group>"; };
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveKeysView.swift; sourceTree = "<group>"; };
4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; };
4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
4C30AC7329A5680900E2BD5A /* EventGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGroupView.swift; sourceTree = "<group>"; };
4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemView.swift; sourceTree = "<group>"; };
4C30AC7729A577AB00E2BD5A /* EventCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventCache.swift; sourceTree = "<group>"; };
4C30AC7F29A6A53F00E2BD5A /* ProfilePicturesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePicturesView.swift; sourceTree = "<group>"; };
4C363A8328233689006E126D /* Parser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = "<group>"; };
4C363A8728236948006E126D /* BlocksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlocksView.swift; sourceTree = "<group>"; };
4C363A8928236B57006E126D /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = "<group>"; };
@@ -408,6 +478,9 @@
4C42812B298C848200DBF26F /* TranslateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslateView.swift; sourceTree = "<group>"; };
4C477C9D282C3A4800033AA3 /* TipCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipCounter.swift; sourceTree = "<group>"; };
4C4A3A5A288A1B2200453788 /* damus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = damus.entitlements; sourceTree = "<group>"; };
4C54AA0629A540BA003E4487 /* NotificationsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsModel.swift; sourceTree = "<group>"; };
4C54AA0929A55429003E4487 /* EventGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGroup.swift; sourceTree = "<group>"; };
4C54AA0B29A5543C003E4487 /* ZapGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapGroup.swift; sourceTree = "<group>"; };
4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeModel.swift; sourceTree = "<group>"; };
4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsView.swift; sourceTree = "<group>"; };
4C5F9113283D694D0052CD1C /* FollowTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowTarget.swift; sourceTree = "<group>"; };
@@ -437,6 +510,10 @@
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>"; };
4CAAD8AC298851D000060CEA /* AccountDeletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletion.swift; sourceTree = "<group>"; };
4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConfigView.swift; sourceTree = "<group>"; };
@@ -470,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>"; };
@@ -519,24 +600,35 @@
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>"; };
F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIPURLBuilder.swift; sourceTree = "<group>"; };
F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToDismiss.swift; sourceTree = "<group>"; };
@@ -584,6 +676,7 @@
isa = PBXGroup;
children = (
3AA24801297E3DC20090C62D /* RepostView.swift */,
4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */,
);
path = Reposts;
sourceTree = "<group>";
@@ -645,6 +738,8 @@
4C0A3F8D280F63FF000448DE /* Models */ = {
isa = PBXGroup;
children = (
4CCEB7A729B29DC90078AA28 /* Search */,
4C54AA0829A55416003E4487 /* Notifications */,
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */,
4C0A3F8E280F640A000448DE /* ThreadModel.swift */,
4C0A3F92280F66F5000448DE /* ReplyMap.swift */,
@@ -665,6 +760,7 @@
4C63334F283D40E500B1C9C3 /* HomeModel.swift */,
4C633351283D419F00B1C9C3 /* SignalModel.swift */,
4C5F9113283D694D0052CD1C /* FollowTarget.swift */,
F75BA12C29A1855400E10810 /* BookmarksManager.swift */,
4C5F9115283D855D0052CD1C /* EventsModel.swift */,
4C5F9117283D88E40052CD1C /* FollowingModel.swift */,
4C987B56283FD07F0042CE38 /* FollowersModel.swift */,
@@ -683,13 +779,38 @@
3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */,
4CE8795A2996C47A00F758CC /* ZapsModel.swift */,
3AA59D1C2999B0400061C48E /* DraftsModel.swift */,
4C54AA0629A540BA003E4487 /* NotificationsModel.swift */,
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */,
);
path = Models;
sourceTree = "<group>";
};
4C30AC7029A5676F00E2BD5A /* Notifications */ = {
isa = PBXGroup;
children = (
4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */,
4C30AC7329A5680900E2BD5A /* EventGroupView.swift */,
4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */,
4C30AC7F29A6A53F00E2BD5A /* ProfilePicturesView.swift */,
);
path = Notifications;
sourceTree = "<group>";
};
4C54AA0829A55416003E4487 /* Notifications */ = {
isa = PBXGroup;
children = (
4C54AA0929A55429003E4487 /* EventGroup.swift */,
4C54AA0B29A5543C003E4487 /* ZapGroup.swift */,
);
path = Notifications;
sourceTree = "<group>";
};
4C75EFA227FA576C0006080F /* Views */ = {
isa = PBXGroup;
children = (
4CFF8F6129CC9A80008DB934 /* Images */,
4CCEB7AC29B53D180078AA28 /* Search */,
4C30AC7029A5676F00E2BD5A /* Notifications */,
4CE0E2B029A3DF4700DB4CA2 /* Timeline */,
4CE879562996C44A00F758CC /* Zaps */,
4CB9D4A52992D01900A9A7E4 /* Profile */,
@@ -702,6 +823,7 @@
4CB88387296AF97C00DC99E7 /* ActionBar */,
4CE4F9E228528C5200C00DD9 /* AddRelayView.swift */,
4C363A8728236948006E126D /* BlocksView.swift */,
F75BA12E29A18EF500E10810 /* BookmarksView.swift */,
4C285C8128385570008A31F1 /* CarouselView.swift */,
4C0A3F8B280F5FCA000448DE /* ChatroomView.swift */,
4C0A3F90280F6528000448DE /* ChatView.swift */,
@@ -721,11 +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 */,
@@ -736,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 */,
@@ -775,6 +895,7 @@
4C7FF7D628233637009601DB /* Util */ = {
isa = PBXGroup;
children = (
7C0F392D29B57C8F0039859C /* Extensions */,
4CE879492995B58700F758CC /* Relays */,
4CF0ABEA29844B2F00D66079 /* AnyCodable */,
4CC7AAE6297EFA7B00430951 /* Zap.swift */,
@@ -802,8 +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>";
@@ -828,6 +951,7 @@
children = (
4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */,
4CB88388296AF99A00DC99E7 /* EventDetailBar.swift */,
5CF72FC129B9142F00124A13 /* ShareAction.swift */,
);
path = ActionBar;
sourceTree = "<group>";
@@ -843,8 +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>";
@@ -862,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 = (
@@ -965,6 +1113,10 @@
4CF0ABDB2981A19E00D66079 /* ListTests.swift */,
4CB883A9297612FF00DC99E7 /* ZapTests.swift */,
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */,
3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */,
3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */,
3A3040F229A91366008A0F29 /* ProfileViewTests.swift */,
3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@@ -999,6 +1151,7 @@
isa = PBXGroup;
children = (
4CE879572996C45300F758CC /* ZapsView.swift */,
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */,
);
path = Zaps;
sourceTree = "<group>";
@@ -1037,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 = (
@@ -1154,6 +1326,15 @@
id,
cs,
ru,
"zh-HK",
"zh-TW",
uk,
bg,
fa,
ko,
"hu-HU",
"sv-SE",
"fr-CA",
);
mainGroup = 4CE6DEDA27F7A08100C66700;
packageReferences = (
@@ -1208,8 +1389,10 @@
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 */,
4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */,
4C216F34286F5ACD00040376 /* DMView.swift in Sources */,
4C3EA64428FF558100C48A62 /* sha256.c in Sources */,
@@ -1222,9 +1405,11 @@
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */,
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */,
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */,
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 */,
@@ -1233,8 +1418,10 @@
4C363AA228296A7E006E126D /* SearchView.swift in Sources */,
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */,
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */,
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */,
F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */,
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */,
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */,
@@ -1259,21 +1446,26 @@
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 */,
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
4C30AC7629A5770900E2BD5A /* NotificationItemView.swift in Sources */,
4C363A8428233689006E126D /* Parser.swift in Sources */,
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */,
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 */,
4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */,
4CB883A82975FC1800DC99E7 /* Zaps.swift in Sources */,
4C75EFB128049D510006080F /* NostrResponse.swift in Sources */,
@@ -1282,10 +1474,13 @@
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */,
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */,
4C285C8228385570008A31F1 /* CarouselView.swift in Sources */,
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */,
F75BA12D29A1855400E10810 /* BookmarksManager.swift in Sources */,
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */,
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 */,
@@ -1294,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 */,
@@ -1310,16 +1506,22 @@
4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */,
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */,
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 */,
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */,
@@ -1335,11 +1537,13 @@
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 */,
4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */,
4CF0ABD82981980C00D66079 /* Lists.swift in Sources */,
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */,
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */,
@@ -1368,7 +1572,10 @@
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 */,
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
@@ -1393,11 +1600,13 @@
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */,
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */,
4C75EFB528049D790006080F /* Relay.swift in Sources */,
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */,
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 */,
@@ -1408,8 +1617,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */,
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */,
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */,
3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */,
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */,
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */,
@@ -1419,6 +1631,7 @@
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */,
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */,
4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */,
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */,
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1468,6 +1681,15 @@
3A41E55B299D52BE001FA465 /* id */,
3A8624DB299E82BE00BD8BE9 /* cs */,
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>";
@@ -1491,6 +1713,15 @@
3A41E559299D52BE001FA465 /* id */,
3A8624D9299E82BE00BD8BE9 /* cs */,
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>";
@@ -1514,6 +1745,16 @@
3A41E55A299D52BE001FA465 /* id */,
3A8624DA299E82BE00BD8BE9 /* cs */,
3A827A19299FC69D00C4D171 /* ru */,
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>";
@@ -1649,7 +1890,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1672,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;
@@ -1691,7 +1932,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 3;
CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -1714,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;

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 {

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,24 +51,24 @@ struct ImageCarousel: View {
.configure { view in
view.framePreloadCount = 3
}
.aspectRatio(contentMode: .fit)
.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
// }
// }
}
}
}
.cornerRadius(10)
.fullScreenCover(isPresented: $open_sheet) {
ImageView(urls: urls)
}
.frame(height: 200)
.frame(height: 350)
.onTapGesture {
open_sheet = true
}

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)
}
}

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
}

View File

@@ -18,6 +18,8 @@ struct TranslateView: View {
@State var translated_note: String? = nil
@State var show_translated_note: Bool = false
@State var translated_artifacts: NoteArtifacts? = nil
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
var TranslateButton: some View {
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
@@ -80,24 +82,7 @@ struct TranslateView: View {
currentLanguage = Locale.current.languageCode ?? "en"
}
// Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in
// and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer.
let originalBlocks = event.blocks(damus_state.keypair.privkey)
let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ")
// Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate.
let languageRecognizer = NLLanguageRecognizer()
languageRecognizer.processString(originalOnlyText)
noteLanguage = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= 0.5 })?.key.rawValue ?? currentLanguage
if let lang = noteLanguage, noteLanguage != currentLanguage {
// If the detected dominant language is a variant, remove the variant component and just take the language part as translation services typically only supports the variant-less language.
if #available(iOS 16, *) {
noteLanguage = Locale.LanguageCode(stringLiteral: lang).identifier(.alpha2)
} else {
noteLanguage = NSLocale(localeIdentifier: lang).languageCode
}
}
noteLanguage = event.note_language(damus_state.keypair.privkey) ?? currentLanguage
guard let note_lang = noteLanguage else {
noteLanguage = currentLanguage
@@ -106,9 +91,9 @@ struct TranslateView: View {
return
}
if note_lang != currentLanguage {
if !preferredLanguages.contains(note_lang) {
do {
// If the note language is different from our language, send a translation request.
// If the note language is different from our preferred languages, send a translation request.
let translator = Translator(damus_state.settings)
let originalContent = event.get_content(damus_state.keypair.privkey)
translated_note = try await translator.translate(originalContent, from: note_lang, to: currentLanguage)
@@ -132,11 +117,21 @@ struct TranslateView: View {
}
checkingTranslationStatus = false
show_translated_note = damus_state.settings.auto_translate
}
}
}
extension View {
func translate_button_style() -> some View {
return self
.font(.footnote)
.contentShape(Rectangle())
.padding([.top, .bottom], 10)
}
}
struct TranslateView_Previews: PreviewProvider {
static var previews: some View {
let ds = test_damus_state()

View File

@@ -12,11 +12,7 @@ struct UserView: View {
let pubkey: String
var body: some View {
let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state)
let followers = FollowersModel(damus_state: damus_state, target: pubkey)
let pv = ProfileView(damus_state: damus_state, profile: pmodel, followers: followers)
NavigationLink(destination: pv) {
NavigationLink(destination: ProfileView(damus_state: damus_state, pubkey: pubkey)) {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
VStack(alignment: .leading) {

View File

@@ -7,6 +7,22 @@
import SwiftUI
enum ZappingEventType {
case failed(ZappingError)
case got_zap_invoice(String)
}
enum ZappingError {
case fetching_invoice
case bad_lnurl
}
struct ZappingEvent {
let is_custom: Bool
let type: ZappingEventType
let event: NostrEvent
}
struct ZapButton: View {
let damus_state: DamusState
let event: NostrEvent
@@ -19,61 +35,8 @@ struct ZapButton: View {
@State var slider_value: Double = 0.0
@State var slider_visible: Bool = false
@State var showing_select_wallet: Bool = false
func send_zap() {
guard let privkey = damus_state.keypair.privkey else {
return
}
// Only take the first 10 because reasons
let relays = Array(damus_state.pool.descriptors.prefix(10))
let target = ZapTarget.note(id: event.id, author: event.pubkey)
// TODO: gather comment?
let content = ""
let zapreq = make_zap_request_event(pubkey: damus_state.pubkey, privkey: privkey, content: content, relays: relays, target: target)
zapping = true
Task {
var mpayreq = damus_state.lnurls.lookup(target.pubkey)
if mpayreq == nil {
mpayreq = await fetch_static_payreq(lnurl)
}
guard let payreq = mpayreq else {
// TODO: show error
DispatchQueue.main.async {
zapping = false
}
return
}
DispatchQueue.main.async {
damus_state.lnurls.endpoints[target.pubkey] = payreq
}
let zap_amount = get_default_zap_amount(pubkey: damus_state.pubkey) ?? 1000
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount) else {
DispatchQueue.main.async {
zapping = false
}
return
}
DispatchQueue.main.async {
zapping = false
if should_show_wallet_selector(damus_state.pubkey) {
self.invoice = inv
self.showing_select_wallet = true
} else {
open_with_wallet(wallet: get_default_wallet(damus_state.pubkey).model, invoice: inv)
}
}
}
//damus_state.pool.send(.event(zapreq))
}
@State var showing_zap_customizer: Bool = false
@State var is_charging: Bool = false
var zap_img: String {
if bar.zapped {
@@ -92,6 +55,10 @@ struct ZapButton: View {
return Color.orange
}
if is_charging {
return Color.yellow
}
if !zapping {
return nil
}
@@ -101,22 +68,66 @@ struct ZapButton: View {
var body: some View {
HStack(spacing: 4) {
EventActionButton(img: zap_img, col: zap_color) {
if bar.zapped {
//notify(.delete, bar.our_tip)
} else if !zapping {
send_zap()
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
}
}
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"))
Text(String("\(bar.zap_total > 0 ? "\(format_msats_abbrev(bar.zap_total))" : "")"))
.font(.footnote)
.foregroundColor(bar.zapped ? Color.orange : Color.gray)
if bar.zap_total > 0 {
Text(verbatim: format_msats_abbrev(bar.zap_total))
.font(.footnote)
.foregroundColor(bar.zapped ? Color.orange : Color.gray)
}
}
.sheet(isPresented: $showing_zap_customizer) {
CustomizeZapView(state: damus_state, event: event, lnurl: lnurl)
}
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: invoice)
}
.onReceive(handle_notify(.zapping)) { notif in
let zap_ev = notif.object as! ZappingEvent
guard zap_ev.event.id == self.event.id else {
return
}
guard !zap_ev.is_custom else {
return
}
switch zap_ev.type {
case .failed:
break
case .got_zap_invoice(let inv):
if should_show_wallet_selector(damus_state.pubkey) {
self.invoice = inv
self.showing_select_wallet = true
} else {
open_with_wallet(wallet: get_default_wallet(damus_state.pubkey).model, invoice: inv)
}
}
self.zapping = false
}
}
}
@@ -128,3 +139,56 @@ 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 keypair = damus_state.keypair.to_full() else {
return
}
// Only take the first 10 because reasons
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(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type)
Task {
var mpayreq = damus_state.lnurls.lookup(target.pubkey)
if mpayreq == nil {
mpayreq = await fetch_static_payreq(lnurl)
}
guard let payreq = mpayreq else {
// TODO: show error
DispatchQueue.main.async {
let typ = ZappingEventType.failed(.bad_lnurl)
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
notify(.zapping, ev)
}
return
}
DispatchQueue.main.async {
damus_state.lnurls.endpoints[target.pubkey] = payreq
}
let zap_amount = amount_sats ?? get_default_zap_amount(pubkey: damus_state.pubkey) ?? 1000
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount, zap_type: zap_type, comment: comment) else {
DispatchQueue.main.async {
let typ = ZappingEventType.failed(.fetching_invoice)
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
notify(.zapping, ev)
}
return
}
DispatchQueue.main.async {
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), event: event)
notify(.zapping, ev)
}
}
return
}

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("", comment: "Toolbar label for unknown views. This label would be displayed only if a new timeline view is added but a toolbar label was not explicitly assigned to it yet.")
}
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()
TimelineView(events: home.notifications, loading: $home.loading, damus: damus, show_friend_icon: true, filter: { _ in true })
}
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) {
@@ -615,7 +625,9 @@ struct ContentView: View {
settings: UserSettingsStore(),
relay_filters: relay_filters,
relay_metadata: metadatas,
drafts: Drafts()
drafts: Drafts(),
events: EventCache(),
bookmarks: BookmarksManager(pubkey: pubkey)
)
home.damus_state = self.damus_state!
@@ -764,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"
}
}

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>

View File

@@ -0,0 +1,71 @@
//
// BookmarksManager.swift
// damus
//
// Created by Joel Klabo on 2/18/23.
//
import Foundation
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
private var _bookmarks: [NostrEvent]
var bookmarks: [NostrEvent] {
get {
return _bookmarks
}
set {
if save_bookmarks(pubkey: pubkey, current_value: _bookmarks, value: newValue) {
self._bookmarks = newValue
self.objectWillChange.send()
}
}
}
init(pubkey: String) {
self._bookmarks = load_bookmarks(pubkey: pubkey)
self.pubkey = pubkey
}
func isBookmarked(_ ev: NostrEvent) -> Bool {
return bookmarks.contains(ev)
}
func updateBookmark(_ ev: NostrEvent) {
if isBookmarked(ev) {
bookmarks = bookmarks.filter { $0 != ev }
} else {
bookmarks.append(ev)
}
}
func clearAll() {
bookmarks = []
}
}

View File

@@ -24,6 +24,8 @@ struct DamusState {
let relay_filters: RelayFilters
let relay_metadata: RelayMetadatas
let drafts: Drafts
let events: EventCache
let bookmarks: BookmarksManager
var pubkey: String {
return keypair.pubkey
@@ -32,9 +34,8 @@ struct DamusState {
var is_privkey_user: Bool {
keypair.privkey != nil
}
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())
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: ""))
}
}

View File

@@ -8,6 +8,6 @@
import Foundation
class Drafts: ObservableObject {
@Published var post: String = ""
@Published var replies: [NostrEvent: String] = [:]
@Published var post: NSMutableAttributedString = NSMutableAttributedString(string: "")
@Published var replies: [NostrEvent: NSMutableAttributedString] = [:]
}

View File

@@ -65,7 +65,7 @@ class EventsModel: ObservableObject {
case .notice(_):
break
case .eose(_):
load_profiles(profiles_subid: profiles_id, relay_id: relay_id, events: events, damus_state: state)
load_profiles(profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state)
}
}
}

View File

@@ -50,22 +50,18 @@ class HomeModel: ObservableObject {
let profiles_subid = UUID().description
@Published var new_events: NewEventsBits = NewEventsBits()
@Published var notifications: EventHolder
@Published var notifications = NotificationsModel()
@Published var dms: DirectMessagesModel
@Published var events: EventHolder
@Published var events = EventHolder()
@Published var loading: Bool = false
@Published var signal: SignalModel = SignalModel()
init() {
self.events = EventHolder()
self.notifications = EventHolder()
self.damus_state = DamusState.empty
self.dms = DirectMessagesModel(our_pubkey: "")
}
init(damus_state: DamusState) {
self.events = EventHolder()
self.notifications = EventHolder()
self.damus_state = damus_state
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
self.setup_debouncer()
@@ -129,36 +125,43 @@ class HomeModel: ObservableObject {
handle_channel_meta(ev)
case .zap:
handle_zap_event(ev)
case .zap_request:
break
}
}
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(ev) {
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
}
@@ -177,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)
}
}
@@ -229,7 +232,7 @@ class HomeModel: ObservableObject {
guard inner_ev.is_valid else {
return
}
if inner_ev.is_textlike {
handle_text_event(sub_id: sub_id, ev)
}
@@ -255,12 +258,11 @@ class HomeModel: ObservableObject {
return
}
// CHECK SIGS ON THESE
switch damus_state.likes.add_event(ev, target: e.ref_id) {
case .already_counted:
break
case .success(let n):
handle_notification(ev: ev)
let liked = Counted(event: ev, id: e.ref_id, total: n)
notify(.liked, liked)
notify(.update_stats, e.ref_id)
@@ -320,9 +322,9 @@ class HomeModel: ObservableObject {
if sub_id == dms_subid {
var dms = dms.dms.flatMap { $0.1.events }
dms.append(contentsOf: incoming_dms)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: dms, damus_state: damus_state)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(dms), damus_state: damus_state)
} else if sub_id == notifications_subid {
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: notifications.all_events, damus_state: damus_state)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_keys(notifications.uniq_pubkeys()), damus_state: damus_state)
}
self.loading = false
@@ -375,7 +377,6 @@ class HomeModel: ObservableObject {
// TODO: separate likes?
var home_filter = NostrFilter.filter_kinds([
NostrKind.text.rawValue,
NostrKind.chat.rawValue,
NostrKind.like.rawValue,
NostrKind.boost.rawValue,
])
@@ -385,13 +386,12 @@ class HomeModel: ObservableObject {
var notifications_filter = NostrFilter.filter_kinds([
NostrKind.text.rawValue,
NostrKind.chat.rawValue,
NostrKind.like.rawValue,
NostrKind.boost.rawValue,
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]
@@ -457,20 +457,38 @@ 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
}
if !notifications.insert(ev) {
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)
}
if !notifications.insert_event(ev) {
return
}
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
}
}
@@ -484,6 +502,8 @@ class HomeModel: ObservableObject {
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
return
}
damus_state.events.insert(ev)
if sub_id == home_subid {
insert_home_event(ev)
@@ -895,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()
}

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)
}
}
}

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
@@ -263,17 +271,19 @@ func format_msats_abbrev(_ msats: Int64) -> String {
return formatter.string(from: sats) ?? sats.stringValue
}
func format_msats(_ msat: Int64) -> String {
func format_msats(_ msat: Int64, locale: Locale = Locale.current) -> String {
let numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
numberFormatter.minimumFractionDigits = 0
numberFormatter.maximumFractionDigits = 3
numberFormatter.roundingMode = .down
numberFormatter.locale = locale
let sats = NSNumber(value: (Double(msat) / 1000.0))
let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue
return String(format: Bundle.main.localizedString(forKey: "sats_count", value: nil, table: nil), 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? {

View File

@@ -0,0 +1,32 @@
//
// ReactionGroup.swift
// damus
//
// Created by William Casarin on 2023-02-21.
//
import Foundation
class EventGroup {
var events: [NostrEvent]
var last_event_at: Int64 {
guard let first = self.events.first else {
return 0
}
return first.created_at
}
init() {
self.events = []
}
init(events: [NostrEvent]) {
self.events = events
}
func insert(_ ev: NostrEvent) -> Bool {
return insert_uniq_sorted_event_created(events: &events, new_ev: ev)
}
}

View File

@@ -0,0 +1,59 @@
//
// ZapGroup.swift
// damus
//
// Created by William Casarin on 2023-02-21.
//
import Foundation
class ZapGroup {
var zaps: [Zap]
var msat_total: Int64
var zappers: Set<String>
var last_event_at: Int64 {
guard let first = zaps.first else {
return 0
}
return first.event.created_at
}
func zap_requests() -> [NostrEvent] {
zaps.map { z in
if let priv = z.private_request {
return priv
} else {
return z.request.ev
}
}
}
init(zaps: [Zap]) {
self.zaps = zaps
self.msat_total = 0
self.zappers = Set()
}
init() {
self.zaps = []
self.msat_total = 0
self.zappers = Set()
}
func insert(_ zap: Zap) -> Bool {
if !insert_uniq_sorted_zap_by_created(zaps: &zaps, new_zap: zap) {
return false
}
msat_total += zap.invoice.amount
if !zappers.contains(zap.request.ev.pubkey) {
zappers.insert(zap.request.ev.pubkey)
}
return true
}
}

View File

@@ -0,0 +1,320 @@
//
// NotificationsModel.swift
// damus
//
// Created by William Casarin on 2023-02-21.
//
import Foundation
enum NotificationItem {
case repost(String, EventGroup)
case reaction(String, EventGroup)
case profile_zap(ZapGroup)
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, _):
return "repost_" + evid
case .reaction(let evid, _):
return "reaction_" + evid
case .profile_zap:
return "profile_zap"
case .event_zap(let evid, _):
return "event_zap_" + evid
case .reply(let ev):
return "reply_" + ev.id
}
}
var last_event_at: Int64 {
switch self {
case .reaction(_, let evgrp):
return evgrp.last_event_at
case .repost(_, let evgrp):
return evgrp.last_event_at
case .profile_zap(let zapgrp):
return zapgrp.last_event_at
case .event_zap(_, let zapgrp):
return zapgrp.last_event_at
case .reply(let reply):
return reply.created_at
}
}
}
class NotificationsModel: ObservableObject, ScrollQueue {
var incoming_zaps: [Zap]
var incoming_events: [NostrEvent]
var should_queue: Bool
// mappings from events to
var zaps: [String: ZapGroup]
var profile_zaps: ZapGroup
var reactions: [String: EventGroup]
var reposts: [String: EventGroup]
var replies: [NostrEvent]
var has_reply: Set<String>
@Published var notifications: [NotificationItem]
init() {
self.zaps = [:]
self.reactions = [:]
self.reposts = [:]
self.replies = []
self.has_reply = Set()
self.should_queue = true
self.incoming_zaps = []
self.incoming_events = []
self.profile_zaps = ZapGroup()
self.notifications = []
}
func set_should_queue(_ val: Bool) {
self.should_queue = val
}
func uniq_pubkeys() -> [String] {
var pks = Set<String>()
for ev in incoming_events {
pks.insert(ev.pubkey)
}
for grp in reposts {
for ev in grp.value.events {
pks.insert(ev.pubkey)
}
}
for ev in replies {
pks.insert(ev.pubkey)
}
for zap in incoming_zaps {
pks.insert(zap.request.ev.pubkey)
}
return Array(pks)
}
func build_notifications() -> [NotificationItem] {
var notifs: [NotificationItem] = []
for el in zaps {
let evid = el.key
let zapgrp = el.value
let notif: NotificationItem = .event_zap(evid, zapgrp)
notifs.append(notif)
}
if !profile_zaps.zaps.isEmpty {
notifs.append(.profile_zap(profile_zaps))
}
for el in reposts {
let evid = el.key
let evgrp = el.value
notifs.append(.repost(evid, evgrp))
}
for el in reactions {
let evid = el.key
let evgrp = el.value
notifs.append(.reaction(evid, evgrp))
}
for reply in replies {
notifs.append(.reply(reply))
}
notifs.sort { $0.last_event_at > $1.last_event_at }
return notifs
}
private func insert_repost(_ ev: NostrEvent) -> Bool {
guard let reposted_ev = ev.inner_event else {
return false
}
let id = reposted_ev.id
if let evgrp = self.reposts[id] {
return evgrp.insert(ev)
} else {
let evgrp = EventGroup()
self.reposts[id] = evgrp
return evgrp.insert(ev)
}
}
private func insert_text(_ ev: NostrEvent) -> Bool {
guard !has_reply.contains(ev.id) else {
return false
}
has_reply.insert(ev.id)
replies.append(ev)
return true
}
private func insert_reaction(_ ev: NostrEvent) -> Bool {
guard let ref_id = ev.referenced_ids.last else {
return false
}
let id = ref_id.id
if let evgrp = self.reactions[id] {
return evgrp.insert(ev)
} else {
let evgrp = EventGroup()
self.reactions[id] = evgrp
return evgrp.insert(ev)
}
}
private func insert_event_immediate(_ ev: NostrEvent) -> Bool {
if ev.known_kind == .boost {
return insert_repost(ev)
} else if ev.known_kind == .like {
return insert_reaction(ev)
} else if ev.known_kind == .text {
return insert_text(ev)
}
return false
}
private func insert_zap_immediate(_ zap: Zap) -> Bool {
switch zap.target {
case .note(let notezt):
let id = notezt.note_id
if let zapgrp = self.zaps[notezt.note_id] {
return zapgrp.insert(zap)
} else {
let zapgrp = ZapGroup()
self.zaps[id] = zapgrp
return zapgrp.insert(zap)
}
case .profile:
return profile_zaps.insert(zap)
}
}
func insert_event(_ ev: NostrEvent) -> Bool {
if should_queue {
return insert_uniq_sorted_event_created(events: &incoming_events, new_ev: ev)
}
if insert_event_immediate(ev) {
self.notifications = build_notifications()
return true
}
return false
}
func insert_zap(_ zap: Zap) -> Bool {
if should_queue {
return insert_uniq_sorted_zap_by_created(zaps: &incoming_zaps, new_zap: zap)
}
if insert_zap_immediate(zap) {
self.notifications = build_notifications()
return true
}
return false
}
func filter(_ isIncluded: (NostrEvent) -> Bool) {
var changed = false
var count = 0
count = incoming_events.count
incoming_events = incoming_events.filter(isIncluded)
changed = changed || incoming_events.count != count
count = profile_zaps.zaps.count
profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request.ev) }
changed = changed || profile_zaps.zaps.count != count
for el in reactions {
count = el.value.events.count
el.value.events = el.value.events.filter(isIncluded)
changed = changed || el.value.events.count != count
}
for el in reposts {
count = el.value.events.count
el.value.events = el.value.events.filter(isIncluded)
changed = changed || el.value.events.count != count
}
for el in zaps {
count = el.value.zaps.count
el.value.zaps = el.value.zaps.filter {
isIncluded($0.request.ev)
}
changed = changed || el.value.zaps.count != count
}
count = replies.count
replies = replies.filter(isIncluded)
changed = changed || replies.count != count
if changed {
self.notifications = build_notifications()
}
}
func flush() -> Bool {
var inserted = false
for zap in incoming_zaps {
inserted = insert_zap_immediate(zap) || inserted
}
for event in incoming_events {
inserted = insert_event_immediate(event) || inserted
}
if inserted {
self.notifications = build_notifications()
}
return inserted
}
}

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
}
}

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
}
}

View File

@@ -0,0 +1,12 @@
//
// SearchResultsModel.swift
// damus
//
// Created by William Casarin on 2023-03-03.
//
import Foundation
class SearchResultsModel: ObservableObject {
}

View File

@@ -76,7 +76,7 @@ class SearchHomeModel: ObservableObject {
// global events are not realtime
unsubscribe(to: relay_id)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: events.all_events, damus_state: damus_state)
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.all_events), damus_state: damus_state)
}
@@ -98,8 +98,31 @@ func find_profiles_to_fetch_pk(profiles: Profiles, event_pubkeys: [String]) -> [
return Array(pubkeys)
}
func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad) -> [String] {
switch load {
case .from_events(let events):
return find_profiles_to_fetch_from_events(profiles: profiles, events: events)
case .from_keys(let pks):
return find_profiles_to_fetch_from_keys(profiles: profiles, pks: pks)
}
}
func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [String]) -> [String] {
var pubkeys = Set<String>()
func find_profiles_to_fetch(profiles: Profiles, events: [NostrEvent]) -> [String] {
for pk in pks {
if profiles.lookup(id: pk) != nil {
continue
}
pubkeys.insert(pk)
}
return Array(pubkeys)
}
func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent]) -> [String] {
var pubkeys = Set<String>()
for ev in events {
@@ -113,9 +136,14 @@ func find_profiles_to_fetch(profiles: Profiles, events: [NostrEvent]) -> [String
return Array(pubkeys)
}
func load_profiles(profiles_subid: String, relay_id: String, events: [NostrEvent], damus_state: DamusState) {
enum PubkeysToLoad {
case from_events([NostrEvent])
case from_keys([String])
}
func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad, damus_state: DamusState) {
var filter = NostrFilter.filter_profiles
let authors = find_profiles_to_fetch(profiles: damus_state.profiles, events: events)
let authors = find_profiles_to_fetch(profiles: damus_state.profiles, load: load)
filter.authors = authors
guard !authors.isEmpty else {

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, 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)
}
}

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:

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,30 @@ 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 auto_translate: Bool {
didSet {
UserDefaults.standard.set(auto_translate, forKey: "auto_translate")
}
}
@Published var show_only_preferred_languages: Bool {
didSet {
UserDefaults.standard.set(show_only_preferred_languages, forKey: "show_only_preferred_languages")
}
}
@Published var translation_service: TranslationService {
didSet {
@@ -159,14 +203,27 @@ 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()
auto_translate = UserDefaults.standard.object(forKey: "auto_translate") as? Bool ?? false
show_only_preferred_languages = UserDefaults.standard.object(forKey: "show_only_preferred_languages") as? Bool ?? false
// Note from @tyiu:
// Default translation service is disabled by default for now until we gain some confidence that it is working well in production.

View File

@@ -13,6 +13,7 @@ class ZapsModel: ObservableObject {
var zaps: [Zap]
let zaps_subid = UUID().description
let profiles_subid = UUID().description
init(state: DamusState, target: ZapTarget) {
self.state = state
@@ -44,34 +45,39 @@ class ZapsModel: ObservableObject {
return
}
guard case .event(_, let ev) = resp else {
return
}
guard ev.kind == 9735 else {
return
}
if let zap = state.zaps.zaps[ev.id] {
if insert_uniq_sorted_zap(zaps: &zaps, new_zap: zap) {
objectWillChange.send()
}
} else {
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
switch resp {
case .notice:
break
case .eose:
let events = self.zaps.map { $0.request.ev }
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state)
case .event(_, let ev):
guard ev.kind == 9735 else {
return
}
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper) else {
return
}
state.zaps.add_zap(zap: zap)
if insert_uniq_sorted_zap(zaps: &zaps, new_zap: zap) {
objectWillChange.send()
if let zap = state.zaps.zaps[ev.id] {
if insert_uniq_sorted_zap_by_amount(zaps: &zaps, new_zap: zap) {
objectWillChange.send()
}
} else {
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
return
}
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: state.keypair.privkey) else {
return
}
state.zaps.add_zap(zap: zap)
if insert_uniq_sorted_zap_by_amount(zaps: &zaps, new_zap: zap) {
objectWillChange.send()
}
}
}
}
}

View File

@@ -140,9 +140,8 @@ struct Profile: Codable {
try container.encode(value)
}
static func displayName(profile: Profile?, pubkey: String) -> String {
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)
}
}

View File

@@ -10,6 +10,7 @@ import CommonCrypto
import secp256k1
import secp256k1_implementation
import CryptoKit
import NaturalLanguage
@@ -157,7 +158,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
@@ -168,6 +169,9 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
return decrypted(privkey: privkey) ?? "*failed to decrypt content*"
}
return content
/*
switch validity {
case .ok:
return content
@@ -176,6 +180,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
case .bad_sig:
return content + "\n\n*WARNING: invalid signature, could be forged!*"
}
*/
}
var description: String {
@@ -211,6 +216,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
@@ -245,6 +260,25 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
return event_is_reply(self, privkey: privkey)
}
func note_language(_ privkey: String?) -> String? {
// Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in
// and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer.
let originalBlocks = blocks(privkey)
let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ")
// Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate.
let languageRecognizer = NLLanguageRecognizer()
languageRecognizer.processString(originalOnlyText)
guard let locale = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= 0.5 })?.key.rawValue else {
return nil
}
// Remove the variant component and just take the language part as translation services typically only supports the variant-less language.
// Moreover, speakers of one variant can generally understand other variants.
return localeToLanguage(locale)
}
public var referenced_ids: [ReferencedId] {
return get_referenced_ids(key: "e")
}
@@ -573,14 +607,115 @@ func zap_target_to_tags(_ target: ZapTarget) -> [[String]] {
}
}
func make_zap_request_event(pubkey: String, privkey: String, content: String, relays: [RelayDescriptor], target: ZapTarget) -> 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)
let ev = NostrEvent(content: content, pubkey: pubkey, kind: 9734, tags: tags)
var kp = keypair
let now = Int64(Date().timeIntervalSince1970)
var message = content
switch zap_type {
case .pub:
break
case .non_zap:
break
case .anon:
tags.append(["anon"])
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: message, pubkey: kp.pubkey, kind: 9734, tags: tags, createdAt: now)
ev.id = calculate_event_id(ev: ev)
ev.sig = sign_event(privkey: privkey, ev: ev)
ev.sig = sign_event(privkey: kp.privkey, ev: ev)
return ev
}
@@ -610,14 +745,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 {
@@ -626,6 +761,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 {
@@ -671,6 +813,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)
@@ -835,7 +1010,7 @@ func first_eref_mention(ev: NostrEvent, privkey: String?) -> Mention? {
extension [ReferencedId] {
var pRefs: [ReferencedId] {
get {
self.filter { ref in
Set(self).filter { ref in
ref.key == "p"
}
}

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])
}

View File

@@ -21,4 +21,5 @@ enum NostrKind: Int {
case chat = 42
case list = 30000
case zap = 9735
case zap_request = 9734
}

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) {

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)
}

View File

@@ -0,0 +1,92 @@
//
// EventCache.swift
// damus
//
// Created by William Casarin on 2023-02-21.
//
import Combine
import Foundation
import UIKit
class EventCache {
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]
}
func insert(_ ev: NostrEvent) {
guard events[ev.id] == nil else {
return
}
events[ev.id] = ev
}
private func prune() {
events = [:]
replies.replies = [:]
}
}

View File

@@ -8,11 +8,15 @@
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]
@Published var should_queue: Bool
var should_queue: Bool
func set_should_queue(_ val: Bool) {
self.should_queue = val
}
var queued: Int {
return incoming.count

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)
}
}

View File

@@ -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
}

View File

@@ -38,8 +38,7 @@ func insert_uniq_by_pubkey(events: inout [NostrEvent], new_ev: NostrEvent, cmp:
return true
}
@discardableResult
func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool {
func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap, cmp: (Zap, Zap) -> Bool) -> Bool {
var i: Int = 0
for zap in zaps {
@@ -48,7 +47,7 @@ func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool {
return false
}
if new_zap.invoice.amount > zap.invoice.amount {
if cmp(new_zap, zap) {
zaps.insert(new_zap, at: i)
return true
}
@@ -59,6 +58,19 @@ func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool {
return true
}
@discardableResult
func insert_uniq_sorted_zap_by_created(zaps: inout [Zap], new_zap: Zap) -> Bool {
return insert_uniq_sorted_zap(zaps: &zaps, new_zap: new_zap) { (a, b) in
a.event.created_at > b.event.created_at
}
}
@discardableResult
func insert_uniq_sorted_zap_by_amount(zaps: inout [Zap], new_zap: Zap) -> Bool {
return insert_uniq_sorted_zap(zaps: &zaps, new_zap: new_zap) { (a, b) in
a.invoice.amount > b.invoice.amount
}
}
func insert_uniq_sorted_event_created(events: inout [NostrEvent], new_ev: NostrEvent) -> Bool {
return insert_uniq_sorted_event(events: &events, new_ev: new_ev) {

View File

@@ -9,8 +9,10 @@ import Foundation
struct LNUrlPayRequest: Decodable {
let allowsNostr: Bool?
let commentAllowed: Int?
let nostrPubkey: String?
let metadata: String?
let minSendable: Int64?
let maxSendable: Int64?
let status: String?

View File

@@ -10,10 +10,11 @@ import LinkPresentation
class CustomLinkView: LPLinkView {
override var intrinsicContentSize: CGSize { CGSize(width: 0, height: super.intrinsicContentSize.height) }
}
enum Metadata {
case linkmeta(LPLinkMetadata)
case linkmeta(CachedMetadata)
case url(URL)
}
@@ -26,12 +27,19 @@ struct LinkViewRepresentable: UIViewRepresentable {
func makeUIView(context: Context) -> CustomLinkView {
switch meta {
case .linkmeta(let linkmeta):
return CustomLinkView(metadata: linkmeta)
return CustomLinkView(metadata: linkmeta.meta)
case .url(let url):
return CustomLinkView(url: url)
}
}
func updateUIView(_ uiView: CustomLinkView, context: Context) {
switch meta {
case .linkmeta(let cached):
cached.intrinsic_height = uiView.intrinsicContentSize.height
case .url:
return
}
}
}

View File

@@ -0,0 +1,34 @@
//
// LocalizationUtil.swift
// damus
//
// Created by Terry Yiu on 2/24/23.
//
import Foundation
func bundleForLocale(locale: Locale?) -> Bundle {
if locale == nil {
return Bundle.main
}
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)
}
/**
Removes the variant part of a locale code so that it contains only the language code.
*/
func localeToLanguage(_ locale: String) -> String? {
if #available(iOS 16, *) {
return Locale.LanguageCode(stringLiteral: locale).identifier(.alpha2)
} else {
return NSLocale(localeIdentifier: locale).languageCode
}
}

View File

@@ -101,6 +101,9 @@ extension Notification.Name {
static var update_stats: Notification.Name {
return Notification.Name("update_stats")
}
static var zapping: Notification.Name {
return Notification.Name("zapping")
}
}
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {

8
damus/Util/PostBox.swift Normal file
View File

@@ -0,0 +1,8 @@
//
// PostBox.swift
// damus
//
// Created by William Casarin on 2023-03-20.
//
import Foundation

View File

@@ -8,8 +8,18 @@
import Foundation
import LinkPresentation
class CachedMetadata {
let meta: LPLinkMetadata
var intrinsic_height: CGFloat?
init(meta: LPLinkMetadata) {
self.meta = meta
self.intrinsic_height = nil
}
}
enum Preview {
case value(LinkViewRepresentable)
case value(CachedMetadata)
case failed
}
@@ -20,12 +30,12 @@ class PreviewCache {
return previews[evid]
}
func store(evid: String, preview: LinkViewRepresentable?) {
func store(evid: String, preview: LPLinkMetadata?) {
switch preview {
case .none:
previews[evid] = .failed
case .some(let meta):
previews[evid] = .value(meta)
previews[evid] = .value(CachedMetadata(meta: meta))
}
}

View File

@@ -50,5 +50,6 @@ public func time_ago_since(_ date: Date, _ calendar: Calendar = Calendar.current
return formatter.string(from: DateComponents(calendar: calendar, second: second))!
}
return NSLocalizedString("now", comment: "String indicating that a given timestamp just occurred")
let bundle = bundleForLocale(locale: calendar.locale ?? Locale.current)
return NSLocalizedString("now", bundle: bundle, comment: "String indicating that a given timestamp just occurred")
}

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) 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,11 +303,15 @@ func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int)
var query = [URLQueryItem(name: "amount", value: "\(amount)")]
if zappable {
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 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))
}
base_url.queryItems = query
@@ -310,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
}

View File

@@ -36,7 +36,7 @@ class Zaps {
if our_zaps[note_target.note_id] == nil {
our_zaps[note_target.note_id] = [zap]
} else {
insert_uniq_sorted_zap(zaps: &(our_zaps[note_target.note_id]!), new_zap: zap)
insert_uniq_sorted_zap_by_amount(zaps: &(our_zaps[note_target.note_id]!), new_zap: zap)
}
case .profile(_):
break

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
}
}
}
}

View File

@@ -26,14 +26,16 @@ struct EventDetailBar: View {
HStack {
if bar.boosts > 0 {
NavigationLink(destination: RepostsView(damus_state: state, model: RepostsModel(state: state, target: target))) {
Text("\(Text(verbatim: "\(bar.boosts)").font(.body.bold())) \(Text(String(format: Bundle.main.localizedString(forKey: "reposts_count", value: nil, table: nil), bar.boosts)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.")
let noun = Text(verbatim: "\(repostsCountString(bar.boosts))").foregroundColor(.gray)
Text("\(Text("\(bar.boosts)").font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.")
}
.buttonStyle(PlainButtonStyle())
}
if bar.likes > 0 {
NavigationLink(destination: ReactionsView(damus_state: state, model: ReactionsModel(state: state, target: target))) {
Text("\(Text(verbatim: "\(bar.likes)").font(.body.bold())) \(Text(String(format: Bundle.main.localizedString(forKey: "reactions_count", value: nil, table: nil), bar.likes)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.")
let noun = Text(verbatim: "\(reactionsCountString(bar.likes))").foregroundColor(.gray)
Text("\(Text("\(bar.likes)").font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.")
}
.buttonStyle(PlainButtonStyle())
}
@@ -41,7 +43,8 @@ struct EventDetailBar: View {
if bar.zaps > 0 {
let dst = ZapsView(state: state, target: .note(id: target, author: target_pk))
NavigationLink(destination: dst) {
Text("\(Text(verbatim: "\(bar.zaps)").font(.body.bold())) \(Text(String(format: Bundle.main.localizedString(forKey: "zaps_count", value: nil, table: nil), bar.zaps)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.")
let noun = Text(verbatim: "\(zapsCountString(bar.zaps))").foregroundColor(.gray)
Text("\(Text("\(bar.zaps)").font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.")
}
.buttonStyle(PlainButtonStyle())
}
@@ -49,6 +52,21 @@ struct EventDetailBar: View {
}
}
func repostsCountString(_ count: Int, locale: Locale = Locale.current) -> String {
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 format = localizedStringFormat(key: "reactions_count", locale: locale)
return String(format: format, locale: locale, count)
}
func zapsCountString(_ count: Int, locale: Locale = Locale.current) -> String {
let format = localizedStringFormat(key: "zaps_count", locale: locale)
return String(format: format, locale: locale, count)
}
struct EventDetailBar_Previews: PreviewProvider {
static var previews: some View {
EventDetailBar(state: test_damus_state(), target: "", target_pk: "")

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)
}
}
}

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
}
}
}

View File

@@ -0,0 +1,61 @@
//
// BookmarksView.swift
// damus
//
// Created by Joel Klabo on 2/18/23.
//
import SwiftUI
struct BookmarksView: View {
let state: DamusState
private let noneFilter: (NostrEvent) -> Bool = { _ in true }
private let bookmarksTitle = NSLocalizedString("Bookmarks", comment: "Title of bookmarks view")
@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 bookmarks.isEmpty {
VStack {
Image(systemName: "bookmark")
.resizable()
.scaledToFit()
.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"))
}
} else {
ScrollView {
InnerTimelineView(events: EventHolder(events: bookmarks, incoming: []), damus: state, show_friend_icon: true, filter: noneFilter)
}
}
}
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(bookmarksTitle)
.toolbar {
if !bookmarks.isEmpty {
Button(NSLocalizedString("Clear All", comment: "Button for clearing bookmarks data.")) {
manager.clearAll()
}
}
}
}
}
/*
struct BookmarksView_Previews: PreviewProvider {
static var previews: some View {
BookmarksView()
}
}
*/

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
}
}
}

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))
}
}
*/

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()
}
}
}
}
@@ -129,30 +141,21 @@ struct ConfigView: View {
}
}
Section(NSLocalizedString("Default Zap Amount in sats", comment: "Section title for zap configuration")) {
TextField(String("1000"), text: $default_zap_amount)
.keyboardType(.numberPad)
.onReceive(Just(default_zap_amount)) { newValue in
let filtered = newValue.filter { Set("0123456789").contains($0) }
if filtered != newValue {
default_zap_amount = filtered
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)
}
if filtered == "" {
set_default_zap_amount(pubkey: state.pubkey, amount: 1000)
return
}
guard let amt = Int(filtered) else {
return
}
set_default_zap_amount(pubkey: state.pubkey, amount: amt)
}
}
Section(NSLocalizedString("Translations", comment: "Section title for selecting the translation service.")) {
Toggle(NSLocalizedString("Show only preferred languages on Universe feed", comment: "Toggle to show notes that are only in the device's preferred languages on the Universe feed and hide notes that are in other languages."), isOn: $settings.show_only_preferred_languages)
.toggleStyle(.switch)
Picker(NSLocalizedString("Service", comment: "Prompt selection of translation service provider."), selection: $settings.translation_service) {
ForEach(TranslationService.allCases, id: \.self) { server in
Text(server.model.displayName)
@@ -197,38 +200,82 @@ struct ConfigView: View {
Link(NSLocalizedString("Get API Key", comment: "Button to navigate to DeepL website to get a translation API key."), destination: URL(string: "https://www.deepl.com/pro-api")!)
}
}
}
Section(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen")) {
Toggle(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen"), isOn: $settings.left_handed)
.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()
if settings.translation_service != .none {
Toggle(NSLocalizedString("Automatically translate notes", comment: "Toggle to automatically translate notes."), isOn: $settings.auto_translate)
.toggleStyle(.switch)
}
}
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("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
}
}
}
let bundleShortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String
let bundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] as! String
Section(NSLocalizedString("Version", comment: "Section title for displaying the version number of the Damus app.")) {
Text(verbatim: "\(bundleShortVersion) (\(bundleVersion))")
if let bundleShortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"], let bundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] {
Section(NSLocalizedString("Version", comment: "Section title for displaying the version number of the Damus app.")) {
Text(verbatim: "\(bundleShortVersion) (\(bundleVersion))")
}
}
}
}
.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) {
@@ -346,3 +393,25 @@ struct ConfigView_Previews: PreviewProvider {
}
}
}
func handle_string_amount(new_value: String) -> Int? {
let digits = Set("0123456789")
let filtered = new_value.filter { digits.contains($0) }
if filtered == "" {
return nil
}
guard let amt = Int(filtered) else {
return nil
}
return amt
}
func clear_kingfisher_cache() -> Void {
KingfisherManager.shared.cache.clearMemoryCache()
KingfisherManager.shared.cache.clearDiskCache()
KingfisherManager.shared.cache.cleanExpiredDiskCache()
}

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)
}
@@ -37,9 +37,7 @@ struct DMChatView: View {
var Header: some View {
let profile = damus_state.profiles.lookup(id: pubkey)
let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state)
let fmodel = FollowersModel(damus_state: damus_state, target: pubkey)
let profile_page = ProfileView(damus_state: damus_state, profile: pmodel, followers: fmodel)
let profile_page = ProfileView(damus_state: damus_state, pubkey: pubkey)
return NavigationLink(destination: profile_page) {
HStack {
ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles)
@@ -183,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
@@ -198,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)

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)
}

View File

@@ -41,6 +41,9 @@ struct DirectMessagesView: View {
ForEach(dms, id: \.0) { tup in
MaybeEvent(tup)
.padding(.top, 10)
Divider()
.padding([.top], 10)
}
}
}

View File

@@ -65,6 +65,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 +105,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)
@@ -201,12 +207,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)
}
}

View File

@@ -16,14 +16,14 @@ 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)
}
}
struct EventDetailView_Previews: PreviewProvider {
static var previews: some View {
let state = test_damus_state()
let _ = test_damus_state()
EventDetailView()
}
}

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,19 +60,7 @@ struct EventView: View {
VStack {
if event.known_kind == .boost {
if let inner_ev = event.inner_event {
VStack(alignment: .leading) {
let prof_model = ProfileModel(pubkey: event.pubkey, damus: damus)
let follow_model = FollowersModel(damus_state: damus, target: event.pubkey)
let prof = damus.profiles.lookup(id: event.pubkey)
let booster_profile = ProfileView(damus_state: damus, profile: prof_model, followers: follow_model)
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()
}
@@ -83,18 +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)
}
Divider()
.padding([.top], 4)
}
}
}
// 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
}
@@ -131,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)
}
}
@@ -181,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()
}

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)
@@ -71,9 +86,14 @@ struct BuilderEventView: View {
}
}
.frame(minWidth: 0, maxWidth: .infinity)
.cornerRadius(8)
.border(Color.gray.opacity(0.2), width: 1)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray.opacity(0.2), lineWidth: 1.0)
)
.onAppear {
guard event == nil else {
return
}
self.load()
}
}

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)
}
}

View File

@@ -11,25 +11,29 @@ struct EventBody: View {
let damus_state: DamusState
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, options: EventViewOptions) {
self.damus_state = damus_state
self.event = event
self.size = size
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 {
event.get_content(damus_state.keypair.privkey)
}
var body: some View {
if event_is_reply(event, privkey: damus_state.keypair.privkey) {
ReplyDescription(event: event, profiles: damus_state.profiles)
}
let should_show_img = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey, booster_pubkey: nil)
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: [])
}
}

View File

@@ -11,52 +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 {
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")
}
HStack {
Menu {
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")
}
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)
MenuItems(event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks)
} label: {
Label(NSLocalizedString("Report", comment: "Context menu option for reporting content."), systemImage: "exclamationmark.bubble")
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
init(event: NostrEvent, keypair: Keypair, target_pubkey: String, bookmarks: BookmarksManager) {
let bookmarked = bookmarks.isBookmarked(event)
self._isBookmarked = State(initialValue: bookmarked)
self.bookmarks = bookmarks
self.event = event
self.keypair = keypair
self.target_pubkey = target_pubkey
}
var body: some View {
Group {
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(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")
}
}
}
}

View File

@@ -31,10 +31,7 @@ struct EventProfile: View {
var body: some View {
HStack(alignment: .center) {
VStack {
let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state)
let pv = ProfileView(damus_state: damus_state, profile: pmodel, followers: FollowersModel(damus_state: damus_state, target: pubkey))
NavigationLink(destination: pv) {
NavigationLink(destination: ProfileView(damus_state: damus_state, pubkey: pubkey)) {
ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles)
}
}

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)
}
}

View File

@@ -26,34 +26,34 @@ struct ReplyDescription_Previews: PreviewProvider {
}
}
func reply_desc(profiles: Profiles, event: NostrEvent) -> String {
func reply_desc(profiles: Profiles, event: NostrEvent, locale: Locale = Locale.current) -> String {
let desc = make_reply_description(event.tags)
let pubkeys = desc.pubkeys
let n = desc.others
let bundle = bundleForLocale(locale: locale)
if desc.pubkeys.count == 0 {
return NSLocalizedString("Replying to self", comment: "Label to indicate that the user is replying to themself.")
return NSLocalizedString("Replying to self", bundle: bundle, comment: "Label to indicate that the user is replying to themself.")
}
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]
let othersCount = n - pubkeys.count
if names.count > 1 {
if uniqueNames.count > 1 {
let othersCount = n - pubkeys.count
if othersCount == 0 {
return String(format: "Replying to %@ & %@", 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: "Replying to %@, %@ & %d others", names[0], names[1], othersCount)
return String(format: localizedStringFormat(key: "replying_to_two_and_others", locale: locale), locale: locale, othersCount, uniqueNames[0], uniqueNames[1])
}
}
if othersCount == 0 {
return String(format: "Replying to %@", names[0])
} else {
return String(format: "Replying to %@ & %d others", names[0], othersCount)
}
return String(format: NSLocalizedString("Replying to %@", bundle: bundle, comment: "Label to indicate that the user is replying to 1 user."), locale: locale, uniqueNames[0])
}

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()
}
}

View File

@@ -7,64 +7,188 @@
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)
VStack {
let pmodel = ProfileModel(pubkey: pubkey, damus: damus)
let pv = ProfileView(damus_state: damus, profile: pmodel, followers: FollowersModel(damus_state: damus, target: pubkey))
NavigationLink(destination: pv) {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus.profiles)
}
Spacer()
Group {
if options.contains(.wide) {
WideStyle
} else {
ThreadedStyle
}
VStack(alignment: .leading) {
HStack(alignment: .center) {
EventProfileName(pubkey: pubkey, 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)
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)
}
}
}
func event_has_tag(ev: NostrEvent, tag: String) -> Bool {
for t in ev.tags {
if t.count >= 1 && t[0] == tag {
return true
}
}
return false
}
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 {
TextEvent(damus: test_damus_state(), event: test_event, pubkey: "pk", has_action_bar: true, booster_pubkey: nil)
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)
}
}
}

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)
}
}

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)
}
}
}
*/

View File

@@ -19,7 +19,7 @@ struct FollowButtonView: View {
Button {
follow_state = perform_follow_btn_action(follow_state, target: target)
} label: {
Text(follow_btn_txt(follow_state, follows_you: follows_you))
Text(verbatim: "\(follow_btn_txt(follow_state, follows_you: follows_you))")
.frame(width: 105, height: 30)
//.padding(.vertical, 10)
.font(.caption.weight(.bold))

View File

@@ -29,7 +29,6 @@ struct FollowersView: View {
@EnvironmentObject var followers: FollowersModel
var body: some View {
let profile = damus_state.profiles.lookup(id: whos)
ScrollView {
LazyVStack(alignment: .leading) {
ForEach(followers.contacts ?? [], id: \.self) { pk in
@@ -38,7 +37,7 @@ struct FollowersView: View {
}
.padding()
}
.navigationBarTitle(NSLocalizedString("\(Profile.displayName(profile: profile, pubkey: whos))'s Followers", comment: "Navigation bar title for view that shows who is following a user."))
.navigationBarTitle(NSLocalizedString("Followers", comment: "Navigation bar title for view that shows who is following a user."))
.onAppear {
followers.subscribe()
}
@@ -56,8 +55,6 @@ struct FollowingView: View {
let whos: String
var body: some View {
let profile = damus_state.profiles.lookup(id: whos)
let who = Profile.displayName(profile: profile, pubkey: whos)
ScrollView {
LazyVStack(alignment: .leading) {
ForEach(following.contacts, id: \.self) { pk in
@@ -72,7 +69,7 @@ struct FollowingView: View {
.onDisappear {
following.unsubscribe()
}
.navigationBarTitle(NSLocalizedString("\(who) following", comment: "Navigation bar title for view that shows who a user is following."))
.navigationBarTitle(NSLocalizedString("Following", comment: "Navigation bar title for view that shows who a user is following."))
}
}

View File

@@ -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)
}
}

View File

@@ -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")
}
}
}
}

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")])
}
}

View File

@@ -27,20 +27,81 @@ struct NoteContentView: View {
let event: NostrEvent
let show_images: Bool
let size: EventViewKind
let preview_height: CGFloat?
let options: EventViewOptions
@State var artifacts: NoteArtifacts
@State var preview: LinkViewRepresentable? = nil
@State var preview: LinkViewRepresentable?
func MainContent() -> some View {
return VStack(alignment: .leading) {
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))
}
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 translateView: some View {
TranslateView(damus_state: damus_state, event: event)
}
var previewView: some View {
Group {
if let preview = self.preview, show_images {
if let preview_height {
preview
.frame(height: preview_height)
} else {
preview
}
} else if let link = artifacts.links.first {
LinkViewRepresentable(meta: .url(link))
.frame(height: 50)
}
}
}
var MainContent: some View {
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 with_padding {
truncatedText
.padding(.horizontal)
} else {
truncatedText
}
}
if size == .selected || damus_state.settings.auto_translate {
if with_padding {
translateView
.padding(.horizontal)
} else {
translateView
}
}
if show_images && artifacts.images.count > 0 {
@@ -51,30 +112,29 @@ struct NoteContentView: View {
Blur()
.disabled(true)
}
.cornerRadius(10)
}
if artifacts.invoices.count > 0 {
InvoicesView(our_pubkey: damus_state.keypair.pubkey, invoices: artifacts.invoices)
//.cornerRadius(10)
}
if let preview = self.preview, show_images {
preview
} else {
ForEach(artifacts.links, id:\.self) { link in
if let url = link {
LinkViewRepresentable(meta: .url(url))
.frame(height: 50)
}
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()
.onAppear() {
self.artifacts = render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
}
MainContent
.onReceive(handle_notify(.profile_updated)) { notif in
let profile = notif.object as! ProfileUpdate
let blocks = event.blocks(damus_state.keypair.privkey)
@@ -92,21 +152,19 @@ struct NoteContentView: View {
}
}
.task {
if let preview = damus_state.previews.lookup(self.event.id) {
switch preview {
case .value(let view):
self.preview = view
case .failed:
// don't try to refetch meta if we've failed
return
}
guard self.preview == nil else {
return
}
if show_images, artifacts.links.count == 1 {
let meta = await getMetaData(for: artifacts.links.first!)
let view = meta.map { LinkViewRepresentable(meta: .linkmeta($0)) }
damus_state.previews.store(evid: self.event.id, preview: view)
damus_state.previews.store(evid: self.event.id, preview: meta)
guard case .value(let cached) = damus_state.previews.lookup(self.event.id) else {
return
}
let view = LinkViewRepresentable(meta: .linkmeta(cached))
self.preview = view
}
@@ -131,15 +189,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
}
@@ -148,16 +206,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
}
}
@@ -167,17 +225,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)
}
}
extension View {
func translate_button_style() -> some View {
return self
.font(.footnote)
.contentShape(Rectangle())
.padding([.top, .bottom], 10)
NoteContentView(damus_state: state, event: NostrEvent(content: content, pubkey: "pk"), show_images: true, size: .normal, artifacts: artifacts, options: [])
}
}
@@ -194,6 +242,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)
}
@@ -201,9 +250,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)
@@ -230,6 +287,57 @@ 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? {
guard case .value(let cached) = previews.lookup(evid) else {
return nil
}
guard let height = cached.intrinsic_height else {
return nil
}
return height
}
func load_cached_preview(previews: PreviewCache, evid: String) -> LinkViewRepresentable? {
guard case .value(let meta) = previews.lookup(evid) else {
return nil
}
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) + "..."
}
}

View File

@@ -0,0 +1,247 @@
//
// RepostGroupView.swift
// damus
//
// Created by William Casarin on 2023-02-21.
//
import SwiftUI
enum EventGroupType {
case repost(EventGroup)
case reaction(EventGroup)
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):
return grp.events
case .reaction(let grp):
return grp.events
case .zap(let zapgrp):
return zapgrp.zap_requests()
case .profile_zap(let zapgrp):
return zapgrp.zap_requests()
}
}
}
enum ReactingTo {
case your_post
case tagged_in
case your_profile
}
func determine_reacting_to(our_pubkey: String, ev: NostrEvent?) -> ReactingTo {
guard let ev else {
return .your_profile
}
if ev.pubkey == our_pubkey {
return .your_post
}
return .tagged_in
}
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)
}
}
/**
Returns a notification string describing user actions in response to an event group type.
The localization keys read by this function are the following (although some keys may not actually be used in practice):
"??" - returned when there are no events associated with the specified event group type.
"reacted_tagged_in_1" - returned when 1 reaction occurred to a post that the current user was tagged in
"reacted_tagged_in_2" - returned when 2 reactions occurred to a post that the current user was tagged in
"reacted_tagged_in_3" - returned when 3 or more reactions occurred to a post that the current user was tagged in
"reacted_your_post_1" - returned when 1 reaction occurred to the current user's post
"reacted_your_post_2" - returned when 2 reactions occurred to the current user's post
"reacted_your_post_3" - returned when 3 or more reactions occurred to the current user's post
"reacted_your_profile_1" - returned when 1 reaction occurred to the current user's profile
"reacted_your_profile_2" - returned when 2 reactions occurred to the current user's profile
"reacted_your_profile_3" - returned when 3 or more reactions occurred to the current user's profile
"reposted_tagged_in_1" - returned when 1 repost occurred to a post that the current user was tagged in
"reposted_tagged_in_2" - returned when 2 reposts occurred to a post that the current user was tagged in
"reposted_tagged_in_3" - returned when 3 or more reposts occurred to a post that the current user was tagged in
"reposted_your_post_1" - returned when 1 repost occurred to the current user's post
"reposted_your_post_2" - returned when 2 reposts occurred to the current user's post
"reposted_your_post_3" - returned when 3 or more reposts occurred to the current user's post
"reposted_your_profile_1" - returned when 1 repost occurred to the current user's profile
"reposted_your_profile_2" - returned when 2 reposts occurred to the current user's profile
"reposted_your_profile_3" - returned when 3 or more reposts occurred to the current user's profile
"zapped_tagged_in_1" - returned when 1 zap occurred to a post that the current user was tagged in
"zapped_tagged_in_2" - returned when 2 zaps occurred to a post that the current user was tagged in
"zapped_tagged_in_3" - returned when 3 or more zaps occurred to a post that the current user was tagged in
"zapped_your_post_1" - returned when 1 zap occurred to the current user's post
"zapped_your_post_2" - returned when 2 zaps occurred to the current user's post
"zapped_your_post_3" - returned when 3 or more zaps occurred to the current user's post
"zapped_your_profile_1" - returned when 1 zap occurred to the current user's profile
"zapped_your_profile_2" - returned when 2 zaps occurred to the current user's profile
"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 format = localizedStringFormat(key: localization_key, locale: locale)
switch group.events.count {
case 1:
let display_name = event_group_author_name(profiles: profiles, ind: 0, group: group)
return String(format: format, locale: locale, display_name)
case 2:
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: format, locale: locale, alice_name, bob_name)
default:
let alice_name = event_group_author_name(profiles: profiles, ind: 0, group: group)
let count = group.events.count - 1
return String(format: format, locale: locale, count, alice_name)
}
}
func reacting_to_verb(group: EventGroupType) -> String {
switch group {
case .reaction:
return "reacted"
case .repost:
return "reposted"
case .zap: fallthrough
case .profile_zap:
return "zapped"
}
}
struct EventGroupView: View {
let state: DamusState
let event: NostrEvent?
let group: EventGroupType
var GroupDescription: some View {
Text(verbatim: "\(reacting_to_text(profiles: state.profiles, our_pubkey: state.pubkey, group: group, ev: event))")
}
func ZapIcon(_ zapgrp: ZapGroup) -> some View {
let fmt = format_msats_abbrev(zapgrp.msat_total)
return VStack(alignment: .center) {
Image(systemName: "bolt.fill")
.foregroundColor(.orange)
Text("\(fmt)")
.foregroundColor(Color.orange)
}
}
var GroupIcon: some View {
Group {
switch group {
case .repost:
Image(systemName: "arrow.2.squarepath")
.foregroundColor(Color("DamusGreen"))
case .reaction:
Image("shaka-full")
.resizable()
.frame(width: 24, height: 24)
.foregroundColor(.accentColor)
case .profile_zap(let zapgrp):
ZapIcon(zapgrp)
case .zap(let zapgrp):
ZapIcon(zapgrp)
}
}
}
var body: some View {
HStack(alignment: .top) {
GroupIcon
.frame(width: PFP_SIZE + 10)
VStack(alignment: .leading) {
ProfilePicturesView(state: state, events: group.events)
GroupDescription
if let event {
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)
}
.buttonStyle(.plain)
}
}
}
.padding([.top], 6)
}
}
let test_encoded_post = "{\"id\": \"8ba545ab96959fe0ce7db31bc10f3ac3aa5353bc4428dbf1e56a7be7062516db\",\"pubkey\": \"7e27509ccf1e297e1df164912a43406218f8bd80129424c3ef798ca3ef5c8444\",\"created_at\": 1677013417,\"kind\": 1,\"tags\": [],\"content\": \"hello\",\"sig\": \"93684f15eddf11f42afbdd81828ee9fc35350344d8650c78909099d776e9ad8d959cd5c4bff7045be3b0b255144add43d0feef97940794a1bc9c309791bebe4a\"}"
let test_repost_1 = NostrEvent(id: "", content: test_encoded_post, pubkey: "pk1", kind: 6, tags: [], createdAt: 1)
let test_repost_2 = NostrEvent(id: "", content: test_encoded_post, pubkey: "pk2", kind: 6, tags: [], createdAt: 1)
let test_reposts = [test_repost_1, test_repost_2]
let test_event_group = EventGroup(events: test_reposts)
struct EventGroupView_Previews: PreviewProvider {
static var previews: some View {
VStack {
EventGroupView(state: test_damus_state(), event: test_event, group: .repost(test_event_group))
.frame(height: 200)
.padding()
EventGroupView(state: test_damus_state(), event: test_event, group: .reaction(test_event_group))
.frame(height: 200)
.padding()
}
}
}

View File

@@ -0,0 +1,86 @@
//
// NotificationItemView.swift
// damus
//
// Created by William Casarin on 2023-02-21.
//
import SwiftUI
enum ShowItem {
case show(NostrEvent?)
case dontshow(NostrEvent?)
}
func notification_item_event(events: EventCache, notif: NotificationItem) -> ShowItem {
switch notif {
case .repost(let evid, _):
return .dontshow(events.lookup(evid))
case .reply(let ev):
return .show(ev)
case .reaction(let evid, _):
return .dontshow(events.lookup(evid))
case .event_zap(let evid, _):
return .dontshow(events.lookup(evid))
case .profile_zap:
return .show(nil)
}
}
struct NotificationItemView: View {
let state: DamusState
let item: NotificationItem
var show_item: ShowItem {
notification_item_event(events: state.events, notif: item)
}
func Item(_ ev: NostrEvent?) -> some View {
Group {
switch item {
case .repost(_, let evgrp):
EventGroupView(state: state, event: ev, group: .repost(evgrp))
case .event_zap(_, let zapgrp):
EventGroupView(state: state, event: ev, group: .zap(zapgrp))
case .profile_zap(let grp):
EventGroupView(state: state, event: nil, group: .profile_zap(grp))
case .reaction(_, let evgrp):
EventGroupView(state: state, event: ev, group: .reaction(evgrp))
case .reply(let ev):
NavigationLink(destination: ThreadView(state: state, thread: ThreadModel(event: ev, damus_state: state))) {
EventView(damus: state, event: ev)
}
.buttonStyle(.plain)
}
Divider()
.padding([.top,.bottom], 5)
}
}
var body: some View {
Group {
switch show_item {
case .show(let ev):
Item(ev)
case .dontshow(let ev):
if let ev {
Item(ev)
}
}
}
}
}
let test_notification_item: NotificationItem = .repost("evid", test_event_group)
struct NotificationItemView_Previews: PreviewProvider {
static var previews: some View {
NotificationItemView(state: test_damus_state(), item: test_notification_item)
}
}

View File

@@ -0,0 +1,137 @@
//
// NotificationsView.swift
// damus
//
// Created by William Casarin on 2023-02-21.
//
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.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
scroll_to_event(scroller: scroller, id: "startblock", delay: 0.0, animate: true, anchor: .top)
}
}
.onAppear {
let _ = notifications.flush()
}
}
}
struct NotificationsView_Previews: PreviewProvider {
static var previews: some View {
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)
}

View File

@@ -0,0 +1,37 @@
//
// ProfilePicturesView.swift
// damus
//
// Created by William Casarin on 2023-02-22.
//
import SwiftUI
struct ProfilePicturesView: View {
let state: DamusState
let events: [NostrEvent]
@State var nav_target: String? = nil
@State var navigating: Bool = false
var body: some View {
NavigationLink(destination: ProfileView(damus_state: state, pubkey: nav_target ?? ""), isActive: $navigating) {
EmptyView()
}
HStack {
ForEach(events.prefix(8)) { ev in
ProfilePicView(pubkey: ev.pubkey, size: 32.0, highlight: .none, profiles: state.profiles)
.onTapGesture {
nav_target = ev.pubkey
navigating = true
}
}
}
}
}
struct ProfilePicturesView_Previews: PreviewProvider {
static var previews: some View {
ProfilePicturesView(state: test_damus_state(), events: [test_event, test_event])
}
}

View File

@@ -36,41 +36,43 @@ struct ParticipantsView: View {
Spacer()
}
VStack {
ForEach(originalReferences.pRefs) { participant in
let pubkey = participant.id
HStack {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey)
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
if let about = profile?.about {
Text(FollowUserView.markdown.process(about))
.lineLimit(3)
.font(.footnote)
ScrollView {
ForEach(originalReferences.pRefs) { participant in
let pubkey = participant.id
HStack {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey)
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
if let about = profile?.about {
Text(FollowUserView.markdown.process(about))
.lineLimit(3)
.font(.footnote)
}
}
Spacer()
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 30))
.foregroundColor(references.contains(participant) ? .purple : .gray)
}
Spacer()
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 30))
.foregroundColor(references.contains(participant) ? .purple : .gray)
}
.onTapGesture {
if references.contains(participant) {
references = references.filter {
$0 != participant
}
} else {
.onTapGesture {
if references.contains(participant) {
// Don't add it twice
references = references.filter {
$0 != participant
}
} else {
references.append(participant)
if references.contains(participant) {
// Don't add it twice
} else {
references.append(participant)
}
}
}
}
}
}
}
}
Spacer()
}

View File

@@ -15,11 +15,14 @@ enum NostrPostResult {
let POST_PLACEHOLDER = NSLocalizedString("Type your post here...", comment: "Text box prompt to ask user to type their post.")
struct PostView: View {
@State var post: String = ""
@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,13 +41,20 @@ struct PostView: View {
func dismiss() {
self.presentationMode.wrappedValue.dismiss()
}
func send_post() {
var kind: NostrKind = .text
if replying_to?.known_kind == .chat {
kind = .chat
}
let content = self.post.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
post.enumerateAttributes(in: NSRange(location: 0, length: post.length), options: []) { attributes, range, stop in
if let link = attributes[.link] as? String {
post.replaceCharacters(in: range, with: link)
}
}
let content = self.post.string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
let new_post = NostrPost(content: content, references: references, kind: kind)
NotificationCenter.default.post(name: .post, object: NostrPostResult.post(new_post))
@@ -52,85 +62,167 @@ struct PostView: View {
if let replying_to {
damus_state.drafts.replies.removeValue(forKey: replying_to)
} else {
damus_state.drafts.post = ""
damus_state.drafts.post = NSMutableAttributedString(string: "")
}
dismiss()
}
var is_post_empty: Bool {
return post.allSatisfy { $0.isWhitespace }
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)
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) {
TextEditor(text: $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.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) {
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() {
if let replying_to {
if damus_state.drafts.replies[replying_to] == nil {
damus_state.drafts.replies[replying_to] = ""
damus_state.drafts.post = NSMutableAttributedString(string: "")
}
if let p = damus_state.drafts.replies[replying_to] {
post = p
@@ -144,13 +236,12 @@ struct PostView: View {
}
}
.onDisappear {
if let replying_to, let reply = damus_state.drafts.replies[replying_to], reply.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
if let replying_to, let reply = damus_state.drafts.replies[replying_to], reply.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
damus_state.drafts.replies.removeValue(forKey: replying_to)
} else if replying_to == nil && damus_state.drafts.post.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
damus_state.drafts.post = ""
} else if replying_to == nil && damus_state.drafts.post.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
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

View File

@@ -20,14 +20,55 @@ struct SearchedUser: Identifiable {
struct UserSearch: View {
let damus_state: DamusState
let search: String
@Binding var post: String
@Binding var post: NSMutableAttributedString
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 {
@@ -36,10 +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
}
post = post.replacingOccurrences(of: "@"+search, with: "@"+pk+" ")
on_user_tapped(user: user)
}
}
}
@@ -49,7 +87,7 @@ struct UserSearch: View {
struct UserSearch_Previews: PreviewProvider {
static let search: String = "jb55"
@State static var post: String = "some @jb55"
@State static var post: NSMutableAttributedString = NSMutableAttributedString(string: "some @jb55")
static var previews: some View {
UserSearch(damus_state: test_damus_state(), search: search, post: $post)
@@ -57,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
}
@@ -79,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
}

View File

@@ -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)
}
}

View File

@@ -0,0 +1,46 @@
//
// MaybeAnonPfpView.swift
// damus
//
// Created by William Casarin on 2023-02-26.
//
import SwiftUI
struct MaybeAnonPfpView: View {
let state: DamusState
let is_anon: Bool
let pubkey: String
init(state: DamusState, event: NostrEvent, pubkey: String) {
self.state = state
self.is_anon = event_is_anonymous(ev: event)
self.pubkey = pubkey
}
init(state: DamusState, is_anon: Bool, pubkey: String) {
self.state = state
self.is_anon = is_anon
self.pubkey = pubkey
}
var body: some View {
Group {
if is_anon {
Image(systemName: "person.fill.questionmark")
.font(.largeTitle)
.frame(width: PFP_SIZE, height: PFP_SIZE)
} else {
NavigationLink(destination: ProfileView(damus_state: state, pubkey: pubkey)) {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: state.profiles)
}
}
}
}
}
struct MaybeAnonPfpView_Previews: PreviewProvider {
static var previews: some View {
MaybeAnonPfpView(state: test_damus_state(), is_anon: true, pubkey: "anon")
}
}

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)
}
}

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)
}
}
}

View File

@@ -66,6 +66,7 @@ struct InnerProfilePicView: View {
.placeholder { _ in
Placeholder
}
.scaledToFill()
}
.frame(width: size, height: size)
.clipShape(Circle())

View File

@@ -49,6 +49,21 @@ func follow_btn_enabled_state(_ fs: FollowState) -> Bool {
}
}
func followersCountString(_ count: Int, locale: Locale = Locale.current) -> String {
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 format = localizedStringFormat(key: "relays_count", locale: locale)
return String(format: format, locale: locale, count)
}
struct EditButton: View {
let damus_state: DamusState
@@ -100,8 +115,6 @@ struct ProfileView: View {
static let markdown = Markdown()
@State private var selected_tab: ProfileTab = .posts
@StateObject var profile: ProfileModel
@StateObject var followers: FollowersModel
@State private var showingEditProfile = false
@State var showing_select_wallet: Bool = false
@State var is_zoomed: Bool = false
@@ -110,6 +123,21 @@ struct ProfileView: View {
@State var filter_state : FilterState = .posts
@State var yOffset: CGFloat = 0
@StateObject var profile: ProfileModel
@StateObject var followers: FollowersModel
init(damus_state: DamusState, profile: ProfileModel, followers: FollowersModel) {
self.damus_state = damus_state
self._profile = StateObject(wrappedValue: profile)
self._followers = StateObject(wrappedValue: followers)
}
init(damus_state: DamusState, pubkey: String) {
self.damus_state = damus_state
self._profile = StateObject(wrappedValue: ProfileModel(pubkey: pubkey, damus: damus_state))
self._followers = StateObject(wrappedValue: FollowersModel(damus_state: damus_state, target: pubkey))
}
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
@Environment(\.openURL) var openURL
@@ -156,6 +184,7 @@ struct ProfileView: View {
}
.frame(height: bannerHeight)
.allowsHitTesting(false)
}
var navbarHeight: CGFloat {
@@ -305,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)
}
}
@@ -319,7 +348,8 @@ struct ProfileView: View {
.foregroundColor(.gray)
} else {
let followerCount = followers.count!
Text("\(Text(verbatim: "\(followerCount)").font(.subheadline.weight(.medium))) \(Text(String(format: Bundle.main.localizedString(forKey: "followers_count", value: nil, table: nil), followerCount)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many people are following a user. In source English, the first variable is the number of followers, and the second variable is 'Follower' or 'Followers'.")
let noun_text = Text(verbatim: "\(followersCountString(followerCount))").font(.subheadline).foregroundColor(.gray)
Text("\(Text("\(followerCount)").font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many people are following a user. In source English, the first variable is the number of followers, and the second variable is 'Follower' or 'Followers'.")
}
}
}
@@ -343,7 +373,8 @@ 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 {
Text("\(Text(verbatim: "\(profile.following)").font(.subheadline.weight(.medium))) \(Text("Following", comment: "Part of a larger sentence to describe how many profiles a user is following.").font(.subheadline).foregroundColor(.gray))", 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'.")
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'.")
}
}
.buttonStyle(PlainButtonStyle())
@@ -366,7 +397,8 @@ struct ProfileView: View {
if let relays = profile.relays {
// Only open relay config view if the user is logged in with private key and they are looking at their own profile.
let relay_text = Text("\(Text(verbatim: "\(relays.keys.count)").font(.subheadline.weight(.medium))) \(Text(String(format: Bundle.main.localizedString(forKey: "relays_count", value: nil, table: nil), relays.keys.count)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.")
let noun_text = Text(verbatim: "\(relaysCountString(relays.keys.count))").font(.subheadline).foregroundColor(.gray)
let relay_text = Text("\(Text("\(relays.keys.count)").font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.")
if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user {
NavigationLink(destination: RelayConfigView(state: damus_state)) {
relay_text
@@ -443,9 +475,7 @@ struct ProfileView: View {
struct ProfileView_Previews: PreviewProvider {
static var previews: some View {
let ds = test_damus_state()
let followers = FollowersModel(damus_state: ds, target: ds.pubkey)
let profile_model = ProfileModel(pubkey: ds.pubkey, damus: ds)
ProfileView(damus_state: ds, profile: profile_model, followers: followers)
ProfileView(damus_state: ds, pubkey: ds.pubkey)
}
}
@@ -491,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(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)

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(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("@" + String(display_name ?? Profile.displayName(profile: profile, pubkey: pubkey)))
.foregroundColor(Color("DamusMediumGrey"))
.font(eventviewsize_to_font(size))
} else {
Text(String(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)
}
}
}

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)

View File

@@ -26,7 +26,7 @@ struct RelayFilterView: View {
}
var body: some View {
Text("To filter your \(timeline.rawValue) feed, please choose applicable relays from the list below:", comment: "Instructions on how to filter a specific timeline feed by choosing relay servers to filter on.")
Text("Please choose relays from the list below to filter the current feed:", comment: "Instructions on how to filter a specific timeline feed by choosing relay servers to filter on.")
.padding()
.padding(.top, 20)
.padding(.bottom, 0)

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)
}
}

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()
}

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: [])
}
}

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