Compare commits
316 Commits
tyiu/parti
...
tyiu/trans
| Author | SHA1 | Date | |
|---|---|---|---|
|
21b6790f74
|
|||
|
|
5a238502cb | ||
|
|
b0aac1fc42 | ||
|
|
72b51a81de | ||
|
|
8ec1fa29b1 | ||
|
|
81683f980a | ||
|
|
b9fc3f90d1 | ||
|
|
695699aa10 | ||
|
|
0a4e75bfec | ||
|
|
9fef2f071a | ||
|
|
c03b4cac11 | ||
|
|
b773df1204 | ||
|
|
c7a34379dd | ||
|
|
eabf37e35c | ||
|
|
e11147b217 | ||
|
|
7674f42596 | ||
|
|
8c37c8f008 | ||
|
|
74dbbcf1a2 | ||
|
|
e3283fc8f8 | ||
|
|
54fdcd1c84 | ||
|
|
5e0ff1a6a0 | ||
| 6517dcba3f | |||
|
|
63e28d4d79 | ||
| e5c0400b54 | |||
|
|
c6c47e824a | ||
|
866e93d338
|
|||
|
f75fc7eebe
|
|||
|
|
d19596c17e | ||
|
|
0b40cd127c | ||
|
|
754ee254e9 | ||
|
|
963cb37762 | ||
|
|
00da97307e | ||
|
|
312c798bb5 | ||
|
|
7110650267 | ||
|
|
242c1011d9 | ||
|
|
e203eece85 | ||
|
|
1b60524070 | ||
|
|
d15a2f0401 | ||
|
|
159d0fa2b5 | ||
|
|
61fddf800e | ||
|
|
b6d5b6f45e | ||
|
|
f5ed9cd5d4 | ||
|
|
57006b928b | ||
|
fd596241a2
|
|||
|
|
98f0b2f2d2 | ||
|
|
9a4d93824a | ||
|
|
f76563b354 | ||
|
|
b2ee924692 | ||
|
|
6fc70748fe | ||
|
|
5e972dbf2d | ||
|
|
4ebdd01b6c | ||
|
|
13c0c0d679 | ||
|
|
8297859f18 | ||
|
|
e996d5703b | ||
|
|
dfc397337b | ||
|
|
f84d4516db | ||
|
|
2e34230119 | ||
|
|
cad89525b7 | ||
|
|
d2cf18aeee | ||
|
|
a8ce39fc96 | ||
|
|
ed90139b0c | ||
|
|
022045d916 | ||
|
|
4bda490010 | ||
|
|
97382adb63 | ||
|
|
c582755246 | ||
|
|
44a59e8d57 | ||
|
|
98685645d3 | ||
|
|
14f71f1a1d | ||
|
|
91cb6a6763 | ||
|
|
a65351154b | ||
|
|
2e2b33e21d | ||
|
|
c24b0afb8f | ||
|
|
a357bbe4a6 | ||
|
|
b687006b64 | ||
|
|
1f095b0896 | ||
|
|
4f7ed36a7c | ||
|
|
393809c7d7 | ||
|
|
9091cb1aae | ||
|
|
e78a82e5b7 | ||
|
|
7b0ef5f4a7 | ||
|
|
66a5df68b3 | ||
|
|
fa2344b9ba | ||
|
|
68c018cf44 | ||
|
f367df2225
|
|||
|
|
e0984aab34 | ||
|
|
eabbb12195 | ||
|
|
7b1f4b7701 | ||
|
|
7b6d3ef9df | ||
|
|
bc58686016 | ||
|
|
a574dcb27c | ||
|
|
761982e359 | ||
|
|
57d48a0395 | ||
|
|
4f96c88b9b | ||
|
|
da11bc575a | ||
|
|
cc9532d958 | ||
|
|
35f4e7c78d | ||
|
|
d8c822858a | ||
|
|
ca0c837231 | ||
|
|
38fc5afa44 | ||
| 9b76afae4f | |||
|
|
f911f1646d | ||
|
|
20fd061293 | ||
|
|
3f5262cd5d | ||
|
|
982d15ab4a | ||
|
|
074b6efc0f | ||
|
|
ad0ca6ca1a | ||
|
|
e140cacfdf | ||
|
|
b825aa80d8 | ||
|
|
9ee91553c1 | ||
|
|
7ce862f552 | ||
|
|
231f9d1853 | ||
|
|
63acf11065 | ||
|
|
0502f06ef8 | ||
|
|
d921a40f24 | ||
| 2e82b349b7 | |||
|
|
b0007af030 | ||
|
|
dd5c2d7301 | ||
|
|
27c0fbf453 | ||
|
|
d1ad4dc9ff | ||
|
|
4c58c4ffef | ||
|
|
cb3603fb35 | ||
|
|
6df5288294 | ||
|
|
9e02dac5d0 | ||
|
|
b7d9db5cec | ||
|
|
e46792e596 | ||
|
|
fc65da3473 | ||
|
|
4f15469320 | ||
|
|
3ea3595902 | ||
|
|
3caebd9c63 | ||
|
|
4d4f340ab0 | ||
|
|
6a549e5019 | ||
|
|
52bf47a494 | ||
|
|
aee243d3e0 | ||
|
|
18745403ce | ||
|
|
07a20040a4 | ||
|
|
ef3ef03b7f | ||
|
|
71e3ee4867 | ||
|
|
252a77fd97 | ||
|
|
a611a5d252 | ||
|
|
1533be77d8 | ||
|
|
c05223ca2b | ||
|
|
5d441d3192 | ||
|
|
04bce34297 | ||
|
|
af8ce3d32d | ||
|
|
cabe584938 | ||
|
|
dd511c3061 | ||
|
|
18449c8c0d | ||
|
|
044631b324 | ||
|
|
318b254b5d | ||
|
|
487419d098 | ||
|
|
ba82f19a11 | ||
|
|
cba6b3aef7 | ||
|
|
6872382bb7 | ||
|
|
42ea150d45 | ||
|
|
85f86ee31f | ||
|
|
96decd2392 | ||
|
|
73f7b69654 | ||
|
|
d982bb886e | ||
|
|
9766653969 | ||
|
|
5d91e7e595 | ||
|
|
ae00c103ad | ||
|
|
88aa713729 | ||
|
|
be1c03ad0e | ||
|
|
b2b62828e3 | ||
|
|
d1a77891c7 | ||
|
|
20505236ae | ||
|
|
094ac34135 | ||
|
|
6b6743fcbb | ||
|
|
8059408d5f | ||
|
|
04fa4edad8 | ||
|
|
6fffe250c2 | ||
|
|
1e7d9a6373 | ||
|
|
21989719fc | ||
|
|
d5e4866c55 | ||
|
|
f305df3471 | ||
|
|
21320367b1 | ||
|
|
82723faf33 | ||
|
|
48434f83ae | ||
|
|
083d0fa0e5 | ||
| d5a646f9ce | |||
|
38a1ad7611
|
|||
|
|
9bc3860f00 | ||
|
|
35f5ac04b4 | ||
|
|
75b73718d1 | ||
|
|
29cacebe58 | ||
|
|
84ae914bcc | ||
|
|
ef5f3ae649 | ||
|
|
f8068a42e5 | ||
|
|
bdde33bb51 | ||
|
|
e3b602df13 | ||
|
|
38b17f1acd | ||
|
|
575b91554c | ||
|
|
f36bc84618 | ||
|
|
d54c9b7d12 | ||
|
2c6647c95a
|
|||
|
|
3c2f281c6d | ||
|
|
4ba63b0dbd | ||
|
|
e2df7d5df6 | ||
|
|
0dfea0680f | ||
|
|
6cc34632fd | ||
|
|
dffb60a601 | ||
|
|
df076b03fd | ||
|
|
fc83cd4db7 | ||
|
|
e01761ce72 | ||
|
|
efc50f5b18 | ||
|
|
10c9e8ddbc | ||
|
|
f88718d56e | ||
|
|
b6a7f52596 | ||
|
|
cff98161ee | ||
|
|
8a70240968 | ||
|
|
a4855775ef | ||
|
|
06c2741bf4 | ||
|
|
721bb9abf5 | ||
|
|
89bb293acd | ||
|
|
f9c330aebf | ||
|
|
ffbfcd36f5 | ||
|
|
52f568f9b3 | ||
|
|
1c2a7db328 | ||
|
|
3110abc65b | ||
|
|
a9f62960ec | ||
|
|
150bbb1eb2 | ||
|
|
0aff41d384 | ||
|
|
3fec9dd209 | ||
|
|
a560d50366 | ||
|
|
174f7f6cc5 | ||
| a325a3c064 | |||
|
|
d0a6c2e2e4 | ||
|
|
b58baca227 | ||
|
|
5423704980 | ||
|
|
241ed1041d | ||
|
|
5134004ff7 | ||
|
|
071a4209ea | ||
|
|
7f385b2e7e | ||
|
|
502c4daf6f | ||
|
|
ffe2c7284a | ||
|
|
6b1f57d6d0 | ||
|
|
77f5268336 | ||
| c72c0079cc | |||
|
|
5ab1d6294c | ||
|
|
2f90f2d4b7 | ||
|
|
7c2e8a6cc5 | ||
|
|
1288732e5d | ||
|
|
4a6c6a65ab | ||
|
|
0f29d67e1f | ||
|
|
9fd2f51971 | ||
|
|
386bae64ca | ||
|
|
4b5c217213 | ||
|
240fda2429
|
|||
|
bacd9b3c38
|
|||
|
0152286859
|
|||
|
|
06e9a1b392
|
||
|
|
483730af18
|
||
|
|
23229015a6
|
||
|
|
7ab95583df
|
||
|
|
b7a48a24e9
|
||
|
04028d9cff
|
|||
|
6918fb46cf
|
|||
|
|
2b854ef9b7
|
||
|
|
5eb61f1ac1
|
||
|
|
c3bbf7aa8f
|
||
|
|
1e52d958ac
|
||
|
|
5252e5f5bb
|
||
|
|
71d5625f04
|
||
|
|
990e783c30
|
||
|
|
3602189133
|
||
|
|
3ca9acdf34
|
||
|
|
2036d5843b
|
||
|
|
2d3bd11d56
|
||
|
|
a715987e71
|
||
|
|
0303031445
|
||
|
|
d6ae9a5d79
|
||
|
|
356bd06e6a
|
||
|
|
e757bdca90
|
||
|
|
ff1b4d724d
|
||
|
|
b8614f055c
|
||
|
|
63ab151a5e
|
||
|
|
fd9d4deb44
|
||
|
|
a5c719673e
|
||
|
|
2bf3c6718d
|
||
|
|
3f3e59488a
|
||
|
|
b1b4b5b6c9
|
||
|
|
7455665672
|
||
|
|
9f22234926
|
||
|
|
21a8a4e96f
|
||
|
|
c635f3d77a
|
||
|
|
2e9a4388b9
|
||
|
|
4a1949eeb8
|
||
|
|
b4fcb58bcb
|
||
|
|
7d852eb33b
|
||
|
|
a448f610c0
|
||
|
|
8cc561b8c6
|
||
|
|
01630d0a4c
|
||
|
16156f4d9a
|
|||
|
f840fe9c80
|
|||
|
77bcd1b715
|
|||
|
|
75fd8de456 | ||
|
|
71f7ea47df | ||
|
|
64b1a57918 | ||
|
|
6c63f8f22a | ||
|
|
673358408a | ||
|
|
e4dd585754 | ||
|
|
436d20dfbd | ||
|
|
810b3e1fa5 | ||
|
|
75fb0d19e2 | ||
|
|
a2749eaaaa | ||
|
|
83c9289345 | ||
|
|
4c3a83772e | ||
| 5cd4c2d75e | |||
|
|
85e797a054 | ||
|
|
9f52e2c246 | ||
|
|
0210ae5d61 | ||
|
|
e5749c8748 | ||
|
|
8b9958a4ad | ||
|
|
87a0bdac94 | ||
|
|
37b964c296 | ||
|
|
b1a2b47116 |
242
CHANGELOG.md
242
CHANGELOG.md
@@ -1,3 +1,226 @@
|
||||
## [1.4.0] - 2023-03-27
|
||||
|
||||
### Added
|
||||
|
||||
- Local zap notifications (Swift)
|
||||
- Add support for video uploads (Swift)
|
||||
- Auto Translation (Terry Yiu)
|
||||
- Portuguese (Brazil) translations (Andressa Munturo)
|
||||
- Spanish (Spain) translations (Max Pleb)
|
||||
- Vietnamese translations (ShiryoRyo)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed small notification hit boxes (Terry Yiu)
|
||||
|
||||
[1.4.0]: https://github.com/damus-io/damus/releases/tag/v1.4.0
|
||||
|
||||
## [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)
|
||||
- Canadian French (Pierre - synoptic_okubo)
|
||||
- Hungarian translations (Zoltan)
|
||||
- Korean translations (sogoagain)
|
||||
- Swedish translations (Pextar)
|
||||
|
||||
|
||||
### 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)
|
||||
- Bulgarian translations (elsat)
|
||||
- Persian translations (Mahdi Taghizadeh)
|
||||
- Ukrainian translations (Valeriia Khudiakova, Tony B)
|
||||
|
||||
|
||||
### 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)
|
||||
- Chinese, Traditional (Hong Kong) translations (rasputin)
|
||||
- Chinese, Traditional (Taiwan) translations (rasputin)
|
||||
|
||||
### 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
|
||||
@@ -6,6 +229,9 @@
|
||||
- Added the ability to select text on posts (OlegAba)
|
||||
- Added Posts or Post & Replies selector to Profile (ericholguin)
|
||||
- Improved profile navbar (OlegAba)
|
||||
- Czech translations (Martin Gabrhel)
|
||||
- Indonesian translations (johnybergzy)
|
||||
- Russian translations (Tony B)
|
||||
|
||||
|
||||
### Changed
|
||||
@@ -54,7 +280,6 @@
|
||||
### Added
|
||||
|
||||
- Relay Filtering (William Casarin)
|
||||
- Japanese translations (Terry Yiu)
|
||||
- Add password autofill on account login and creation (Terry Yiu)
|
||||
- Show if relay is paid (William Casarin)
|
||||
- Add "Follows You" indicator on profile (William Casarin)
|
||||
@@ -67,6 +292,10 @@
|
||||
- Copy invoice button (Joel Klabo)
|
||||
- Receive Lightning Zaps (William Casarin)
|
||||
- Allow text selection in bio (Suhail Saqan)
|
||||
- Chinese, Simplified (China mainland) translations (haolong, rasputin)
|
||||
- Dutch translations (Heimen Stoffels - Vistaus)
|
||||
- Greek translations (milicode)
|
||||
- Japanese translations (akiomik, foxytanuki, Guetsu Ren - Nighthaven, h3y6e, middlingphys)
|
||||
|
||||
|
||||
### Changed
|
||||
@@ -101,6 +330,7 @@
|
||||
- LibreTranslate note translations (Terry Yiu)
|
||||
- Added support for account deletion (William Casarin)
|
||||
- User tagging and autocompletion in posts (Swift)
|
||||
- Polish translations (pysiak)
|
||||
|
||||
|
||||
### Changed
|
||||
@@ -123,7 +353,8 @@
|
||||
|
||||
### Added
|
||||
|
||||
- Added Arabic and Portuguese translations (Barodane, Antonio Chagas)
|
||||
- Arabic translations (Barodane)
|
||||
- Portuguese translations (Antonio Chagas)
|
||||
- Add QRCode view for sharing your pubkey (ericholguin)
|
||||
- Added nostr: uri handling (William Casarin)
|
||||
|
||||
@@ -150,7 +381,8 @@
|
||||
### Added
|
||||
|
||||
- Reposts view (Terry Yiu)
|
||||
- Translations for it_IT, it_CH, fr_FR, de_DE, de_AT and lv_LV (Nicolò Carcagnì, Solobalbo, Gregor, Peter Gerstbach, SYX)
|
||||
- Italian translations (Nicolò Carcagnì)
|
||||
- Latvian translations (SYX)
|
||||
- Added ability to block users (William Casarin)
|
||||
- Added a way to report content (William Casarin)
|
||||
- Stretchable profile cover header (Swift)
|
||||
@@ -177,7 +409,9 @@
|
||||
|
||||
- Show website on profiles (William Casarin)
|
||||
- Add the ability to choose participants when replying (Joel Klabo)
|
||||
- Translations for de_AT, de_DE, tr_TR, fr_FR (Gregor, Peter Gerstbach, Taylan Benli, Solobalbo)
|
||||
- German translations (Gregor, Peter Gerstbach)
|
||||
- Turkish translations (Taylan Benli)
|
||||
- French (France) translations (Solobalbo)
|
||||
- Add DM Message Requests (William Casarin)
|
||||
|
||||
|
||||
|
||||
@@ -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 */; };
|
||||
@@ -116,12 +129,17 @@
|
||||
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFBA2804A34C0006080F /* ProofOfWork.swift */; };
|
||||
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FF7D42823313F009601DB /* Mentions.swift */; };
|
||||
4C8682872814DE470026224F /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8682862814DE470026224F /* ProfileView.swift */; };
|
||||
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */; };
|
||||
4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD152839DB54008EE7EF /* NostrMetadata.swift */; };
|
||||
4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD17283A9EE5008EE7EF /* LoginView.swift */; };
|
||||
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD19283AA67F008EE7EF /* Bech32.swift */; };
|
||||
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */; };
|
||||
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,10 +173,15 @@
|
||||
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 */; };
|
||||
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */; };
|
||||
4CE4F0F229D4FCFA005914DB /* DebouncedOnChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */; };
|
||||
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F8CC281352B30009DFBB /* Notifications.swift */; };
|
||||
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */; };
|
||||
4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */; };
|
||||
@@ -201,29 +224,40 @@
|
||||
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 */; };
|
||||
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfileZoomView.swift */; };
|
||||
5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF72FC129B9142F00124A13 /* ShareAction.swift */; };
|
||||
6439E014296790CF0020672B /* ProfilePicImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfilePicImageView.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 */; };
|
||||
F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParticipantsView.swift */; };
|
||||
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParicipantsView.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -255,6 +289,24 @@
|
||||
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>"; };
|
||||
3A325AC429C9E0B8002BE7ED /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A325AC529C9E0B8002BE7ED /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = vi; path = vi.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3A325AC629C9E0B8002BE7ED /* vi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = vi; path = vi.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3A325AC729C9E0CF002BE7ED /* es-ES */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-ES"; path = "es-ES.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A325AC829C9E0CF002BE7ED /* es-ES */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-ES"; path = "es-ES.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A325AC929C9E0CF002BE7ED /* es-ES */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-ES"; path = "es-ES.lproj/Localizable.stringsdict"; 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 +339,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>"; };
|
||||
@@ -296,9 +354,27 @@
|
||||
3AC524EE298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3AC524EF298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3AC524F0298C000B00693EBF /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ar; path = ar.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3AC59CA729CDDB78007E04A6 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3AC59CA829CDDB78007E04A6 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3AC59CA929CDDB78007E04A6 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "pt-BR"; path = "pt-BR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
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 +404,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 +489,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>"; };
|
||||
@@ -431,12 +515,17 @@
|
||||
4C75EFBA2804A34C0006080F /* ProofOfWork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProofOfWork.swift; sourceTree = "<group>"; };
|
||||
4C7FF7D42823313F009601DB /* Mentions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mentions.swift; sourceTree = "<group>"; };
|
||||
4C8682862814DE470026224F /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
|
||||
4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusColors.swift; sourceTree = "<group>"; };
|
||||
4C90BD152839DB54008EE7EF /* NostrMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrMetadata.swift; sourceTree = "<group>"; };
|
||||
4C90BD17283A9EE5008EE7EF /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; };
|
||||
4C90BD19283AA67F008EE7EF /* Bech32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32.swift; sourceTree = "<group>"; };
|
||||
4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Tests.swift; sourceTree = "<group>"; };
|
||||
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,10 +559,15 @@
|
||||
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>"; };
|
||||
4CE0E2B529A3ED5500DB4CA2 /* InnerTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InnerTimelineView.swift; sourceTree = "<group>"; };
|
||||
4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebouncedOnChange.swift; sourceTree = "<group>"; };
|
||||
4CE4F8CC281352B30009DFBB /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
|
||||
4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigView.swift; sourceTree = "<group>"; };
|
||||
4CE4F9E0285287B800C00DD9 /* TextFieldAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldAlert.swift; sourceTree = "<group>"; };
|
||||
@@ -519,28 +613,39 @@
|
||||
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>"; };
|
||||
6439E013296790CF0020672B /* ProfileZoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZoomView.swift; sourceTree = "<group>"; };
|
||||
5CF72FC129B9142F00124A13 /* ShareAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAction.swift; sourceTree = "<group>"; };
|
||||
6439E013296790CF0020672B /* ProfilePicImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePicImageView.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>"; };
|
||||
F7F0BA262978E54D009531F3 /* ParticipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantsView.swift; sourceTree = "<group>"; };
|
||||
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParicipantsView.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -584,6 +689,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3AA24801297E3DC20090C62D /* RepostView.swift */,
|
||||
4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */,
|
||||
);
|
||||
path = Reposts;
|
||||
sourceTree = "<group>";
|
||||
@@ -645,6 +751,8 @@
|
||||
4C0A3F8D280F63FF000448DE /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CCEB7A729B29DC90078AA28 /* Search */,
|
||||
4C54AA0829A55416003E4487 /* Notifications */,
|
||||
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */,
|
||||
4C0A3F8E280F640A000448DE /* ThreadModel.swift */,
|
||||
4C0A3F92280F66F5000448DE /* ReplyMap.swift */,
|
||||
@@ -665,6 +773,7 @@
|
||||
4C63334F283D40E500B1C9C3 /* HomeModel.swift */,
|
||||
4C633351283D419F00B1C9C3 /* SignalModel.swift */,
|
||||
4C5F9113283D694D0052CD1C /* FollowTarget.swift */,
|
||||
F75BA12C29A1855400E10810 /* BookmarksManager.swift */,
|
||||
4C5F9115283D855D0052CD1C /* EventsModel.swift */,
|
||||
4C5F9117283D88E40052CD1C /* FollowingModel.swift */,
|
||||
4C987B56283FD07F0042CE38 /* FollowersModel.swift */,
|
||||
@@ -683,13 +792,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 +836,7 @@
|
||||
4CB88387296AF97C00DC99E7 /* ActionBar */,
|
||||
4CE4F9E228528C5200C00DD9 /* AddRelayView.swift */,
|
||||
4C363A8728236948006E126D /* BlocksView.swift */,
|
||||
F75BA12E29A18EF500E10810 /* BookmarksView.swift */,
|
||||
4C285C8128385570008A31F1 /* CarouselView.swift */,
|
||||
4C0A3F8B280F5FCA000448DE /* ChatroomView.swift */,
|
||||
4C0A3F90280F6528000448DE /* ChatView.swift */,
|
||||
@@ -721,28 +856,25 @@
|
||||
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 */,
|
||||
F7F0BA262978E54D009531F3 /* ParticipantsView.swift */,
|
||||
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */,
|
||||
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */,
|
||||
4C3AC7A628369BA200E1F516 /* SearchHomeView.swift */,
|
||||
4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */,
|
||||
4C363AA128296A7E006E126D /* SearchView.swift */,
|
||||
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */,
|
||||
4C3AC7A02835A81400E1F516 /* SetupView.swift */,
|
||||
E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */,
|
||||
E9E4ED0A295867B900DD7078 /* ThreadView.swift */,
|
||||
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */,
|
||||
4CB55EF4295E679D007FD187 /* UserRelaysView.swift */,
|
||||
647D9A8C2968520300A295DE /* SideMenuView.swift */,
|
||||
9609F057296E220800069BF3 /* BannerImageView.swift */,
|
||||
4CB8838E296F781C00DC99E7 /* ReactionsView.swift */,
|
||||
6439E013296790CF0020672B /* ProfileZoomView.swift */,
|
||||
4CF0ABD529817F5B00D66079 /* ReportView.swift */,
|
||||
4CF0ABE42981EE0C00D66079 /* EULAView.swift */,
|
||||
3AA247FE297E3D900090C62D /* RepostsView.swift */,
|
||||
@@ -775,6 +907,7 @@
|
||||
4C7FF7D628233637009601DB /* Util */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7C0F392D29B57C8F0039859C /* Extensions */,
|
||||
4CE879492995B58700F758CC /* Relays */,
|
||||
4CF0ABEA29844B2F00D66079 /* AnyCodable */,
|
||||
4CC7AAE6297EFA7B00430951 /* Zap.swift */,
|
||||
@@ -802,8 +935,11 @@
|
||||
4CB883B5297730E400DC99E7 /* LNUrls.swift */,
|
||||
3AB72AB8298ECF30004BB58C /* Translator.swift */,
|
||||
4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */,
|
||||
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */,
|
||||
4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */,
|
||||
3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */,
|
||||
4C30AC7729A577AB00E2BD5A /* EventCache.swift */,
|
||||
4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */,
|
||||
4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */,
|
||||
);
|
||||
path = Util;
|
||||
sourceTree = "<group>";
|
||||
@@ -828,6 +964,7 @@
|
||||
children = (
|
||||
4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */,
|
||||
4CB88388296AF99A00DC99E7 /* EventDetailBar.swift */,
|
||||
5CF72FC129B9142F00124A13 /* ShareAction.swift */,
|
||||
);
|
||||
path = ActionBar;
|
||||
sourceTree = "<group>";
|
||||
@@ -843,8 +980,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 +1005,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 = (
|
||||
@@ -894,6 +1055,7 @@
|
||||
4CB883AF297705DD00DC99E7 /* ZapButton.swift */,
|
||||
4C42812B298C848200DBF26F /* TranslateView.swift */,
|
||||
7CFF6316299FEFE5005D382A /* SelectableText.swift */,
|
||||
4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
@@ -965,6 +1127,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 +1165,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CE879572996C45300F758CC /* ZapsView.swift */,
|
||||
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */,
|
||||
);
|
||||
path = Zaps;
|
||||
sourceTree = "<group>";
|
||||
@@ -1037,6 +1204,26 @@
|
||||
path = Posting;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4CFF8F6129CC9A80008DB934 /* Images */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */,
|
||||
4CFF8F6629CC9E3A008DB934 /* ImageView.swift */,
|
||||
6439E013296790CF0020672B /* ProfilePicImageView.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 = (
|
||||
@@ -1137,23 +1324,35 @@
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
Base,
|
||||
"es-419",
|
||||
"en-US",
|
||||
"tr-TR",
|
||||
"fr-FR",
|
||||
"lv-LV",
|
||||
"it-IT",
|
||||
de,
|
||||
"pt-PT",
|
||||
"pl-PL",
|
||||
ar,
|
||||
nl,
|
||||
"zh-CN",
|
||||
"el-GR",
|
||||
ja,
|
||||
id,
|
||||
bg,
|
||||
cs,
|
||||
de,
|
||||
"el-GR",
|
||||
"en-US",
|
||||
"es-419",
|
||||
"es-ES",
|
||||
fa,
|
||||
"fr-CA",
|
||||
"fr-FR",
|
||||
"hu-HU",
|
||||
id,
|
||||
"it-IT",
|
||||
ja,
|
||||
ko,
|
||||
"lv-LV",
|
||||
nl,
|
||||
"pl-PL",
|
||||
"pt-BR",
|
||||
"pt-PT",
|
||||
ru,
|
||||
"sv-SE",
|
||||
"tr-TR",
|
||||
uk,
|
||||
vi,
|
||||
"zh-CN",
|
||||
"zh-HK",
|
||||
"zh-TW",
|
||||
);
|
||||
mainGroup = 4CE6DEDA27F7A08100C66700;
|
||||
packageReferences = (
|
||||
@@ -1208,8 +1407,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 +1423,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 +1436,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 */,
|
||||
@@ -1245,7 +1450,7 @@
|
||||
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */,
|
||||
4C3D52B6298DB4E6001C5831 /* ZapEvent.swift in Sources */,
|
||||
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */,
|
||||
F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */,
|
||||
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */,
|
||||
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
|
||||
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */,
|
||||
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */,
|
||||
@@ -1259,21 +1464,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 +1492,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 +1507,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 +1524,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,14 +1555,17 @@
|
||||
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 */,
|
||||
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */,
|
||||
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
|
||||
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
|
||||
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */,
|
||||
6439E014296790CF0020672B /* ProfilePicImageView.swift in Sources */,
|
||||
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */,
|
||||
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */,
|
||||
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */,
|
||||
@@ -1368,8 +1591,12 @@
|
||||
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 */,
|
||||
4CE4F0F229D4FCFA005914DB /* DebouncedOnChange.swift in Sources */,
|
||||
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
|
||||
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
|
||||
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */,
|
||||
@@ -1393,11 +1620,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 +1637,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 +1651,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 +1701,18 @@
|
||||
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 */,
|
||||
3A325AC629C9E0B8002BE7ED /* vi */,
|
||||
3A325AC929C9E0CF002BE7ED /* es-ES */,
|
||||
3AC59CA929CDDB78007E04A6 /* pt-BR */,
|
||||
);
|
||||
name = Localizable.stringsdict;
|
||||
sourceTree = "<group>";
|
||||
@@ -1491,6 +1736,18 @@
|
||||
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 */,
|
||||
3A325AC529C9E0B8002BE7ED /* vi */,
|
||||
3A325AC829C9E0CF002BE7ED /* es-ES */,
|
||||
3AC59CA829CDDB78007E04A6 /* pt-BR */,
|
||||
);
|
||||
name = InfoPlist.strings;
|
||||
sourceTree = "<group>";
|
||||
@@ -1514,6 +1771,19 @@
|
||||
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 */,
|
||||
3A325AC429C9E0B8002BE7ED /* vi */,
|
||||
3A325AC729C9E0CF002BE7ED /* es-ES */,
|
||||
3AC59CA729CDDB78007E04A6 /* pt-BR */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
@@ -1649,7 +1919,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1672,7 +1942,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
MARKETING_VERSION = 1.4.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -1691,7 +1961,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1714,7 +1984,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
MARKETING_VERSION = 1.4.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xF4",
|
||||
"green" : "0xEE",
|
||||
"red" : "0xEE"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x1E",
|
||||
"green" : "0x1C",
|
||||
"red" : "0x1C"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
21
damus/Assets.xcassets/bitcoin-logo.imageset/Contents.json
vendored
Normal file
21
damus/Assets.xcassets/bitcoin-logo.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "bitcoin-logo.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
7
damus/Assets.xcassets/bitcoin-logo.imageset/bitcoin-logo.svg
vendored
Normal file
7
damus/Assets.xcassets/bitcoin-logo.imageset/bitcoin-logo.svg
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20px" height="20px" viewBox="0 0 20 20" version="1.1">
|
||||
<g id="surface1">
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(96.862745%,57.647059%,10.196078%);fill-opacity:1;" d="M 19.699219 12.417969 C 18.363281 17.777344 12.9375 21.035156 7.582031 19.699219 C 2.226562 18.363281 -1.035156 12.9375 0.300781 7.582031 C 1.636719 2.222656 7.0625 -1.035156 12.417969 0.300781 C 17.773438 1.632812 21.035156 7.0625 19.699219 12.417969 Z M 19.699219 12.417969 "/>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(100%,100%,100%);fill-opacity:1;" d="M 14.410156 8.574219 C 14.609375 7.246094 13.59375 6.53125 12.210938 6.050781 L 12.660156 4.25 L 11.5625 3.976562 L 11.125 5.730469 C 10.835938 5.660156 10.539062 5.589844 10.246094 5.523438 L 10.6875 3.757812 L 9.589844 3.484375 L 9.140625 5.285156 C 8.902344 5.230469 8.667969 5.179688 8.4375 5.121094 L 8.441406 5.117188 L 6.925781 4.738281 L 6.636719 5.910156 C 6.636719 5.910156 7.449219 6.097656 7.433594 6.109375 C 7.875 6.21875 7.957031 6.511719 7.941406 6.746094 L 7.429688 8.800781 C 7.460938 8.808594 7.5 8.820312 7.546875 8.835938 L 7.429688 8.808594 L 6.710938 11.683594 C 6.65625 11.820312 6.519531 12.023438 6.210938 11.945312 C 6.21875 11.960938 5.410156 11.746094 5.410156 11.746094 L 4.867188 13 L 6.296875 13.359375 C 6.5625 13.425781 6.820312 13.492188 7.078125 13.558594 L 6.621094 15.382812 L 7.71875 15.65625 L 8.167969 13.851562 C 8.46875 13.933594 8.757812 14.007812 9.042969 14.078125 L 8.59375 15.875 L 9.691406 16.148438 L 10.144531 14.328125 C 12.015625 14.683594 13.425781 14.539062 14.015625 12.847656 C 14.492188 11.484375 13.992188 10.699219 13.007812 10.1875 C 13.726562 10.019531 14.265625 9.550781 14.410156 8.574219 Z M 11.902344 12.089844 C 11.5625 13.453125 9.269531 12.71875 8.523438 12.53125 L 9.128906 10.117188 C 9.871094 10.300781 12.253906 10.667969 11.902344 12.089844 Z M 12.242188 8.554688 C 11.933594 9.796875 10.023438 9.164062 9.402344 9.011719 L 9.949219 6.820312 C 10.570312 6.976562 12.5625 7.261719 12.242188 8.554688 Z M 12.242188 8.554688 "/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -8,8 +8,8 @@
|
||||
import SwiftUI
|
||||
|
||||
let RECTANGLE_GRADIENT = LinearGradient(gradient: Gradient(colors: [
|
||||
Color("DamusPurple"),
|
||||
Color("DamusBlue")
|
||||
DamusColors.purple,
|
||||
DamusColors.blue
|
||||
]), startPoint: .leading, endPoint: .trailing)
|
||||
|
||||
struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
|
||||
@@ -52,9 +52,10 @@ struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
|
||||
.accentColor(tag == selection ? textColor() : .gray)
|
||||
}
|
||||
}
|
||||
.background(Color(UIColor.systemBackground))
|
||||
}
|
||||
|
||||
func textColor() -> Color {
|
||||
colorScheme == .light ? Color("DamusBlack") : Color("DamusWhite")
|
||||
colorScheme == .light ? DamusColors.black : DamusColors.white
|
||||
}
|
||||
}
|
||||
|
||||
22
damus/Components/DamusColors.swift
Normal file
22
damus/Components/DamusColors.swift
Normal file
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// DamusColors.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-03-27.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
class DamusColors {
|
||||
static let adaptableGrey = Color("DamusAdaptableGrey")
|
||||
static let white = Color("DamusWhite")
|
||||
static let black = Color("DamusBlack")
|
||||
static let lightGrey = Color("DamusLightGrey")
|
||||
static let mediumGrey = Color("DamusMediumGrey")
|
||||
static let darkGrey = Color("DamusDarkGrey")
|
||||
static let green = Color("DamusGreen")
|
||||
static let purple = Color("DamusPurple")
|
||||
static let blue = Color("DamusBlue")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ struct InvoiceView: View {
|
||||
.foregroundColor(.gray)
|
||||
} else {
|
||||
Image(systemName: "checkmark.circle")
|
||||
.foregroundColor(Color("DamusGreen"))
|
||||
.foregroundColor(DamusColors.green)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,19 +24,33 @@ struct NIP05Badge: View {
|
||||
self.clickable = clickable
|
||||
}
|
||||
|
||||
var nip05_color: Color {
|
||||
return get_nip05_color(pubkey: pubkey, contacts: contacts)
|
||||
var nip05_color: Bool {
|
||||
return use_nip05_color(pubkey: pubkey, contacts: contacts)
|
||||
}
|
||||
|
||||
var Seal: some View {
|
||||
Group {
|
||||
if nip05_color {
|
||||
LINEAR_GRADIENT
|
||||
.mask(Image(systemName: "checkmark.seal.fill")
|
||||
.resizable()
|
||||
).frame(width: 14, height: 14)
|
||||
} else {
|
||||
Image(systemName: "checkmark.seal.fill")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 2) {
|
||||
Image(systemName: "checkmark.seal.fill")
|
||||
.font(.footnote)
|
||||
.foregroundColor(nip05_color)
|
||||
Seal
|
||||
|
||||
if show_domain {
|
||||
if clickable {
|
||||
Text(nip05.host)
|
||||
.foregroundColor(nip05_color)
|
||||
.nip05_colorized(gradient: nip05_color)
|
||||
.onTapGesture {
|
||||
if let nip5url = nip05.siteUrl {
|
||||
openURL(nip5url)
|
||||
@@ -44,7 +58,7 @@ struct NIP05Badge: View {
|
||||
}
|
||||
} else {
|
||||
Text(nip05.host)
|
||||
.foregroundColor(nip05_color)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,8 +66,19 @@ struct NIP05Badge: View {
|
||||
}
|
||||
}
|
||||
|
||||
func get_nip05_color(pubkey: String, contacts: Contacts) -> Color {
|
||||
return contacts.is_friend_or_self(pubkey) ? .accentColor : .gray
|
||||
extension View {
|
||||
func nip05_colorized(gradient: Bool) -> some View {
|
||||
if gradient {
|
||||
return AnyView(self.foregroundStyle(LINEAR_GRADIENT))
|
||||
} else {
|
||||
return AnyView(self.foregroundColor(.gray))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func use_nip05_color(pubkey: String, contacts: Contacts) -> Bool {
|
||||
return contacts.is_friend_or_self(pubkey) ? true : false
|
||||
}
|
||||
|
||||
struct NIP05Badge_Previews: PreviewProvider {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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.")) {
|
||||
@@ -28,7 +30,7 @@ struct TranslateView: View {
|
||||
|
||||
func Translated(lang: String, artifacts: NoteArtifacts) -> some View {
|
||||
return Group {
|
||||
Button(NSLocalizedString("Translated from \(lang)", comment: "Button to indicate that the note has been translated from a different language.")) {
|
||||
Button(String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang)) {
|
||||
show_translated_note = false
|
||||
}
|
||||
.translate_button_style()
|
||||
@@ -38,7 +40,7 @@ struct TranslateView: View {
|
||||
}
|
||||
|
||||
func CheckingStatus(lang: String) -> some View {
|
||||
return Button(NSLocalizedString("Translating from \(lang)...", comment: "Button to indicate that the note is in the process of being translated from a different language.")) {
|
||||
return Button(String(format: NSLocalizedString("Translating from %@...", comment: "Button to indicate that the note is in the process of being translated from a different language."), lang)) {
|
||||
show_translated_note = false
|
||||
}
|
||||
.translate_button_style()
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -22,6 +22,7 @@ struct WebsiteLink: View {
|
||||
}, label: {
|
||||
Text(link_text)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(LINEAR_GRADIENT)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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: DamusColors.purple, 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,67 @@ 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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -36,5 +46,9 @@
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Damus needs access to your camera if you want to upload photos from it</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Damus needs access to your microphone if you want to upload recorded videos from it</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
71
damus/Models/BookmarksManager.swift
Normal file
71
damus/Models/BookmarksManager.swift
Normal 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 = []
|
||||
}
|
||||
}
|
||||
@@ -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: ""))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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] = [:]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,47 @@ 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(profiles: Profiles, 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) {
|
||||
if damus_state.settings.zap_vibration {
|
||||
// Generate zap vibration
|
||||
zap_vibrate(zap_amount: zap.invoice.amount)
|
||||
}
|
||||
// Create in-app local notification for zap received.
|
||||
create_in_app_zap_notification(profiles: profiles, zap: zap)
|
||||
}
|
||||
|
||||
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(profiles: self.damus_state.profiles, ev: ev, our_keypair: our_keypair, zapper: local_zapper)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -177,7 +184,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(profiles: self.damus_state.profiles, ev: ev, our_keypair: our_keypair, zapper: zapper)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +236,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 +262,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 +326,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 +381,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 +390,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 +461,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 +506,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)
|
||||
@@ -651,6 +675,7 @@ func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEve
|
||||
|
||||
DispatchQueue.main.async {
|
||||
profiles.validated[ev.pubkey] = validated
|
||||
profiles.nip05_pubkey[nip05] = ev.pubkey
|
||||
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
||||
}
|
||||
}
|
||||
@@ -895,3 +920,55 @@ 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()
|
||||
}
|
||||
|
||||
func describe_zap_type(_ zap: Zap) -> String? {
|
||||
if zap.private_request != nil {
|
||||
return "Private"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func create_in_app_zap_notification(profiles: Profiles, zap: Zap) {
|
||||
let content = UNMutableNotificationContent()
|
||||
let typ = describe_zap_type(zap).map({ "\($0) " }) ?? ""
|
||||
|
||||
content.title = typ + "Zap"
|
||||
let satString = zap.invoice.amount == 1000 ? "sat" : "sats"
|
||||
|
||||
let src = zap.private_request ?? zap.request.ev
|
||||
let anon = event_is_anonymous(ev: src)
|
||||
let pk = anon ? "anon" : src.pubkey
|
||||
let profile = profiles.lookup(id: pk)
|
||||
let sats = format_msats_abbrev(zap.invoice.amount)
|
||||
let name = Profile.displayName(profile: profile, pubkey: pk).display_name
|
||||
let message = src.content.count == 0 ? "" : ": \"\(src.content)\""
|
||||
|
||||
content.body = "You received \(sats) \(satString) from \(name)\(message)"
|
||||
content.sound = UNNotificationSound.default
|
||||
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
|
||||
|
||||
let request = UNNotificationRequest(identifier: "myZapNotification", content: content, trigger: trigger)
|
||||
|
||||
UNUserNotificationCenter.current().add(request) { error in
|
||||
if let error = error {
|
||||
print("Error: \(error)")
|
||||
} else {
|
||||
print("Local notification scheduled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
54
damus/Models/ImageUploadModel.swift
Normal file
54
damus/Models/ImageUploadModel.swift
Normal file
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// ImageUploadModel.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-03-16.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
|
||||
enum MediaUpload {
|
||||
case image(URL)
|
||||
case video(URL)
|
||||
|
||||
var genericFileName: String {
|
||||
"damus_generic_filename.\(file_extension)"
|
||||
}
|
||||
|
||||
var file_extension: String {
|
||||
switch self {
|
||||
case .image(let url):
|
||||
return url.pathExtension
|
||||
case .video(let url):
|
||||
return url.pathExtension
|
||||
}
|
||||
}
|
||||
|
||||
var is_image: Bool {
|
||||
if case .image = self {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
class ImageUploadModel: NSObject, URLSessionTaskDelegate, ObservableObject {
|
||||
@Published var progress: Double? = nil
|
||||
|
||||
func start(media: MediaUpload, uploader: MediaUploader) async -> ImageUploadResult {
|
||||
let res = await create_upload_request(mediaToUpload: media, mediaUploader: 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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? {
|
||||
|
||||
32
damus/Models/Notifications/EventGroup.swift
Normal file
32
damus/Models/Notifications/EventGroup.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
59
damus/Models/Notifications/ZapGroup.swift
Normal file
59
damus/Models/Notifications/ZapGroup.swift
Normal 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
|
||||
}
|
||||
}
|
||||
|
||||
320
damus/Models/NotificationsModel.swift
Normal file
320
damus/Models/NotificationsModel.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
12
damus/Models/Search/SearchResultsModel.swift
Normal file
12
damus/Models/Search/SearchResultsModel.swift
Normal file
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// SearchResultsModel.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-03-03.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
class SearchResultsModel: ObservableObject {
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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_media_uploader(_ pubkey: String) -> MediaUploader {
|
||||
if let defaultMediaUploader = UserDefaults.standard.string(forKey: "default_media_uploader"),
|
||||
let defaultMediaUploader = MediaUploader(rawValue: defaultMediaUploader) {
|
||||
return defaultMediaUploader
|
||||
} else {
|
||||
return .nostrBuild
|
||||
}
|
||||
}
|
||||
|
||||
private func get_translation_service(_ pubkey: String) -> TranslationService? {
|
||||
guard let translation_service = UserDefaults.standard.string(forKey: "translation_service") else {
|
||||
return nil
|
||||
@@ -83,6 +97,12 @@ class UserSettingsStore: ObservableObject {
|
||||
UserDefaults.standard.set(default_wallet.rawValue, forKey: "default_wallet")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var default_media_uploader: MediaUploader {
|
||||
didSet {
|
||||
UserDefaults.standard.set(default_media_uploader.rawValue, forKey: "default_media_uploader")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var show_wallet_selector: Bool {
|
||||
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_media_uploader = get_media_uploader(pubkey)
|
||||
|
||||
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
|
||||
zap_vibration = UserDefaults.standard.object(forKey: "zap_vibration") as? Bool ?? false
|
||||
disable_animation = should_disable_image_animation()
|
||||
auto_translate = UserDefaults.standard.object(forKey: "auto_translate") as? Bool ?? true
|
||||
show_only_preferred_languages = UserDefaults.standard.object(forKey: "show_only_preferred_languages") as? Bool ?? false
|
||||
|
||||
// Note from @tyiu:
|
||||
// Default translation service is disabled by default for now until we gain some confidence that it is working well in production.
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -21,4 +21,5 @@ enum NostrKind: Int {
|
||||
case chat = 42
|
||||
case list = 30000
|
||||
case zap = 9735
|
||||
case zap_request = 9734
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import UIKit
|
||||
class Profiles {
|
||||
var profiles: [String: TimestampedProfile] = [:]
|
||||
var validated: [String: NIP05] = [:]
|
||||
var nip05_pubkey: [String: String] = [:]
|
||||
var zappers: [String: String] = [:]
|
||||
|
||||
func is_validated(_ pk: String) -> NIP05? {
|
||||
|
||||
69
damus/Util/DebouncedOnChange.swift
Normal file
69
damus/Util/DebouncedOnChange.swift
Normal file
@@ -0,0 +1,69 @@
|
||||
// https://github.com/Tunous/DebouncedOnChange/blob/5670ea13e8ad33e9cc3197f6d13ce492dc0e46ab/Sources/DebouncedOnChange/DebouncedChangeViewModifier.swift
|
||||
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
|
||||
extension View {
|
||||
|
||||
/// Adds a modifier for this view that fires an action only when a time interval in seconds represented by
|
||||
/// `debounceTime` elapses between value changes.
|
||||
///
|
||||
/// Each time the value changes before `debounceTime` passes, the previous action will be cancelled and the next
|
||||
/// action /// will be scheduled to run after that time passes again. This mean that the action will only execute
|
||||
/// after changes to the value /// stay unmodified for the specified `debounceTime` in seconds.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - value: The value to check against when determining whether to run the closure.
|
||||
/// - debounceTime: The time in seconds to wait after each value change before running `action` closure.
|
||||
/// - action: A closure to run when the value changes.
|
||||
/// - Returns: A view that fires an action after debounced time when the specified value changes.
|
||||
public func onChange<Value>(
|
||||
of value: Value,
|
||||
debounceTime: TimeInterval,
|
||||
perform action: @escaping (_ newValue: Value) -> Void
|
||||
) -> some View where Value: Equatable {
|
||||
self.modifier(DebouncedChangeViewModifier(trigger: value, debounceTime: debounceTime, action: action))
|
||||
}
|
||||
}
|
||||
|
||||
private struct DebouncedChangeViewModifier<Value>: ViewModifier where Value: Equatable {
|
||||
let trigger: Value
|
||||
let debounceTime: TimeInterval
|
||||
let action: (Value) -> Void
|
||||
|
||||
@State private var debouncedTask: Task<Void, Never>?
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content.onChange(of: trigger) { value in
|
||||
debouncedTask?.cancel()
|
||||
debouncedTask = Task.delayed(seconds: debounceTime) { @MainActor in
|
||||
action(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Task {
|
||||
|
||||
/// Asynchronously runs the given `operation` in its own task after the specified number of `seconds`.
|
||||
///
|
||||
/// The operation will be executed after specified number of `seconds` passes. You can cancel the task earlier
|
||||
/// for the operation to be skipped.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - time: Delay time in seconds.
|
||||
/// - operation: The operation to execute.
|
||||
/// - Returns: Handle to the task which can be cancelled.
|
||||
@discardableResult
|
||||
public static func delayed(
|
||||
seconds: TimeInterval,
|
||||
operation: @escaping @Sendable () async -> Void
|
||||
) -> Self where Success == Void, Failure == Never {
|
||||
Self {
|
||||
do {
|
||||
try await Task<Never, Never>.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
|
||||
await operation()
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
damus/Util/DisplayName.swift
Normal file
66
damus/Util/DisplayName.swift
Normal 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)
|
||||
}
|
||||
92
damus/Util/EventCache.swift
Normal file
92
damus/Util/EventCache.swift
Normal 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 = [:]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
49
damus/Util/Extensions/Binding+.swift
Normal file
49
damus/Util/Extensions/Binding+.swift
Normal file
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// Binding+.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Oleg Abalonski on 3/5/23.
|
||||
// Ref: https://josephduffy.co.uk/posts/mapping-optional-binding-to-bool
|
||||
|
||||
import os.log
|
||||
import SwiftUI
|
||||
|
||||
extension Binding where Value == Bool {
|
||||
/// Creates a binding by mapping an optional value to a `Bool` that is
|
||||
/// `true` when the value is non-`nil` and `false` when the value is `nil`.
|
||||
///
|
||||
/// When the value of the produced binding is set to `false` the value
|
||||
/// of `bindingToOptional`'s `wrappedValue` is set to `nil`.
|
||||
///
|
||||
/// Setting the value of the produce binding to `true` does nothing and
|
||||
/// will log an error.
|
||||
///
|
||||
/// - parameter bindingToOptional: A `Binding` to an optional value, used to calculate the `wrappedValue`.
|
||||
public init<Wrapped>(mappedTo bindingToOptional: Binding<Wrapped?>) {
|
||||
self.init(
|
||||
get: { bindingToOptional.wrappedValue != nil },
|
||||
set: { newValue in
|
||||
if !newValue {
|
||||
bindingToOptional.wrappedValue = nil
|
||||
} else {
|
||||
os_log(
|
||||
.error,
|
||||
"Optional binding mapped to optional has been set to `true`, which will have no effect. Current value: %@",
|
||||
String(describing: bindingToOptional.wrappedValue)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Binding {
|
||||
/// Returns a binding by mapping this binding's value to a `Bool` that is
|
||||
/// `true` when the value is non-`nil` and `false` when the value is `nil`.
|
||||
///
|
||||
/// When the value of the produced binding is set to `false` this binding's value
|
||||
/// is set to `nil`.
|
||||
public func mappedToBool<Wrapped>() -> Binding<Bool> where Value == Wrapped? {
|
||||
return Binding<Bool>(mappedTo: self)
|
||||
}
|
||||
}
|
||||
@@ -14,19 +14,18 @@ extension KFOptionSetter {
|
||||
options.callbackQueue = .dispatch(.global(qos: .background))
|
||||
options.processingQueue = .dispatch(.global(qos: .background))
|
||||
options.downloader = CustomImageDownloader.shared
|
||||
options.backgroundDecode = true
|
||||
options.cacheOriginalImage = true
|
||||
options.scaleFactor = UIScreen.main.scale
|
||||
|
||||
options.processor = CustomImageProcessor(
|
||||
maxSize: imageContext.maxMebibyteSize(),
|
||||
downsampleSize: imageContext.downsampleSize()
|
||||
)
|
||||
|
||||
options.cacheSerializer = CustomCacheSerializer(
|
||||
maxSize: imageContext.maxMebibyteSize(),
|
||||
downsampleSize: imageContext.downsampleSize()
|
||||
)
|
||||
options.backgroundDecode = true
|
||||
options.cacheOriginalImage = true
|
||||
options.scaleFactor = UIScreen.main.scale
|
||||
options.onlyLoadFirstFrame = should_disable_image_animation()
|
||||
|
||||
return self
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
34
damus/Util/LocalizationUtil.swift
Normal file
34
damus/Util/LocalizationUtil.swift
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -39,11 +39,20 @@ enum NIP05Validation {
|
||||
case valid
|
||||
}
|
||||
|
||||
func validate_nip05(pubkey: String, nip05_str: String) async -> NIP05? {
|
||||
struct FetchedNIP05 {
|
||||
let response: NIP05Response
|
||||
let nip05: NIP05Response
|
||||
}
|
||||
|
||||
func fetch_nip05_str(nip05_str: String) async -> NIP05Response? {
|
||||
guard let nip05 = NIP05.parse(nip05_str) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return await fetch_nip05(nip05: nip05)
|
||||
}
|
||||
|
||||
func fetch_nip05(nip05: NIP05) async -> NIP05Response? {
|
||||
guard let url = nip05.url else {
|
||||
return nil
|
||||
}
|
||||
@@ -57,6 +66,18 @@ func validate_nip05(pubkey: String, nip05_str: String) async -> NIP05? {
|
||||
return nil
|
||||
}
|
||||
|
||||
return decoded
|
||||
}
|
||||
|
||||
func validate_nip05(pubkey: String, nip05_str: String) async -> NIP05? {
|
||||
guard let nip05 = NIP05.parse(nip05_str) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let decoded = await fetch_nip05(nip05: nip05) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let stored_pk = decoded.names[nip05.username] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -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
8
damus/Util/PostBox.swift
Normal file
@@ -0,0 +1,8 @@
|
||||
//
|
||||
// PostBox.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-03-20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
@@ -75,10 +77,10 @@ struct EventActionBar: View {
|
||||
send_like()
|
||||
}
|
||||
}
|
||||
|
||||
Text(verbatim: "\(bar.likes > 0 ? "\(bar.likes)" : "")")
|
||||
.font(.footnote.weight(.medium))
|
||||
.foregroundColor(bar.liked ? Color.accentColor : Color.gray)
|
||||
|
||||
.nip05_colorized(gradient: bar.liked)
|
||||
}
|
||||
|
||||
if let lnurl = self.lnurl {
|
||||
@@ -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,48 @@ 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) {
|
||||
Image(liked ? "shaka-full" : "shaka-line")
|
||||
.foregroundColor(liked ? .accentColor : .gray)
|
||||
|
||||
Button(action: {
|
||||
withAnimation(Animation.easeOut(duration: 0.15)) {
|
||||
self.action()
|
||||
shouldAnimate = true
|
||||
amountOfAngleIncrease = 20.0
|
||||
}
|
||||
}) {
|
||||
if liked {
|
||||
LINEAR_GRADIENT
|
||||
.mask(Image("shaka-full")
|
||||
.resizable()
|
||||
).frame(width: 14, height: 14)
|
||||
} else {
|
||||
Image("shaka-line")
|
||||
.foregroundColor(.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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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: "")
|
||||
|
||||
110
damus/Views/ActionBar/ShareAction.swift
Normal file
110
damus/Views/ActionBar/ShareAction.swift
Normal 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 ? DamusColors.mediumGrey : DamusColors.white
|
||||
|
||||
VStack {
|
||||
Text("Share Note", comment: "Title text to indicate that the buttons below are meant to be used to share a note with others.")
|
||||
.padding()
|
||||
.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 ? NSLocalizedString("Remove Bookmark", comment: "Button text to remove bookmark from a note.") : NSLocalizedString("Add Bookmark", comment: "Button text to add bookmark to a note.")
|
||||
let boomarkCol = isBookmarked ? Color(.red) : col
|
||||
ShareActionButton(img: bookmarkImg, text: bookmarkTxt, 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 ? DamusColors.black : DamusColors.white)
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 24)
|
||||
.stroke(colorScheme == .light ? DamusColors.mediumGrey : DamusColors.white, lineWidth: 1)
|
||||
}
|
||||
.padding(EdgeInsets(top: 10, leading: 50, bottom: 25, trailing: 50))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,72 +8,41 @@
|
||||
import SwiftUI
|
||||
|
||||
struct AddRelayView: View {
|
||||
@Binding var show_add_relay: Bool
|
||||
@Binding var relay: String
|
||||
|
||||
let action: (String?) -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Form {
|
||||
Section(NSLocalizedString("Add Relay", comment: "Label for section for adding a relay server.")) {
|
||||
ZStack(alignment: .leading) {
|
||||
HStack{
|
||||
TextField(NSLocalizedString("wss://some.relay.com", comment: "Placeholder example for relay server address."), text: $relay)
|
||||
.padding(2)
|
||||
.padding(.leading, 25)
|
||||
.autocorrectionDisabled(true)
|
||||
.textInputAutocapitalization(.never)
|
||||
|
||||
Label("", systemImage: "xmark.circle.fill")
|
||||
.foregroundColor(.blue)
|
||||
.padding(.trailing, -25.0)
|
||||
.opacity((relay == "") ? 0.0 : 1.0)
|
||||
.onTapGesture {
|
||||
self.relay = ""
|
||||
}
|
||||
}
|
||||
|
||||
Label("", systemImage: "doc.on.clipboard")
|
||||
.padding(.leading, -10)
|
||||
.onTapGesture {
|
||||
if let pastedrelay = UIPasteboard.general.string {
|
||||
self.relay = pastedrelay
|
||||
}
|
||||
}
|
||||
ZStack(alignment: .leading) {
|
||||
HStack{
|
||||
TextField(NSLocalizedString("wss://some.relay.com", comment: "Placeholder example for relay server address."), text: $relay)
|
||||
.padding(2)
|
||||
.padding(.leading, 25)
|
||||
.autocorrectionDisabled(true)
|
||||
.textInputAutocapitalization(.never)
|
||||
|
||||
Label("", systemImage: "xmark.circle.fill")
|
||||
.foregroundColor(.accentColor)
|
||||
.padding(.trailing, -25.0)
|
||||
.opacity((relay == "") ? 0.0 : 1.0)
|
||||
.onTapGesture {
|
||||
self.relay = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VStack {
|
||||
HStack {
|
||||
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of view adding user inputted relay.")) {
|
||||
show_add_relay = false
|
||||
action(nil)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(NSLocalizedString("Add", comment: "Button to confirm adding user inputted relay.")) {
|
||||
show_add_relay = false
|
||||
action(relay)
|
||||
relay = ""
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.contentShape(Rectangle())
|
||||
Label("", systemImage: "doc.on.clipboard")
|
||||
.padding(.leading, -10)
|
||||
.onTapGesture {
|
||||
if let pastedrelay = UIPasteboard.general.string {
|
||||
self.relay = pastedrelay
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AddRelayView_Previews: PreviewProvider {
|
||||
@State static var show: Bool = true
|
||||
@State static var relay: String = ""
|
||||
|
||||
static var previews: some View {
|
||||
AddRelayView(show_add_relay: $show, relay: $relay, action: {_ in })
|
||||
AddRelayView(relay: $relay)
|
||||
}
|
||||
}
|
||||
|
||||
244
damus/Views/AttachMediaUtility.swift
Normal file
244
damus/Views/AttachMediaUtility.swift
Normal file
@@ -0,0 +1,244 @@
|
||||
//
|
||||
// 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(mediaData: Data, boundary: String, mediaUploader: MediaUploader, mediaToUpload: MediaUpload) -> Data {
|
||||
let body = NSMutableData();
|
||||
let contentType = mediaToUpload.is_image ? "image/jpg" : "video/mp4"
|
||||
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=\(mediaUploader.nameParam); filename=\(mediaToUpload.genericFileName)\r\n")
|
||||
body.appendString(string: "Content-Type: \(contentType)\r\n\r\n")
|
||||
body.append(mediaData as Data)
|
||||
body.appendString(string: "\r\n")
|
||||
body.appendString(string: "--\(boundary)--\r\n")
|
||||
return body as Data
|
||||
}
|
||||
|
||||
func create_upload_request(mediaToUpload: MediaUpload, mediaUploader: MediaUploader, progress: URLSessionTaskDelegate) async -> ImageUploadResult {
|
||||
var mediaData: Data?
|
||||
guard let url = URL(string: mediaUploader.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")
|
||||
|
||||
switch mediaToUpload {
|
||||
case .image(let img):
|
||||
do {
|
||||
mediaData = try Data(contentsOf: img)
|
||||
} catch {
|
||||
return .failed(error)
|
||||
}
|
||||
case .video(let url):
|
||||
do {
|
||||
mediaData = try Data(contentsOf: url)
|
||||
} catch {
|
||||
return .failed(error)
|
||||
}
|
||||
}
|
||||
|
||||
guard let mediaData else {
|
||||
return .failed(nil)
|
||||
}
|
||||
|
||||
request.httpBody = create_upload_body(mediaData: mediaData, boundary: boundary, mediaUploader: mediaUploader, mediaToUpload: mediaToUpload)
|
||||
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(for: request, delegate: progress)
|
||||
|
||||
guard let responseString = String(data: data, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue)) else {
|
||||
print("Upload failed getting response string")
|
||||
return .failed(nil)
|
||||
}
|
||||
|
||||
guard let url = mediaUploader.getMediaURL(from: responseString, mediaIsImage: mediaToUpload.is_image) else {
|
||||
print("Upload failed getting media 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 damusState: DamusState
|
||||
let onImagePicked: (URL) -> Void
|
||||
let onVideoPicked: (URL) -> Void
|
||||
|
||||
final class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
|
||||
@Binding private var presentationMode: PresentationMode
|
||||
private let sourceType: UIImagePickerController.SourceType
|
||||
private let onImagePicked: (URL) -> Void
|
||||
private let onVideoPicked: (URL) -> Void
|
||||
|
||||
init(presentationMode: Binding<PresentationMode>,
|
||||
sourceType: UIImagePickerController.SourceType,
|
||||
onImagePicked: @escaping (URL) -> Void,
|
||||
onVideoPicked: @escaping (URL) -> Void) {
|
||||
_presentationMode = presentationMode
|
||||
self.sourceType = sourceType
|
||||
self.onImagePicked = onImagePicked
|
||||
self.onVideoPicked = onVideoPicked
|
||||
}
|
||||
|
||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||
if let videoURL = info[UIImagePickerController.InfoKey.mediaURL] as? URL {
|
||||
// Handle the selected video
|
||||
onVideoPicked(videoURL)
|
||||
} else if let imageURL = info[UIImagePickerController.InfoKey.imageURL] as? URL {
|
||||
// Handle the selected image
|
||||
self.onImagePicked(imageURL)
|
||||
}
|
||||
presentationMode.dismiss()
|
||||
}
|
||||
|
||||
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||
presentationMode.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
return Coordinator(presentationMode: presentationMode,
|
||||
sourceType: sourceType,
|
||||
onImagePicked: { url in
|
||||
// Handle the selected image URL
|
||||
onImagePicked(url)
|
||||
},
|
||||
onVideoPicked: { videoURL in
|
||||
// Handle the selected video URL
|
||||
onVideoPicked(videoURL)
|
||||
})
|
||||
}
|
||||
|
||||
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
|
||||
let picker = UIImagePickerController()
|
||||
picker.sourceType = sourceType
|
||||
let mediaUploader = get_media_uploader(damusState.keypair.pubkey)
|
||||
picker.mediaTypes = ["public.image", "com.compuserve.gif"]
|
||||
if mediaUploader.supportsVideo {
|
||||
picker.mediaTypes.append("public.movie")
|
||||
}
|
||||
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 MediaUploader: 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 supportsVideo: Bool {
|
||||
switch self {
|
||||
case .nostrBuild:
|
||||
return true
|
||||
case .nostrImg:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
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 getMediaURL(from responseString: String, mediaIsImage: Bool) -> 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 = mediaIsImage ? "https://nostr.build/i/\(nostrBuildImageName)" : "https://nostr.build/av/\(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
|
||||
}
|
||||
}
|
||||
}
|
||||
61
damus/Views/BookmarksView.swift
Normal file
61
damus/Views/BookmarksView.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
@@ -27,6 +29,8 @@ struct ConfigView: View {
|
||||
@ObservedObject var settings: UserSettingsStore
|
||||
|
||||
let generator = UIImpactFeedbackGenerator(style: .light)
|
||||
|
||||
private let DELETE_KEYWORD = "DELETE"
|
||||
|
||||
init(state: DamusState) {
|
||||
self.state = state
|
||||
@@ -35,6 +39,10 @@ struct ConfigView: View {
|
||||
_privkey = State(initialValue: self.state.keypair.privkey_bech32 ?? "")
|
||||
_settings = ObservedObject(initialValue: state.settings)
|
||||
}
|
||||
|
||||
func textColor() -> Color {
|
||||
colorScheme == .light ? DamusColors.black : DamusColors.white
|
||||
}
|
||||
|
||||
func authenticateLocally(completion: @escaping (Bool) -> Void) {
|
||||
// Need to authenticate only once while ConfigView is presented
|
||||
@@ -66,12 +74,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 +143,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,40 +202,84 @@ 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_media_uploader) {
|
||||
ForEach(MediaUploader.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)
|
||||
TextField(String(format: NSLocalizedString("Type %@ to delete", comment: "Text field prompt asking user to type DELETE in all caps to confirm that they want to proceed with deleting their account."), DELETE_KEYWORD), text: $delete_text)
|
||||
Button(NSLocalizedString("Cancel", comment: "Cancel deleting the user."), role: .cancel) {
|
||||
confirm_delete_account = false
|
||||
}
|
||||
@@ -239,7 +288,7 @@ struct ConfigView: View {
|
||||
return
|
||||
}
|
||||
|
||||
guard delete_text == "DELETE" else {
|
||||
guard delete_text == DELETE_KEYWORD else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -346,3 +395,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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -41,6 +41,9 @@ struct DirectMessagesView: View {
|
||||
ForEach(dms, id: \.0) { tup in
|
||||
MaybeEvent(tup)
|
||||
.padding(.top, 10)
|
||||
|
||||
Divider()
|
||||
.padding([.top], 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -81,7 +83,7 @@ struct EditMetadataView: View {
|
||||
}
|
||||
|
||||
func imageBorderColor() -> Color {
|
||||
colorScheme == .light ? Color("DamusWhite") : Color("DamusBlack")
|
||||
colorScheme == .light ? DamusColors.white : DamusColors.black
|
||||
}
|
||||
|
||||
func save() {
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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: [])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,18 +13,13 @@ 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))
|
||||
}
|
||||
|
||||
@@ -32,14 +27,10 @@ struct MutedEventView: View {
|
||||
return !should_show_event(contacts: damus_state.contacts, ev: event)
|
||||
}
|
||||
|
||||
var FillColor: Color {
|
||||
colorScheme == .light ? Color("DamusLightGrey") : Color("DamusDarkGrey")
|
||||
}
|
||||
|
||||
var MutedBox: some View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.foregroundColor(FillColor)
|
||||
.foregroundColor(DamusColors.adaptableGrey)
|
||||
|
||||
HStack {
|
||||
Text("Post from a user you've blocked", comment: "Text to indicate that what is being shown is a post from a user who has been blocked.")
|
||||
@@ -55,17 +46,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 +84,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
22
damus/Views/Events/WideEventView.swift
Normal file
22
damus/Views/Events/WideEventView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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(DamusColors.green)
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
@@ -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))
|
||||
@@ -50,11 +50,11 @@ struct FollowButtonView: View {
|
||||
}
|
||||
|
||||
func filledTextColor() -> Color {
|
||||
colorScheme == .light ? Color("DamusWhite") : Color("DamusBlack")
|
||||
colorScheme == .light ? DamusColors.white : DamusColors.black
|
||||
}
|
||||
|
||||
func fillColor() -> Color {
|
||||
colorScheme == .light ? Color("DamusBlack") : Color("DamusWhite")
|
||||
colorScheme == .light ? DamusColors.black : DamusColors.white
|
||||
}
|
||||
|
||||
func emptyColor() -> Color {
|
||||
@@ -62,7 +62,7 @@ struct FollowButtonView: View {
|
||||
}
|
||||
|
||||
func borderColor() -> Color {
|
||||
colorScheme == .light ? Color("DamusDarkGrey") : Color("DamusLightGrey")
|
||||
colorScheme == .light ? DamusColors.darkGrey : DamusColors.lightGrey
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,16 +29,16 @@ 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
|
||||
FollowUserView(target: .pubkey(pk), damus_state: damus_state)
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
.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()
|
||||
}
|
||||
@@ -46,7 +46,6 @@ struct FollowersView: View {
|
||||
followers.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
struct FollowingView: View {
|
||||
@@ -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."))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
51
damus/Views/Images/ImageContainerView.swift
Normal file
51
damus/Views/Images/ImageContainerView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
43
damus/Views/Images/ImageContextMenuModifier.swift
Normal file
43
damus/Views/Images/ImageContextMenuModifier.swift
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
82
damus/Views/Images/ImageView.swift
Normal file
82
damus/Views/Images/ImageView.swift
Normal file
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// 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 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 {
|
||||
NavDismissBarView()
|
||||
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")])
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
private struct ImageContainerView: View {
|
||||
struct ProfileImageContainerView: View {
|
||||
|
||||
let url: URL?
|
||||
|
||||
@@ -39,14 +39,11 @@ private struct ImageContainerView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct ProfileZoomView: View {
|
||||
|
||||
let pubkey: String
|
||||
let profiles: Profiles
|
||||
struct NavDismissBarView: View {
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
var navBarView: some View {
|
||||
var body: some View {
|
||||
HStack {
|
||||
Button(action: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
@@ -61,6 +58,14 @@ struct ProfileZoomView: View {
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct ProfilePicImageView: View {
|
||||
|
||||
let pubkey: String
|
||||
let profiles: Profiles
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -68,7 +73,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)
|
||||
@@ -79,7 +84,7 @@ struct ProfileZoomView: View {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}))
|
||||
}
|
||||
.overlay(navBarView, alignment: .top)
|
||||
.overlay(NavDismissBarView(), alignment: .top)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +92,7 @@ struct ProfileZoomView_Previews: PreviewProvider {
|
||||
static let pubkey = "ca48854ac6555fed8e439ebb4fa2d928410e0eef13fa41164ec45aaaa132d846"
|
||||
|
||||
static var previews: some View {
|
||||
ProfileZoomView(
|
||||
ProfilePicImageView(
|
||||
pubkey: pubkey,
|
||||
profiles: make_preview_profiles(pubkey))
|
||||
}
|
||||
@@ -47,7 +47,7 @@ struct TabButton: View {
|
||||
.frame(width: 10, height: 10, alignment: .topTrailing)
|
||||
.alignmentGuide(VerticalAlignment.center) { a in a.height + 2.0 }
|
||||
.alignmentGuide(HorizontalAlignment.center) { a in a.width - 12.0 }
|
||||
.foregroundColor(.accentColor)
|
||||
.foregroundStyle(LINEAR_GRADIENT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = DamusColors.purple
|
||||
return attributedString
|
||||
}
|
||||
|
||||
func url_str(_ url: URL) -> AttributedString {
|
||||
var attributedString = AttributedString(stringLiteral: url.absoluteString)
|
||||
attributedString.link = url
|
||||
attributedString.foregroundColor = .purple
|
||||
attributedString.foregroundColor = DamusColors.purple
|
||||
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 = DamusColors.purple
|
||||
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 = DamusColors.purple
|
||||
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) + "..."
|
||||
}
|
||||
}
|
||||
|
||||
250
damus/Views/Notifications/EventGroupView.swift
Normal file
250
damus/Views/Notifications/EventGroupView.swift
Normal file
@@ -0,0 +1,250 @@
|
||||
//
|
||||
// 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(DamusColors.green)
|
||||
case .reaction:
|
||||
LINEAR_GRADIENT
|
||||
.mask(Image("shaka-full")
|
||||
.resizable()
|
||||
).frame(width: 24, height: 24)
|
||||
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)
|
||||
|
||||
if let event {
|
||||
let thread = ThreadModel(event: event, damus_state: state)
|
||||
let dest = ThreadView(state: state, thread: thread)
|
||||
NavigationLink(destination: dest) {
|
||||
VStack(alignment: .leading) {
|
||||
GroupDescription
|
||||
EventBody(damus_state: state, event: event, size: .normal, options: [])
|
||||
.padding([.top], 1)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
} else {
|
||||
GroupDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
.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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
86
damus/Views/Notifications/NotificationItemView.swift
Normal file
86
damus/Views/Notifications/NotificationItemView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
137
damus/Views/Notifications/NotificationsView.swift
Normal file
137
damus/Views/Notifications/NotificationsView.swift
Normal 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)
|
||||
}
|
||||
37
damus/Views/Notifications/ProfilePicturesView.swift
Normal file
37
damus/Views/Notifications/ProfilePicturesView.swift
Normal 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])
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,7 @@ struct ParticipantsView: View {
|
||||
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)
|
||||
@@ -51,9 +51,9 @@ struct ParticipantsView: View {
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 30))
|
||||
.foregroundColor(references.contains(participant) ? .purple : .gray)
|
||||
@@ -71,7 +71,7 @@ struct ParticipantsView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
@@ -10,8 +10,8 @@ import SwiftUI
|
||||
|
||||
let BUTTON_SIZE = 57.0
|
||||
let LINEAR_GRADIENT = LinearGradient(gradient: Gradient(colors: [
|
||||
Color("DamusPurple"),
|
||||
Color("DamusBlue")
|
||||
DamusColors.purple,
|
||||
DamusColors.blue
|
||||
]), startPoint: .topTrailing, endPoint: .bottomTrailing)
|
||||
|
||||
func PostButton(action: @escaping () -> ()) -> some View {
|
||||
|
||||
@@ -15,11 +15,15 @@ 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 attach_camera: 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 +42,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 +63,185 @@ 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 CameraButton: some View {
|
||||
Button(action: {
|
||||
attach_camera = true
|
||||
}, label: {
|
||||
Image(systemName: "camera")
|
||||
})
|
||||
}
|
||||
|
||||
var AttachmentBar: some View {
|
||||
HStack(alignment: .center) {
|
||||
ImageButton
|
||||
CameraButton
|
||||
}
|
||||
.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(media: MediaUpload) {
|
||||
let uploader = get_media_uploader(damus_state.pubkey)
|
||||
|
||||
Task.init {
|
||||
let res = await image_upload.start(media: media, 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, damusState: damus_state) { img in
|
||||
handle_upload(media: .image(img))
|
||||
} onVideoPicked: { url in
|
||||
handle_upload(media: .video(url))
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $attach_camera) {
|
||||
ImagePicker(sourceType: .camera, damusState: damus_state) { img in
|
||||
handle_upload(media: .image(img))
|
||||
} onVideoPicked: { url in
|
||||
handle_upload(media: .video(url))
|
||||
}
|
||||
}
|
||||
.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 +255,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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user