Compare commits
386 Commits
tyiu/trans
...
tyiu/hide-
| Author | SHA1 | Date | |
|---|---|---|---|
|
ec0870b548
|
|||
|
|
2ff12823f2 | ||
|
|
d6d996e84b | ||
|
|
d1fce5054d | ||
|
|
300cd87fc2 | ||
| bff3c0dd52 | |||
|
|
ddd027141a | ||
|
|
9b3e25dd6d | ||
|
|
1ae6a3d871 | ||
|
|
4821ba61a5 | ||
|
|
15849e290e | ||
|
|
6ed562ed24 | ||
| 93580e5296 | |||
|
|
a5c33e4431 | ||
| ad7a79c2bb | |||
|
|
eaec3ae011 | ||
|
|
5a1b966191 | ||
|
|
a320fae2bc | ||
|
|
8e0136a13a | ||
|
|
8f767b03ae | ||
|
|
209b23674d | ||
|
|
86917dbd69 | ||
|
|
59daf555cd | ||
|
|
916c7885f8 | ||
|
|
252763ea77 | ||
|
|
aa610c18b2 | ||
|
|
1de96a9dc5 | ||
|
|
746a4093de | ||
|
|
5cc9288759 | ||
|
|
3bedc83764 | ||
|
|
eb17e07478 | ||
|
|
b96ae84068 | ||
|
|
af7d4d2c53 | ||
|
d93a0600f3
|
|||
|
|
e9e5756c94
|
||
|
|
0b3cc2092f
|
||
|
|
285ab11324
|
||
|
|
3610a76c55
|
||
|
|
4644c57bf3
|
||
|
|
33887982b0
|
||
|
|
c3d3db352e
|
||
|
|
8f08e5c4c8
|
||
|
|
e00e89c16b
|
||
|
|
9151ef02a0
|
||
|
|
709a707942
|
||
|
|
343b7a2bcc
|
||
|
|
6534ba3bde
|
||
|
|
02fc065005 | ||
|
|
668b0a94df | ||
|
|
ebba9d3004 | ||
|
|
8733a34933 | ||
|
|
0de6cfe344 | ||
|
|
a4d40dbfa6 | ||
|
|
05d332eac3 | ||
|
|
b5a3697d78 | ||
|
|
247270f3d3 | ||
|
|
9327068264 | ||
|
|
c277c14bcd | ||
|
|
e688a691fc | ||
|
|
b470af8f1d | ||
|
|
efd1168217 | ||
|
|
f7a3f9ab76 | ||
|
|
95041600dc | ||
|
|
8a8d2ebbc3 | ||
|
|
fad0a6b783 | ||
|
|
f5d7465368 | ||
|
|
8a785559c6 | ||
|
|
d4c8c15cc3 | ||
|
|
41a462871c | ||
|
|
76a669acc2 | ||
|
|
39236dc094 | ||
|
|
5860125802 | ||
|
|
ae96c3b707 | ||
|
|
136f6f37e8 | ||
|
|
21f84f722b | ||
|
|
47747379ee | ||
|
|
a1b95d40e6 | ||
|
|
0fc69d862a | ||
|
fb0330476d
|
|||
|
|
4b978594fa
|
||
|
|
f0bbba7a33 | ||
|
|
b5faae9d1c | ||
|
|
a4d4954abd | ||
|
|
735376b00f | ||
|
|
042e02d2e4 | ||
|
|
40468b1603 | ||
|
|
8c19ec1532 | ||
| 1ac9620242 | |||
|
|
d5ecc9bce4 | ||
|
|
d82b69aac5 | ||
|
|
bad6ba3643 | ||
|
|
5c131e62d7 | ||
|
|
29ab48287f | ||
|
|
5c854519db | ||
|
|
2cc04e24a3 | ||
|
|
d24bea366d | ||
|
|
85ce8cb93c | ||
|
32bb8c365d
|
|||
|
|
d9285ab3ca
|
||
|
3cba771655
|
|||
|
f6f2517fda
|
|||
|
|
047325e6b2
|
||
|
|
ba2108d659
|
||
|
|
863c7baa8b
|
||
|
|
8a5e95e47a
|
||
|
|
de0997216d
|
||
|
|
cc64c82ec4
|
||
|
|
e2ca02399b
|
||
|
|
5418f55cee
|
||
|
|
eb65d473cd
|
||
|
|
dd337c4805
|
||
|
|
2f6ed72f6d
|
||
|
|
d71bb33408
|
||
|
|
72cfb2b071
|
||
|
|
a67cb2df90
|
||
|
|
23e9ce1455 | ||
|
|
5f1132cbc8 | ||
|
|
806c6257df | ||
|
|
18aafb086e | ||
|
|
fc534ea42d | ||
|
|
54c8958250 | ||
|
|
e9f71ed07c | ||
|
|
c719058487 | ||
|
|
ab853c406c | ||
|
|
8a88824677 | ||
|
|
0b3918710a | ||
|
|
1320ff6bec | ||
|
|
10cab37270 | ||
|
|
179da97090 | ||
|
|
2b2d124495 | ||
|
|
2a2af056eb | ||
|
|
f56edd5547 | ||
|
|
b30d0c01db | ||
|
|
e8d63768c1 | ||
|
|
1f648057d5 | ||
|
|
ea14099b62 | ||
|
|
f5942f5123 | ||
|
|
cb8585e4f8 | ||
|
|
526689c742 | ||
|
|
32a9856e2a | ||
|
|
6082a2829f | ||
|
|
b3f6b451bf | ||
|
|
91d51c5e76 | ||
|
|
3262fe806a | ||
|
|
d04e9c9b5f | ||
|
|
015eb5f9fe | ||
|
|
4a525a7581 | ||
|
|
72e14fc3a8 | ||
|
|
af275965ee | ||
|
|
9f5913828a | ||
|
|
c7c21cdee7 | ||
|
|
44a2c4ba7b | ||
|
|
a74aea9d12 | ||
|
|
0866c70346 | ||
|
|
2b34e88a47 | ||
|
|
2aa8d527b9 | ||
|
|
88cbb55953 | ||
|
|
e738c6c1ca | ||
|
|
f4e0c8df5c | ||
|
|
269269d056 | ||
|
|
fd49539615 | ||
|
|
dbb5c19002 | ||
|
|
6961113734 | ||
|
|
4642656ce2 | ||
|
|
0c2132b122 | ||
|
|
a1311a940a | ||
|
|
af18975240 | ||
|
|
f500da03e8 | ||
|
|
8a230861bf | ||
|
|
13354b0eb5 | ||
|
|
c6f4643b5a | ||
|
|
ee34b1c0a3 | ||
|
|
a2cd51b6e7 | ||
|
|
831a409fe6 | ||
|
|
11e22628bb | ||
|
|
6598b5b4bb
|
||
|
|
ca293ef29b
|
||
|
|
8051324d3e
|
||
|
|
a11cf66088
|
||
|
|
a10142d3b9
|
||
|
|
83fcd8600a
|
||
|
|
6283157ef4
|
||
|
|
8b2f45da41
|
||
|
|
ed467a2f79
|
||
|
|
feace9b70d
|
||
|
|
62bccd6b60
|
||
|
|
6f4d12cab4
|
||
|
|
436fe830c7
|
||
|
|
69b6d54b0e
|
||
|
|
13522028ba
|
||
|
|
1459581ec9
|
||
|
|
31aedd2a6e
|
||
|
|
bb1dff13ce
|
||
|
|
3b8c884b30
|
||
|
|
30299fafed
|
||
|
|
46c208c9a5 | ||
|
|
8e78bf9e1a | ||
|
|
82fff4591c | ||
|
|
b8226d674d | ||
| 3f3892ba1d | |||
|
|
1b6224e665 | ||
|
|
782f8d6a69 | ||
|
|
9ad6af0e4f | ||
|
|
e16ea0f4dc | ||
|
|
5b1808d8e7 | ||
|
|
d50b2deb26 | ||
|
|
9de4730e17 | ||
|
|
382265dd39 | ||
|
6c7f8cdbe5
|
|||
|
|
36a92b3795 | ||
|
|
6d4d218c28 | ||
|
|
8679da1275
|
||
|
|
59dcbc130c
|
||
|
|
badd3210e5
|
||
|
|
dd6cc3cf4f
|
||
|
|
36a0ca9c80
|
||
|
|
a7d3b19665
|
||
|
|
226e567987
|
||
|
|
898ffc0186
|
||
|
|
4d0f7b576b
|
||
|
|
230d049384
|
||
|
|
774a4173b0
|
||
|
|
f8b5d91720
|
||
|
|
0832c82ee8
|
||
|
|
4786c6f0cb | ||
|
|
8d0aea22fd | ||
|
|
3d27e49e70 | ||
|
|
e9be227009 | ||
|
|
2e640db012 | ||
|
|
eed16449fe | ||
|
|
3661d64450 | ||
|
|
24c82293b3 | ||
|
|
7fb1bc48c4 | ||
|
|
2ae4a156da | ||
|
|
f700dd799f | ||
|
|
00548adc1f | ||
|
|
7c08d4af45 | ||
|
|
c7bf1da797 | ||
|
|
c5341ba337 | ||
|
|
95fb7bccf8 | ||
|
|
6499738994 | ||
|
|
532eaa35bf | ||
|
9c1bb30f5f
|
|||
|
|
4683b417b9
|
||
|
|
2176b58215
|
||
|
|
3c7627bb9a
|
||
|
|
463ca5ce7b
|
||
|
|
b6a0626890
|
||
|
|
dbfad189cb
|
||
|
|
8159213ef0
|
||
|
|
684841c03a
|
||
|
|
e429bd7df9
|
||
|
|
45ebcf565a
|
||
|
|
c031b08c9a
|
||
|
|
d98503732e
|
||
|
|
bfe0a39370
|
||
|
|
36de599326
|
||
|
|
3cbdfb7ddf
|
||
|
|
ddd309634f
|
||
|
|
6bbf56f80c
|
||
|
|
de1332d6ea
|
||
|
|
648f25ea7a
|
||
|
|
b46dc04ae8
|
||
|
|
8cabfaf90e
|
||
|
|
0b02956bb7
|
||
|
|
4abea02d6e
|
||
|
|
5d20c49f7d
|
||
|
|
c11e0fa919
|
||
|
|
54bfeb04da
|
||
|
|
214eec9cb6
|
||
|
|
b48de9525e
|
||
|
|
690f3ec740
|
||
|
6df4ffc1f0
|
|||
|
|
83cead1fbb
|
||
|
|
478600cf0e
|
||
|
|
75002802fa
|
||
|
|
faf072d432
|
||
|
|
e888da26a7
|
||
|
|
b532dc48e1 | ||
|
|
7bf18336d0 | ||
|
|
b131c74ee3 | ||
|
|
b9fed6e4eb | ||
|
|
6910e84fd8 | ||
|
|
5f0c1a59e6 | ||
|
|
d8a9efc6e2 | ||
|
|
185732a633 | ||
|
|
b585e8c21c | ||
|
|
b14858b4b1 | ||
|
|
f236ea52e1 | ||
|
|
9c8391b33b | ||
|
|
89b2382ad7 | ||
|
|
2ce0a771ea | ||
|
|
0436c68ec5 | ||
|
|
88740a2bc7 | ||
|
|
e3f578e48a | ||
|
|
39788bd590 | ||
|
|
8c2c8e283f | ||
|
|
8a473885c7 | ||
|
|
ba1c2cd2b9 | ||
|
|
bedf7e0648 | ||
|
|
14802e334d | ||
|
|
98d11fb71e | ||
|
|
9ca959d8d3 | ||
|
|
584a20ade1 | ||
|
|
67041b22f4 | ||
|
|
825a2f944f | ||
|
|
1aa70efee0 | ||
|
|
e2812a9aa1 | ||
|
|
679779ab3e | ||
|
|
98a630248e | ||
|
2f4e33fc9f
|
|||
|
|
ddf60e78b9 | ||
|
|
08badfc746
|
||
|
|
6dfc39e1ed
|
||
|
|
6655b6ac9d
|
||
|
|
c81c14a761
|
||
|
|
3da12e708f | ||
|
|
3d97dc593e | ||
|
|
c6e5193111 | ||
|
|
61ef709d91 | ||
|
|
f6133814e8 | ||
|
|
1701dbdfb9 | ||
|
|
8fe8611527 | ||
|
|
41f692a0c4 | ||
|
|
e0d4841147 | ||
|
|
1bf171a09e | ||
|
|
c5f3a8e509 | ||
|
|
02c973fbb2 | ||
|
|
4996272942 | ||
|
|
094f63bcff | ||
|
|
f9339440f2 | ||
|
|
997bc38885 | ||
|
|
b9d62e300b | ||
|
|
1282b8f5f6 | ||
|
|
127f1e07e3 | ||
|
|
cc190c3618 | ||
|
|
c9c51c6d4a | ||
|
|
b63159a29f | ||
|
|
647c6f8428 | ||
|
|
c8f5a2cffc | ||
|
|
65e22ea0dc
|
||
|
|
e6ce0ef8a3
|
||
|
|
5c949ea0c2
|
||
|
|
4881d41831
|
||
|
|
af7177c436
|
||
|
|
26cf0380c0
|
||
|
|
499e3f6fd4
|
||
|
|
82b237754a
|
||
|
|
0608222cb0 | ||
|
|
b83204898b | ||
|
|
0dd74fde7f | ||
|
|
7a55ea13e3 | ||
|
|
2815827b9f | ||
|
|
4ac3e71039 | ||
|
|
2596542cb6 | ||
|
|
915f3901a7 | ||
|
|
39f39c7382 | ||
|
|
f2ce146e98 | ||
|
|
9ba3543d91 | ||
|
|
dec5de83b9 | ||
|
|
ab6ec18e35 | ||
| 35ee2f5744 | |||
|
|
42e6281e9b | ||
| ae82114a33 | |||
|
|
87d1cd9095 | ||
|
|
2d679bf087 | ||
|
|
aaa6eb013a | ||
|
|
fce689f0ef | ||
|
|
89f33be4cf | ||
|
|
96b6929235 | ||
|
|
a811a12da3 | ||
|
|
8edb1f289d | ||
|
|
cdbd8f2722 | ||
|
|
069a8f52ca | ||
|
|
7664985768 | ||
|
|
a77b0e1bbc | ||
|
|
d8e516841e | ||
|
|
dc33ad962d | ||
|
|
f7eae9275f | ||
|
|
e5adbb2fee | ||
|
|
88d26dc4c2 | ||
|
|
4d71297941 | ||
|
|
21ba936c72 | ||
|
|
04fa8f1805 | ||
|
|
3076e6ee7f | ||
|
08b7e50bd8
|
196
CHANGELOG.md
196
CHANGELOG.md
@@ -1,3 +1,198 @@
|
||||
## [1.4.3-2] - 2023-04-17
|
||||
|
||||
### Added
|
||||
|
||||
- Add deep links for local notifications (Swift)
|
||||
- Add thread muting (Terry Yiu)
|
||||
- Preview media uploads when posting (Swift)
|
||||
- Add QR Code in profiles (ericholguin)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Always check signatures of profile events (William Casarin)
|
||||
- Ask permission before uploading media (Swift)
|
||||
- Show DM message in local notification (William Casarin)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed repost turning green too early and not reposting sometimes (Swift)
|
||||
- Fix shuffling when choosing users to reply to (Joshua Jiang)
|
||||
- Do not translate own notes if logged in with private key (Terry Yiu)
|
||||
- Load missing profiles from boosts on home view (Gísli Kristjánsson)
|
||||
- Load missing profiles from boosts on profile view (Gísli Kristjánsson)
|
||||
- Fix tap area when mentioning users (OlegAba)
|
||||
- Fix invalid DM author notifications (William Casarin)
|
||||
- Fix relay signal indicator, properly show how many relays you are connected to (William Casarin)
|
||||
|
||||
|
||||
[1.4.3-2]: https://github.com/damus-io/damus/releases/tag/v1.4.3-2
|
||||
|
||||
## [1.4.2-2] - 2023-04-12
|
||||
|
||||
### Added
|
||||
|
||||
- Include #btc in custom #bitcoin hashtag (William Casarin)
|
||||
- Make notification dots configurable (William Casarin)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Display follows in most recent to oldest (Luis Cabrera)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix hitches caused by syncronous loading of cached images (William Casarin)
|
||||
- Fix tabs sometimes not switching (William Casarin)
|
||||
|
||||
|
||||
[1.4.2-2]: https://github.com/damus-io/damus/releases/tag/v1.4.2-2
|
||||
|
||||
## [1.4.1-8] - 2023-04-10
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for nostr: bech32 urls in posts and DMs (NIP19) (Bartholomew Joyce)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Don't leak mentions in DMs (William Casarin)
|
||||
- Fix tap area when mentioning users (OlegAba)
|
||||
|
||||
[1.4.1-8]: https://github.com/damus-io/damus/releases/tag/v1.4.1-8
|
||||
## [1.4.1-7] - 2023-04-07
|
||||
|
||||
### Added
|
||||
|
||||
- Add #zap and #zapathon custom hashtags (William Casarin)
|
||||
- Add custom #plebchain icon (William Casarin)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Add validation to prevent whitespaces be inputted on NIP-05 input field (Terry Yiu)
|
||||
- Change reply color from blue to purple. Blue is banned from Damus. (William Casarin)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix padding in post view (OlegAba)
|
||||
- Show most recently bookmarked notes at the top (Bryan Montz)
|
||||
|
||||
|
||||
[1.4.1-7]: https://github.com/damus-io/damus/releases/tag/v1.4.1-7
|
||||
|
||||
## [1.4.1-6] - 2023-04-06
|
||||
|
||||
### Added
|
||||
|
||||
- Custom hashtags for #bitcoin, #nostr and #coffeechain (William Casarin)
|
||||
|
||||
### Changed
|
||||
|
||||
- Disable translations in DMs by default (William Casarin)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Don't show Translating... if we're not actually translating (William Casarin)
|
||||
|
||||
|
||||
[1.4.1-6]: https://github.com/damus-io/damus/releases/tag/v1.4.1-6
|
||||
|
||||
## [1.4.1-4] - 2023-04-06
|
||||
|
||||
### Added
|
||||
|
||||
- Cache translations (William Casarin)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix translation text popping (William Casarin)
|
||||
- Fix broken auto-translations (William Casarin)
|
||||
- Fix extraneous padding on some image posts (William Casarin)
|
||||
- Fix crash in relay list view (William Casarin)
|
||||
|
||||
[1.4.1-4]: https://github.com/damus-io/damus/releases/tag/v1.4.1-4
|
||||
|
||||
## [1.4.1-3] - 2023-04-05
|
||||
|
||||
### Added
|
||||
|
||||
- Added text truncation settings (William Casarin)
|
||||
|
||||
### Changed
|
||||
|
||||
- Rename block to mute (William Casarin)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Reduce chopping of images (mainvolume)
|
||||
- Fix some notification settings not saving (William Casarin)
|
||||
- Fix broken camera uploads (again) (Joel Klabo)
|
||||
|
||||
|
||||
[1.4.1-3]: https://github.com/damus-io/damus/releases/tag/v1.4.1-3
|
||||
|
||||
## [1.4.1-2] - 2023-04-04
|
||||
|
||||
### Added
|
||||
|
||||
- Reply counts (William Casarin)
|
||||
- Add option to only show notification from people you follow (Swift)
|
||||
- Added local notifications for other events (Swift)
|
||||
- Show a custom view when tagged user isn't found (ericholguin)
|
||||
- Show referenced notes in DMs (William Casarin)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Show full bleed images on selected events in threads (William Casarin)
|
||||
- Improvement to square image displaying (mainvolume)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix broken website links that have missing https:// prefixes (William Casarin)
|
||||
- Get around CCP bootstrap relay banning by caching user's relays as their bootstrap relays (William Casarin)
|
||||
|
||||
|
||||
[1.4.1-2]: https://github.com/damus-io/damus/releases/tag/v1.4.1-2
|
||||
|
||||
## [1.4.1] - 2023-04-03
|
||||
|
||||
### Added
|
||||
|
||||
- Profile Picture Upload (Joel Klabo)
|
||||
- Enable offline posting (William Casarin)
|
||||
- Add auto-translation caching to ruduce api usage (Terry Yiu)
|
||||
- Added support for gif uploads (Swift)
|
||||
- Add a Divider in the Follows List for Large Screens (Joel Klabo)
|
||||
- Upload Photos and Videos from Camera (Joel Klabo)
|
||||
- Added ability to lookup users by nip05 identifiers (William Casarin)
|
||||
|
||||
### Changed
|
||||
|
||||
- Only truncate timeline text if enabled in settings (William Casarin)
|
||||
- Make mentions wide in notifications like in timeline (William Casarin)
|
||||
- Broadcast events you are replying to (William Casarin)
|
||||
- Broadcast now also broadcasts event user's profile (William Casarin)
|
||||
- Improved look of reply view (ericholguin)
|
||||
- Remove gradient in some places for visibility (ericholguin)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix cropped images (mainvolume)
|
||||
- Truncate long text in notification items (William Casarin)
|
||||
- Restore missing reply description on selected events (William Casarin)
|
||||
- Show sent DMs immediately (William Casarin)
|
||||
- Fixed size of translated text (William Casarin)
|
||||
- Fix crash when reposting (William Casarin)
|
||||
- Fix unclickable image dismiss button (OlegAba)
|
||||
|
||||
|
||||
[1.4.1]: https://github.com/damus-io/damus/releases/tag/v1.4.1
|
||||
## [1.4.0] - 2023-03-27
|
||||
|
||||
### Added
|
||||
@@ -844,4 +1039,3 @@
|
||||
|
||||
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2
|
||||
|
||||
|
||||
|
||||
@@ -91,13 +91,12 @@ int bech32_encode(char *output, const char *hrp, const uint8_t *data, size_t dat
|
||||
return 1;
|
||||
}
|
||||
|
||||
bech32_encoding bech32_decode(char* hrp, uint8_t *data, size_t *data_len, const char *input, size_t max_input_len) {
|
||||
bech32_encoding bech32_decode_len(char* hrp, uint8_t *data, size_t *data_len, const char *input, size_t input_len) {
|
||||
uint32_t chk = 1;
|
||||
size_t i;
|
||||
size_t input_len = strlen(input);
|
||||
size_t hrp_len;
|
||||
int have_lower = 0, have_upper = 0;
|
||||
if (input_len < 8 || input_len > max_input_len) {
|
||||
if (input_len < 8) {
|
||||
return BECH32_ENCODING_NONE;
|
||||
}
|
||||
*data_len = 0;
|
||||
@@ -154,6 +153,14 @@ bech32_encoding bech32_decode(char* hrp, uint8_t *data, size_t *data_len, const
|
||||
}
|
||||
}
|
||||
|
||||
bech32_encoding bech32_decode(char* hrp, uint8_t *data, size_t *data_len, const char *input, size_t max_input_len) {
|
||||
size_t len = strlen(input);
|
||||
if (len > max_input_len) {
|
||||
return BECH32_ENCODING_NONE;
|
||||
}
|
||||
return bech32_decode_len(hrp, data, data_len, input, len);
|
||||
}
|
||||
|
||||
int bech32_convert_bits(uint8_t* out, size_t* outlen, int outbits, const uint8_t* in, size_t inlen, int inbits, int pad) {
|
||||
uint32_t val = 0;
|
||||
int bits = 0;
|
||||
|
||||
@@ -118,6 +118,14 @@ bech32_encoding bech32_decode(
|
||||
size_t max_input_len
|
||||
);
|
||||
|
||||
bech32_encoding bech32_decode_len(
|
||||
char *hrp,
|
||||
uint8_t *data,
|
||||
size_t *data_len,
|
||||
const char *input,
|
||||
size_t input_len
|
||||
);
|
||||
|
||||
/* Helper from bech32: translates inbits-bit bytes to outbits-bit bytes.
|
||||
* @outlen is incremented as bytes are added.
|
||||
* @pad is true if we're to pad, otherwise truncate last byte if necessary
|
||||
|
||||
56
damus-c/block.h
Normal file
56
damus-c/block.h
Normal file
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// block.h
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-09.
|
||||
//
|
||||
|
||||
#ifndef block_h
|
||||
#define block_h
|
||||
|
||||
#include "nostr_bech32.h"
|
||||
#include "str_block.h"
|
||||
|
||||
#define MAX_BLOCKS 1024
|
||||
|
||||
enum block_type {
|
||||
BLOCK_HASHTAG = 1,
|
||||
BLOCK_TEXT = 2,
|
||||
BLOCK_MENTION_INDEX = 3,
|
||||
BLOCK_MENTION_BECH32 = 4,
|
||||
BLOCK_URL = 5,
|
||||
BLOCK_INVOICE = 6,
|
||||
};
|
||||
|
||||
|
||||
typedef struct invoice_block {
|
||||
struct str_block invstr;
|
||||
union {
|
||||
struct bolt11 *bolt11;
|
||||
};
|
||||
} invoice_block_t;
|
||||
|
||||
typedef struct mention_bech32_block {
|
||||
struct str_block str;
|
||||
struct nostr_bech32 bech32;
|
||||
} mention_bech32_block_t;
|
||||
|
||||
typedef struct block {
|
||||
enum block_type type;
|
||||
union {
|
||||
struct str_block str;
|
||||
struct invoice_block invoice;
|
||||
struct mention_bech32_block mention_bech32;
|
||||
int mention_index;
|
||||
} block;
|
||||
} block_t;
|
||||
|
||||
typedef struct blocks {
|
||||
int num_blocks;
|
||||
struct block *blocks;
|
||||
} blocks_t;
|
||||
|
||||
void blocks_init(struct blocks *blocks);
|
||||
void blocks_free(struct blocks *blocks);
|
||||
|
||||
#endif /* block_h */
|
||||
171
damus-c/cursor.h
Normal file
171
damus-c/cursor.h
Normal file
@@ -0,0 +1,171 @@
|
||||
//
|
||||
// cursor.h
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-09.
|
||||
//
|
||||
|
||||
#ifndef cursor_h
|
||||
#define cursor_h
|
||||
|
||||
#include <ctype.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef unsigned char u8;
|
||||
|
||||
struct cursor {
|
||||
const u8 *p;
|
||||
const u8 *start;
|
||||
const u8 *end;
|
||||
};
|
||||
|
||||
static inline int is_whitespace(char c) {
|
||||
return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
|
||||
}
|
||||
|
||||
static inline int is_boundary(char c) {
|
||||
return !isalnum(c);
|
||||
}
|
||||
|
||||
static inline int is_invalid_url_ending(char c) {
|
||||
return c == '!' || c == '?' || c == ')' || c == '.' || c == ',' || c == ';';
|
||||
}
|
||||
|
||||
static inline int is_alphanumeric(char c) {
|
||||
return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
|
||||
}
|
||||
|
||||
static inline void make_cursor(struct cursor *c, const u8 *content, size_t len)
|
||||
{
|
||||
c->start = content;
|
||||
c->end = content + len;
|
||||
c->p = content;
|
||||
}
|
||||
|
||||
static inline int consume_until_boundary(struct cursor *cur) {
|
||||
char c;
|
||||
|
||||
while (cur->p < cur->end) {
|
||||
c = *cur->p;
|
||||
|
||||
if (is_boundary(c))
|
||||
return 1;
|
||||
|
||||
cur->p++;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int consume_until_whitespace(struct cursor *cur, int or_end) {
|
||||
char c;
|
||||
int consumedAtLeastOne = 0;
|
||||
|
||||
while (cur->p < cur->end) {
|
||||
c = *cur->p;
|
||||
|
||||
if (is_whitespace(c))
|
||||
return consumedAtLeastOne;
|
||||
|
||||
cur->p++;
|
||||
consumedAtLeastOne = 1;
|
||||
}
|
||||
|
||||
return or_end;
|
||||
}
|
||||
|
||||
static inline int consume_until_non_alphanumeric(struct cursor *cur, int or_end) {
|
||||
char c;
|
||||
int consumedAtLeastOne = 0;
|
||||
|
||||
while (cur->p < cur->end) {
|
||||
c = *cur->p;
|
||||
|
||||
if (!is_alphanumeric(c))
|
||||
return consumedAtLeastOne;
|
||||
|
||||
cur->p++;
|
||||
consumedAtLeastOne = 1;
|
||||
}
|
||||
|
||||
return or_end;
|
||||
}
|
||||
|
||||
static inline int parse_char(struct cursor *cur, char c) {
|
||||
if (cur->p >= cur->end)
|
||||
return 0;
|
||||
|
||||
if (*cur->p == c) {
|
||||
cur->p++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline int peek_char(struct cursor *cur, int ind) {
|
||||
if ((cur->p + ind < cur->start) || (cur->p + ind >= cur->end))
|
||||
return -1;
|
||||
|
||||
return *(cur->p + ind);
|
||||
}
|
||||
|
||||
static int parse_digit(struct cursor *cur, int *digit) {
|
||||
int c;
|
||||
if ((c = peek_char(cur, 0)) == -1)
|
||||
return 0;
|
||||
|
||||
c -= '0';
|
||||
|
||||
if (c >= 0 && c <= 9) {
|
||||
*digit = c;
|
||||
cur->p++;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static inline int pull_byte(struct cursor *cur, u8 *byte) {
|
||||
if (cur->p >= cur->end)
|
||||
return 0;
|
||||
|
||||
*byte = *cur->p;
|
||||
cur->p++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int pull_bytes(struct cursor *cur, int count, const u8 **bytes) {
|
||||
if (cur->p + count > cur->end)
|
||||
return 0;
|
||||
|
||||
*bytes = cur->p;
|
||||
cur->p += count;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int parse_str(struct cursor *cur, const char *str) {
|
||||
int i;
|
||||
char c, cs;
|
||||
unsigned long len;
|
||||
|
||||
len = strlen(str);
|
||||
|
||||
if (cur->p + len >= cur->end)
|
||||
return 0;
|
||||
|
||||
for (i = 0; i < len; i++) {
|
||||
c = tolower(cur->p[i]);
|
||||
cs = tolower(str[i]);
|
||||
|
||||
if (c != cs)
|
||||
return 0;
|
||||
}
|
||||
|
||||
cur->p += len;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
#endif /* cursor_h */
|
||||
166
damus-c/damus.c
166
damus-c/damus.c
@@ -6,127 +6,13 @@
|
||||
//
|
||||
|
||||
#include "damus.h"
|
||||
#include "cursor.h"
|
||||
#include "bolt11.h"
|
||||
#include "bech32.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef unsigned char u8;
|
||||
|
||||
struct cursor {
|
||||
const u8 *p;
|
||||
const u8 *start;
|
||||
const u8 *end;
|
||||
};
|
||||
|
||||
static inline int is_whitespace(char c) {
|
||||
return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
|
||||
}
|
||||
|
||||
static inline int is_boundary(char c) {
|
||||
return !isalnum(c);
|
||||
}
|
||||
|
||||
static inline int is_invalid_url_ending(char c) {
|
||||
return c == '!' || c == '?' || c == ')' || c == '.' || c == ',' || c == ';';
|
||||
}
|
||||
|
||||
static void make_cursor(struct cursor *c, const u8 *content, size_t len)
|
||||
{
|
||||
c->start = content;
|
||||
c->end = content + len;
|
||||
c->p = content;
|
||||
}
|
||||
|
||||
static int consume_until_boundary(struct cursor *cur) {
|
||||
char c;
|
||||
|
||||
while (cur->p < cur->end) {
|
||||
c = *cur->p;
|
||||
|
||||
if (is_boundary(c))
|
||||
return 1;
|
||||
|
||||
cur->p++;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int consume_until_whitespace(struct cursor *cur, int or_end) {
|
||||
char c;
|
||||
bool consumedAtLeastOne = false;
|
||||
|
||||
while (cur->p < cur->end) {
|
||||
c = *cur->p;
|
||||
|
||||
if (is_whitespace(c))
|
||||
return consumedAtLeastOne;
|
||||
|
||||
cur->p++;
|
||||
consumedAtLeastOne = true;
|
||||
}
|
||||
|
||||
return or_end;
|
||||
}
|
||||
|
||||
static int parse_char(struct cursor *cur, char c) {
|
||||
if (cur->p >= cur->end)
|
||||
return 0;
|
||||
|
||||
if (*cur->p == c) {
|
||||
cur->p++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static inline int peek_char(struct cursor *cur, int ind) {
|
||||
if ((cur->p + ind < cur->start) || (cur->p + ind >= cur->end))
|
||||
return -1;
|
||||
|
||||
return *(cur->p + ind);
|
||||
}
|
||||
|
||||
static int parse_digit(struct cursor *cur, int *digit) {
|
||||
int c;
|
||||
if ((c = peek_char(cur, 0)) == -1)
|
||||
return 0;
|
||||
|
||||
c -= '0';
|
||||
|
||||
if (c >= 0 && c <= 9) {
|
||||
*digit = c;
|
||||
cur->p++;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_str(struct cursor *cur, const char *str) {
|
||||
int i;
|
||||
char c, cs;
|
||||
unsigned long len;
|
||||
|
||||
len = strlen(str);
|
||||
|
||||
if (cur->p + len >= cur->end)
|
||||
return 0;
|
||||
|
||||
for (i = 0; i < len; i++) {
|
||||
c = tolower(cur->p[i]);
|
||||
cs = tolower(str[i]);
|
||||
|
||||
if (c != cs)
|
||||
return 0;
|
||||
}
|
||||
|
||||
cur->p += len;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int parse_mention(struct cursor *cur, struct block *block) {
|
||||
static int parse_mention_index(struct cursor *cur, struct block *block) {
|
||||
int d1, d2, d3, ind;
|
||||
const u8 *start = cur->p;
|
||||
|
||||
@@ -151,8 +37,8 @@ static int parse_mention(struct cursor *cur, struct block *block) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
block->type = BLOCK_MENTION;
|
||||
block->block.mention = ind;
|
||||
block->type = BLOCK_MENTION_INDEX;
|
||||
block->block.mention_index = ind;
|
||||
|
||||
return 1;
|
||||
}
|
||||
@@ -274,6 +160,27 @@ static int parse_invoice(struct cursor *cur, struct block *block) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
static int parse_mention_bech32(struct cursor *cur, struct block *block) {
|
||||
const u8 *start = cur->p;
|
||||
|
||||
if (!parse_str(cur, "nostr:"))
|
||||
return 0;
|
||||
|
||||
block->block.str.start = (const char *)cur->p;
|
||||
|
||||
if (!parse_nostr_bech32(cur, &block->block.mention_bech32.bech32)) {
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
|
||||
block->block.str.end = (const char *)cur->p;
|
||||
|
||||
block->type = BLOCK_MENTION_BECH32;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int add_text_then_block(struct cursor *cur, struct blocks *blocks, struct block block, const u8 **start, const u8 *pre_mention)
|
||||
{
|
||||
if (!add_text_block(blocks, *start, pre_mention))
|
||||
@@ -303,7 +210,7 @@ int damus_parse_content(struct blocks *blocks, const char *content) {
|
||||
|
||||
pre_mention = cur.p;
|
||||
if (cp == -1 || is_whitespace(cp)) {
|
||||
if (c == '#' && (parse_mention(&cur, &block) || parse_hashtag(&cur, &block))) {
|
||||
if (c == '#' && (parse_mention_index(&cur, &block) || parse_hashtag(&cur, &block))) {
|
||||
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
|
||||
return 0;
|
||||
continue;
|
||||
@@ -315,6 +222,10 @@ int damus_parse_content(struct blocks *blocks, const char *content) {
|
||||
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
|
||||
return 0;
|
||||
continue;
|
||||
} else if (c == 'n' && parse_mention_bech32(&cur, &block)) {
|
||||
if (!add_text_then_block(&cur, blocks, block, &start, pre_mention))
|
||||
return 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,8 +246,17 @@ void blocks_init(struct blocks *blocks) {
|
||||
}
|
||||
|
||||
void blocks_free(struct blocks *blocks) {
|
||||
if (blocks->blocks) {
|
||||
free(blocks->blocks);
|
||||
blocks->num_blocks = 0;
|
||||
if (!blocks->blocks) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < blocks->num_blocks; ++i) {
|
||||
if (blocks->blocks[i].type == BLOCK_MENTION_BECH32) {
|
||||
free(blocks->blocks[i].block.mention_bech32.bech32.buffer);
|
||||
blocks->blocks[i].block.mention_bech32.bech32.buffer = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
free(blocks->blocks);
|
||||
blocks->num_blocks = 0;
|
||||
}
|
||||
|
||||
@@ -9,45 +9,10 @@
|
||||
#define damus_h
|
||||
|
||||
#include <stdio.h>
|
||||
#include "nostr_bech32.h"
|
||||
#include "block.h"
|
||||
typedef unsigned char u8;
|
||||
|
||||
#define MAX_BLOCKS 1024
|
||||
|
||||
enum block_type {
|
||||
BLOCK_HASHTAG = 1,
|
||||
BLOCK_TEXT = 2,
|
||||
BLOCK_MENTION = 3,
|
||||
BLOCK_URL = 4,
|
||||
BLOCK_INVOICE = 5,
|
||||
};
|
||||
|
||||
typedef struct str_block {
|
||||
const char *start;
|
||||
const char *end;
|
||||
} str_block_t;
|
||||
|
||||
typedef struct invoice_block {
|
||||
struct str_block invstr;
|
||||
union {
|
||||
struct bolt11 *bolt11;
|
||||
};
|
||||
} invoice_block_t;
|
||||
|
||||
typedef struct block {
|
||||
enum block_type type;
|
||||
union {
|
||||
struct str_block str;
|
||||
struct invoice_block invoice;
|
||||
int mention;
|
||||
} block;
|
||||
} block_t;
|
||||
|
||||
typedef struct blocks {
|
||||
int num_blocks;
|
||||
struct block *blocks;
|
||||
} blocks_t;
|
||||
|
||||
void blocks_init(struct blocks *blocks);
|
||||
void blocks_free(struct blocks *blocks);
|
||||
int damus_parse_content(struct blocks *blocks, const char *content);
|
||||
|
||||
#endif /* damus_h */
|
||||
|
||||
295
damus-c/nostr_bech32.c
Normal file
295
damus-c/nostr_bech32.c
Normal file
@@ -0,0 +1,295 @@
|
||||
//
|
||||
// nostr_bech32.c
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-09.
|
||||
//
|
||||
|
||||
#include "nostr_bech32.h"
|
||||
#include <stdlib.h>
|
||||
#include "cursor.h"
|
||||
#include "bech32.h"
|
||||
|
||||
#define MAX_TLVS 16
|
||||
|
||||
#define TLV_SPECIAL 0
|
||||
#define TLV_RELAY 1
|
||||
#define TLV_AUTHOR 2
|
||||
#define TLV_KIND 3
|
||||
#define TLV_KNOWN_TLVS 4
|
||||
|
||||
struct nostr_tlv {
|
||||
u8 type;
|
||||
u8 len;
|
||||
const u8 *value;
|
||||
};
|
||||
|
||||
struct nostr_tlvs {
|
||||
struct nostr_tlv tlvs[MAX_TLVS];
|
||||
int num_tlvs;
|
||||
};
|
||||
|
||||
static int parse_nostr_tlv(struct cursor *cur, struct nostr_tlv *tlv) {
|
||||
// get the tlv tag
|
||||
if (!pull_byte(cur, &tlv->type))
|
||||
return 0;
|
||||
|
||||
// unknown, fail!
|
||||
if (tlv->type >= TLV_KNOWN_TLVS)
|
||||
return 0;
|
||||
|
||||
// get the length
|
||||
if (!pull_byte(cur, &tlv->len))
|
||||
return 0;
|
||||
|
||||
// is the reported length greater then our buffer? if so fail
|
||||
if (cur->p + tlv->len > cur->end)
|
||||
return 0;
|
||||
|
||||
tlv->value = cur->p;
|
||||
cur->p += tlv->len;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int parse_nostr_tlvs(struct cursor *cur, struct nostr_tlvs *tlvs) {
|
||||
int i;
|
||||
tlvs->num_tlvs = 0;
|
||||
|
||||
for (i = 0; i < MAX_TLVS; i++) {
|
||||
if (parse_nostr_tlv(cur, &tlvs->tlvs[i])) {
|
||||
tlvs->num_tlvs++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (tlvs->num_tlvs == 0)
|
||||
return 0;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int find_tlv(struct nostr_tlvs *tlvs, u8 type, struct nostr_tlv **tlv) {
|
||||
*tlv = NULL;
|
||||
|
||||
for (int i = 0; i < tlvs->num_tlvs; i++) {
|
||||
if (tlvs->tlvs[i].type == type) {
|
||||
*tlv = &tlvs->tlvs[i];
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_nostr_bech32_type(const char *prefix, enum nostr_bech32_type *type) {
|
||||
// Parse type
|
||||
if (strcmp(prefix, "note") == 0) {
|
||||
*type = NOSTR_BECH32_NOTE;
|
||||
return 1;
|
||||
} else if (strcmp(prefix, "npub") == 0) {
|
||||
*type = NOSTR_BECH32_NPUB;
|
||||
return 1;
|
||||
} else if (strcmp(prefix, "nprofile") == 0) {
|
||||
*type = NOSTR_BECH32_NPROFILE;
|
||||
return 1;
|
||||
} else if (strcmp(prefix, "nevent") == 0) {
|
||||
*type = NOSTR_BECH32_NEVENT;
|
||||
return 1;
|
||||
} else if (strcmp(prefix, "nrelay") == 0) {
|
||||
*type = NOSTR_BECH32_NRELAY;
|
||||
return 1;
|
||||
} else if (strcmp(prefix, "naddr") == 0) {
|
||||
*type = NOSTR_BECH32_NADDR;
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int parse_nostr_bech32_note(struct cursor *cur, struct bech32_note *note) {
|
||||
return pull_bytes(cur, 32, ¬e->event_id);
|
||||
}
|
||||
|
||||
static int parse_nostr_bech32_npub(struct cursor *cur, struct bech32_npub *npub) {
|
||||
return pull_bytes(cur, 32, &npub->pubkey);
|
||||
}
|
||||
|
||||
static int tlvs_to_relays(struct nostr_tlvs *tlvs, struct relays *relays) {
|
||||
struct nostr_tlv *tlv;
|
||||
struct str_block *str;
|
||||
|
||||
relays->num_relays = 0;
|
||||
|
||||
for (int i = 0; i < tlvs->num_tlvs; i++) {
|
||||
tlv = &tlvs->tlvs[i];
|
||||
if (tlv->type != TLV_RELAY)
|
||||
continue;
|
||||
|
||||
if (relays->num_relays + 1 > MAX_RELAYS)
|
||||
break;
|
||||
|
||||
str = &relays->relays[relays->num_relays++];
|
||||
str->start = (const char*)tlv->value;
|
||||
str->end = (const char*)(tlv->value + tlv->len);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int parse_nostr_bech32_nevent(struct cursor *cur, struct bech32_nevent *nevent) {
|
||||
struct nostr_tlvs tlvs;
|
||||
struct nostr_tlv *tlv;
|
||||
|
||||
if (!parse_nostr_tlvs(cur, &tlvs))
|
||||
return 0;
|
||||
|
||||
if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
|
||||
return 0;
|
||||
|
||||
if (tlv->len != 32)
|
||||
return 0;
|
||||
|
||||
nevent->event_id = tlv->value;
|
||||
|
||||
if (find_tlv(&tlvs, TLV_AUTHOR, &tlv)) {
|
||||
nevent->pubkey = tlv->value;
|
||||
} else {
|
||||
nevent->pubkey = NULL;
|
||||
}
|
||||
|
||||
return tlvs_to_relays(&tlvs, &nevent->relays);
|
||||
}
|
||||
|
||||
static int parse_nostr_bech32_naddr(struct cursor *cur, struct bech32_naddr *naddr) {
|
||||
struct nostr_tlvs tlvs;
|
||||
struct nostr_tlv *tlv;
|
||||
|
||||
if (!parse_nostr_tlvs(cur, &tlvs))
|
||||
return 0;
|
||||
|
||||
if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
|
||||
return 0;
|
||||
|
||||
naddr->identifier.start = (const char*)tlv->value;
|
||||
naddr->identifier.end = (const char*)tlv->value + tlv->len;
|
||||
|
||||
if (!find_tlv(&tlvs, TLV_AUTHOR, &tlv))
|
||||
return 0;
|
||||
|
||||
naddr->pubkey = tlv->value;
|
||||
|
||||
return tlvs_to_relays(&tlvs, &naddr->relays);
|
||||
}
|
||||
|
||||
static int parse_nostr_bech32_nprofile(struct cursor *cur, struct bech32_nprofile *nprofile) {
|
||||
struct nostr_tlvs tlvs;
|
||||
struct nostr_tlv *tlv;
|
||||
|
||||
if (!parse_nostr_tlvs(cur, &tlvs))
|
||||
return 0;
|
||||
|
||||
if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
|
||||
return 0;
|
||||
|
||||
if (tlv->len != 32)
|
||||
return 0;
|
||||
|
||||
nprofile->pubkey = tlv->value;
|
||||
|
||||
return tlvs_to_relays(&tlvs, &nprofile->relays);
|
||||
}
|
||||
|
||||
static int parse_nostr_bech32_nrelay(struct cursor *cur, struct bech32_nrelay *nrelay) {
|
||||
struct nostr_tlvs tlvs;
|
||||
struct nostr_tlv *tlv;
|
||||
|
||||
if (!parse_nostr_tlvs(cur, &tlvs))
|
||||
return 0;
|
||||
|
||||
if (!find_tlv(&tlvs, TLV_SPECIAL, &tlv))
|
||||
return 0;
|
||||
|
||||
nrelay->relay.start = (const char*)tlv->value;
|
||||
nrelay->relay.end = (const char*)tlv->value + tlv->len;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
int parse_nostr_bech32(struct cursor *cur, struct nostr_bech32 *obj) {
|
||||
const u8 *start, *end;
|
||||
|
||||
start = cur->p;
|
||||
|
||||
if (!consume_until_non_alphanumeric(cur, 1)) {
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
|
||||
end = cur->p;
|
||||
|
||||
size_t data_len;
|
||||
size_t input_len = end - start;
|
||||
if (input_len < 10 || input_len > 10000) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
obj->buffer = malloc(input_len * 2);
|
||||
if (!obj->buffer)
|
||||
return 0;
|
||||
|
||||
u8 data[input_len];
|
||||
char prefix[input_len];
|
||||
|
||||
if (bech32_decode_len(prefix, data, &data_len, (const char*)start, input_len) == BECH32_ENCODING_NONE) {
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
|
||||
obj->buflen = 0;
|
||||
if (!bech32_convert_bits(obj->buffer, &obj->buflen, 8, data, data_len, 5, 0)) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (!parse_nostr_bech32_type(prefix, &obj->type)) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
struct cursor bcur;
|
||||
make_cursor(&bcur, obj->buffer, obj->buflen);
|
||||
|
||||
switch (obj->type) {
|
||||
case NOSTR_BECH32_NOTE:
|
||||
if (!parse_nostr_bech32_note(&bcur, &obj->data.note))
|
||||
goto fail;
|
||||
break;
|
||||
case NOSTR_BECH32_NPUB:
|
||||
if (!parse_nostr_bech32_npub(&bcur, &obj->data.npub))
|
||||
goto fail;
|
||||
break;
|
||||
case NOSTR_BECH32_NEVENT:
|
||||
if (!parse_nostr_bech32_nevent(&bcur, &obj->data.nevent))
|
||||
goto fail;
|
||||
break;
|
||||
case NOSTR_BECH32_NADDR:
|
||||
if (!parse_nostr_bech32_naddr(&bcur, &obj->data.naddr))
|
||||
goto fail;
|
||||
break;
|
||||
case NOSTR_BECH32_NPROFILE:
|
||||
if (!parse_nostr_bech32_nprofile(&bcur, &obj->data.nprofile))
|
||||
goto fail;
|
||||
break;
|
||||
case NOSTR_BECH32_NRELAY:
|
||||
if (!parse_nostr_bech32_nrelay(&bcur, &obj->data.nrelay))
|
||||
goto fail;
|
||||
break;
|
||||
}
|
||||
|
||||
return 1;
|
||||
|
||||
fail:
|
||||
free(obj->buffer);
|
||||
cur->p = start;
|
||||
return 0;
|
||||
}
|
||||
78
damus-c/nostr_bech32.h
Normal file
78
damus-c/nostr_bech32.h
Normal file
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// nostr_bech32.h
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-09.
|
||||
//
|
||||
|
||||
#ifndef nostr_bech32_h
|
||||
#define nostr_bech32_h
|
||||
|
||||
#include <stdio.h>
|
||||
#include "str_block.h"
|
||||
#include "cursor.h"
|
||||
typedef unsigned char u8;
|
||||
#define MAX_RELAYS 10
|
||||
|
||||
struct relays {
|
||||
struct str_block relays[MAX_RELAYS];
|
||||
int num_relays;
|
||||
};
|
||||
|
||||
enum nostr_bech32_type {
|
||||
NOSTR_BECH32_NOTE = 1,
|
||||
NOSTR_BECH32_NPUB = 2,
|
||||
NOSTR_BECH32_NPROFILE = 3,
|
||||
NOSTR_BECH32_NEVENT = 4,
|
||||
NOSTR_BECH32_NRELAY = 5,
|
||||
NOSTR_BECH32_NADDR = 6,
|
||||
};
|
||||
|
||||
struct bech32_note {
|
||||
const u8 *event_id;
|
||||
};
|
||||
|
||||
struct bech32_npub {
|
||||
const u8 *pubkey;
|
||||
};
|
||||
|
||||
struct bech32_nevent {
|
||||
struct relays relays;
|
||||
const u8 *event_id;
|
||||
const u8 *pubkey; // optional
|
||||
};
|
||||
|
||||
struct bech32_nprofile {
|
||||
struct relays relays;
|
||||
const u8 *pubkey;
|
||||
};
|
||||
|
||||
struct bech32_naddr {
|
||||
struct relays relays;
|
||||
struct str_block identifier;
|
||||
const u8 *pubkey;
|
||||
};
|
||||
|
||||
struct bech32_nrelay {
|
||||
struct str_block relay;
|
||||
};
|
||||
|
||||
typedef struct nostr_bech32 {
|
||||
enum nostr_bech32_type type;
|
||||
u8 *buffer; // holds strings and tlv stuff
|
||||
size_t buflen;
|
||||
|
||||
union {
|
||||
struct bech32_note note;
|
||||
struct bech32_npub npub;
|
||||
struct bech32_nevent nevent;
|
||||
struct bech32_nprofile nprofile;
|
||||
struct bech32_naddr naddr;
|
||||
struct bech32_nrelay nrelay;
|
||||
} data;
|
||||
} nostr_bech32_t;
|
||||
|
||||
|
||||
int parse_nostr_bech32(struct cursor *cur, struct nostr_bech32 *obj);
|
||||
|
||||
#endif /* nostr_bech32_h */
|
||||
16
damus-c/str_block.h
Normal file
16
damus-c/str_block.h
Normal file
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// str_block.h
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-09.
|
||||
//
|
||||
|
||||
#ifndef str_block_h
|
||||
#define str_block_h
|
||||
|
||||
typedef struct str_block {
|
||||
const char *start;
|
||||
const char *end;
|
||||
} str_block_t;
|
||||
|
||||
#endif /* str_block_h */
|
||||
@@ -17,6 +17,7 @@
|
||||
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 */; };
|
||||
3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; };
|
||||
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
|
||||
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
|
||||
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
|
||||
@@ -37,6 +38,13 @@
|
||||
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F8E280F640A000448DE /* ThreadModel.swift */; };
|
||||
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F90280F6528000448DE /* ChatView.swift */; };
|
||||
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; };
|
||||
4C1A9A1A29DCA17E00516EAC /* ReplyCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */; };
|
||||
4C1A9A1D29DDCF9B00516EAC /* NotificationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A1C29DDCF9B00516EAC /* NotificationSettingsView.swift */; };
|
||||
4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A1E29DDD24B00516EAC /* AppearanceSettingsView.swift */; };
|
||||
4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2029DDD3E100516EAC /* KeySettingsView.swift */; };
|
||||
4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2229DDDB8100516EAC /* IconLabel.swift */; };
|
||||
4C1A9A2529DDDF2600516EAC /* ZapSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */; };
|
||||
4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */; };
|
||||
4C216F32286E388800040376 /* DMChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F31286E388800040376 /* DMChatView.swift */; };
|
||||
4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; };
|
||||
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */; };
|
||||
@@ -129,6 +137,11 @@
|
||||
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 */; };
|
||||
4C8D00C829DF791C0036AF10 /* CompatibleAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */; };
|
||||
4C8D00CA29DF80350036AF10 /* TruncatedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00C929DF80350036AF10 /* TruncatedText.swift */; };
|
||||
4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */; };
|
||||
4C8D00CF29E38B950036AF10 /* nostr_bech32.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00CE29E38B950036AF10 /* nostr_bech32.c */; };
|
||||
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00D329E3C5D40036AF10 /* NIP19Tests.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 */; };
|
||||
@@ -164,11 +177,11 @@
|
||||
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */; };
|
||||
4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */; };
|
||||
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */; };
|
||||
4CC6193A29DC777C006A86D1 /* RelayBootstrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC6193929DC777C006A86D1 /* RelayBootstrap.swift */; };
|
||||
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAE6297EFA7B00430951 /* Zap.swift */; };
|
||||
4CC7AAEB297F0AEC00430951 /* BuilderEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */; };
|
||||
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEC297F0B9E00430951 /* Highlight.swift */; };
|
||||
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAEF297F11C700430951 /* SelectedEventView.swift */; };
|
||||
4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF1297F129C00430951 /* EmbeddedEventView.swift */; };
|
||||
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF3297F18B400430951 /* ReplyDescription.swift */; };
|
||||
4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF5297F1A6A00430951 /* EventBody.swift */; };
|
||||
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */; };
|
||||
@@ -178,10 +191,14 @@
|
||||
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 */; };
|
||||
4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128929E9D10C0006FA5A /* SignalView.swift */; };
|
||||
4CDA128C29EB19C40006FA5A /* LocalNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128B29EB19C40006FA5A /* LocalNotification.swift */; };
|
||||
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */; };
|
||||
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 */; };
|
||||
4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F0F329D779B5005914DB /* PostBox.swift */; };
|
||||
4CE4F0F829DB7399005914DB /* ThiccDivider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE4F0F729DB7399005914DB /* ThiccDivider.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 */; };
|
||||
@@ -231,6 +248,7 @@
|
||||
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 */; };
|
||||
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; };
|
||||
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
|
||||
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
|
||||
5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF72FC129B9142F00124A13 /* ShareAction.swift */; };
|
||||
@@ -252,12 +270,14 @@
|
||||
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; };
|
||||
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; };
|
||||
E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadView.swift */; };
|
||||
F757933A29D7AECD007DEAC1 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F757933929D7AECD007DEAC1 /* ImagePicker.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 */; };
|
||||
F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */; };
|
||||
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */; };
|
||||
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParicipantsView.swift */; };
|
||||
F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA262978E54D009531F3 /* ParticipantsView.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -310,9 +330,7 @@
|
||||
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>"; };
|
||||
3A4F3320297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A4F3321297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-FR"; path = "fr-FR.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A4F3322297CCFEE004B5F72 /* fr-FR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-FR"; path = "fr-FR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutedThreadsManager.swift; sourceTree = "<group>"; };
|
||||
3A5C4575296A879E0032D398 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-419"; path = "es-419.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A5CAE1E298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
@@ -320,6 +338,9 @@
|
||||
3A66D927299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3A66D928299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A66D929299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3A821C3E29E819D500B4BCA7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A821C3F29E819D500B4BCA7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3A821C4029E819D500B4BCA7 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fr; path = fr.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3A827A18299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3A827A19299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A827A1A299FC69D00C4D171 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
@@ -366,9 +387,6 @@
|
||||
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>"; };
|
||||
@@ -393,6 +411,13 @@
|
||||
4C0A3F8E280F640A000448DE /* ThreadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadModel.swift; sourceTree = "<group>"; };
|
||||
4C0A3F90280F6528000448DE /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; };
|
||||
4C0A3F92280F66F5000448DE /* ReplyMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyMap.swift; sourceTree = "<group>"; };
|
||||
4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyCounter.swift; sourceTree = "<group>"; };
|
||||
4C1A9A1C29DDCF9B00516EAC /* NotificationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsView.swift; sourceTree = "<group>"; };
|
||||
4C1A9A1E29DDD24B00516EAC /* AppearanceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceSettingsView.swift; sourceTree = "<group>"; };
|
||||
4C1A9A2029DDD3E100516EAC /* KeySettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeySettingsView.swift; sourceTree = "<group>"; };
|
||||
4C1A9A2229DDDB8100516EAC /* IconLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconLabel.swift; sourceTree = "<group>"; };
|
||||
4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapSettingsView.swift; sourceTree = "<group>"; };
|
||||
4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationSettingsView.swift; sourceTree = "<group>"; };
|
||||
4C216F31286E388800040376 /* DMChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMChatView.swift; sourceTree = "<group>"; };
|
||||
4C216F33286F5ACD00040376 /* DMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMView.swift; sourceTree = "<group>"; };
|
||||
4C216F352870A9A700040376 /* InputDismissKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDismissKeyboard.swift; sourceTree = "<group>"; };
|
||||
@@ -515,6 +540,15 @@
|
||||
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>"; };
|
||||
4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompatibleAttribute.swift; sourceTree = "<group>"; };
|
||||
4C8D00C929DF80350036AF10 /* TruncatedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TruncatedText.swift; sourceTree = "<group>"; };
|
||||
4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hashtags.swift; sourceTree = "<group>"; };
|
||||
4C8D00CD29E38B950036AF10 /* nostr_bech32.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = nostr_bech32.h; sourceTree = "<group>"; };
|
||||
4C8D00CE29E38B950036AF10 /* nostr_bech32.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = nostr_bech32.c; sourceTree = "<group>"; };
|
||||
4C8D00D029E38E4C0036AF10 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = "<group>"; };
|
||||
4C8D00D129E397AD0036AF10 /* block.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = block.h; sourceTree = "<group>"; };
|
||||
4C8D00D229E3C19F0036AF10 /* str_block.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = str_block.h; sourceTree = "<group>"; };
|
||||
4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP19Tests.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>"; };
|
||||
@@ -550,11 +584,11 @@
|
||||
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNameView.swift; sourceTree = "<group>"; };
|
||||
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowsYou.swift; sourceTree = "<group>"; };
|
||||
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteLink.swift; sourceTree = "<group>"; };
|
||||
4CC6193929DC777C006A86D1 /* RelayBootstrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayBootstrap.swift; sourceTree = "<group>"; };
|
||||
4CC7AAE6297EFA7B00430951 /* Zap.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Zap.swift; sourceTree = "<group>"; };
|
||||
4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuilderEventView.swift; sourceTree = "<group>"; };
|
||||
4CC7AAEC297F0B9E00430951 /* Highlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Highlight.swift; sourceTree = "<group>"; };
|
||||
4CC7AAEF297F11C700430951 /* SelectedEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedEventView.swift; sourceTree = "<group>"; };
|
||||
4CC7AAF1297F129C00430951 /* EmbeddedEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddedEventView.swift; sourceTree = "<group>"; };
|
||||
4CC7AAF3297F18B400430951 /* ReplyDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyDescription.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
@@ -564,10 +598,14 @@
|
||||
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>"; };
|
||||
4CDA128929E9D10C0006FA5A /* SignalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalView.swift; sourceTree = "<group>"; };
|
||||
4CDA128B29EB19C40006FA5A /* LocalNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalNotification.swift; sourceTree = "<group>"; };
|
||||
4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventHolder.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
4CE4F0F329D779B5005914DB /* PostBox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostBox.swift; sourceTree = "<group>"; };
|
||||
4CE4F0F729DB7399005914DB /* ThiccDivider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThiccDivider.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
@@ -620,6 +658,7 @@
|
||||
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>"; };
|
||||
5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = "<group>"; };
|
||||
5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; };
|
||||
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
|
||||
5CF72FC129B9142F00124A13 /* ShareAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAction.swift; sourceTree = "<group>"; };
|
||||
@@ -640,12 +679,14 @@
|
||||
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 /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; };
|
||||
F757933929D7AECD007DEAC1 /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.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>"; };
|
||||
F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditProfilePictureControl.swift; sourceTree = "<group>"; };
|
||||
F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToDismiss.swift; sourceTree = "<group>"; };
|
||||
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParicipantsView.swift; sourceTree = "<group>"; };
|
||||
F7F0BA262978E54D009531F3 /* ParticipantsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParticipantsView.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -681,6 +722,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */,
|
||||
5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */,
|
||||
);
|
||||
path = "Empty Views";
|
||||
sourceTree = "<group>";
|
||||
@@ -744,6 +786,11 @@
|
||||
4C3EA67428FF7A5A00C48A62 /* take.c */,
|
||||
4C3EA67628FF7A9800C48A62 /* talstr.c */,
|
||||
4C3EA67828FF7ABF00C48A62 /* list.c */,
|
||||
4C8D00CD29E38B950036AF10 /* nostr_bech32.h */,
|
||||
4C8D00CE29E38B950036AF10 /* nostr_bech32.c */,
|
||||
4C8D00D029E38E4C0036AF10 /* cursor.h */,
|
||||
4C8D00D129E397AD0036AF10 /* block.h */,
|
||||
4C8D00D229E3C19F0036AF10 /* str_block.h */,
|
||||
);
|
||||
path = "damus-c";
|
||||
sourceTree = "<group>";
|
||||
@@ -794,10 +841,23 @@
|
||||
3AA59D1C2999B0400061C48E /* DraftsModel.swift */,
|
||||
4C54AA0629A540BA003E4487 /* NotificationsModel.swift */,
|
||||
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */,
|
||||
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C1A9A1B29DDCF8B00516EAC /* Settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C1A9A1C29DDCF9B00516EAC /* NotificationSettingsView.swift */,
|
||||
4C1A9A1E29DDD24B00516EAC /* AppearanceSettingsView.swift */,
|
||||
4C1A9A2029DDD3E100516EAC /* KeySettingsView.swift */,
|
||||
4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */,
|
||||
4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C30AC7029A5676F00E2BD5A /* Notifications */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -821,6 +881,7 @@
|
||||
4C75EFA227FA576C0006080F /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C1A9A1B29DDCF8B00516EAC /* Settings */,
|
||||
4CFF8F6129CC9A80008DB934 /* Images */,
|
||||
4CCEB7AC29B53D180078AA28 /* Search */,
|
||||
4C30AC7029A5676F00E2BD5A /* Notifications */,
|
||||
@@ -857,12 +918,13 @@
|
||||
4C75EFAC28049CFB0006080F /* PostButton.swift */,
|
||||
4C75EFA327FA577B0006080F /* PostView.swift */,
|
||||
9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */,
|
||||
F757933929D7AECD007DEAC1 /* ImagePicker.swift */,
|
||||
9C83F89229A937B900136C08 /* TextViewWrapper.swift */,
|
||||
4CEE2AF8280B2EAC00AB5EEF /* PowView.swift */,
|
||||
4C3AC7A42836987600E1F516 /* MainTabView.swift */,
|
||||
4C363A8B28236B92006E126D /* PubkeyView.swift */,
|
||||
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */,
|
||||
F7F0BA262978E54D009531F3 /* ParicipantsView.swift */,
|
||||
F7F0BA262978E54D009531F3 /* ParticipantsView.swift */,
|
||||
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */,
|
||||
4C3AC7A628369BA200E1F516 /* SearchHomeView.swift */,
|
||||
4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */,
|
||||
@@ -907,6 +969,7 @@
|
||||
4C7FF7D628233637009601DB /* Util */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CE4F0F329D779B5005914DB /* PostBox.swift */,
|
||||
7C0F392D29B57C8F0039859C /* Extensions */,
|
||||
4CE879492995B58700F758CC /* Relays */,
|
||||
4CF0ABEA29844B2F00D66079 /* AnyCodable */,
|
||||
@@ -940,6 +1003,10 @@
|
||||
4C30AC7729A577AB00E2BD5A /* EventCache.swift */,
|
||||
4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */,
|
||||
4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */,
|
||||
4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */,
|
||||
4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */,
|
||||
4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */,
|
||||
4CDA128B29EB19C40006FA5A /* LocalNotification.swift */,
|
||||
);
|
||||
path = Util;
|
||||
sourceTree = "<group>";
|
||||
@@ -955,6 +1022,7 @@
|
||||
4CE8794D2996B16A00F758CC /* RelayToggle.swift */,
|
||||
4CE8794F2996B2BD00F758CC /* RelayStatus.swift */,
|
||||
4CE879512996B68900F758CC /* RelayType.swift */,
|
||||
4CDA128929E9D10C0006FA5A /* SignalView.swift */,
|
||||
);
|
||||
path = Relays;
|
||||
sourceTree = "<group>";
|
||||
@@ -982,6 +1050,7 @@
|
||||
children = (
|
||||
4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */,
|
||||
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */,
|
||||
F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */,
|
||||
4CEE2AF2280B25C500AB5EEF /* ProfilePicView.swift */,
|
||||
4C8682862814DE470026224F /* ProfileView.swift */,
|
||||
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */,
|
||||
@@ -996,7 +1065,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CC7AAEF297F11C700430951 /* SelectedEventView.swift */,
|
||||
4CC7AAF1297F129C00430951 /* EmbeddedEventView.swift */,
|
||||
4CC7AAF3297F18B400430951 /* ReplyDescription.swift */,
|
||||
4CC7AAF5297F1A6A00430951 /* EventBody.swift */,
|
||||
4CC7AAEA297F0AEC00430951 /* BuilderEventView.swift */,
|
||||
@@ -1056,6 +1124,9 @@
|
||||
4C42812B298C848200DBF26F /* TranslateView.swift */,
|
||||
7CFF6316299FEFE5005D382A /* SelectableText.swift */,
|
||||
4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */,
|
||||
4CE4F0F729DB7399005914DB /* ThiccDivider.swift */,
|
||||
4C1A9A2229DDDB8100516EAC /* IconLabel.swift */,
|
||||
4C8D00C929DF80350036AF10 /* TruncatedText.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
@@ -1131,6 +1202,7 @@
|
||||
3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */,
|
||||
3A3040F229A91366008A0F29 /* ProfileViewTests.swift */,
|
||||
3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */,
|
||||
4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */,
|
||||
);
|
||||
path = damusTests;
|
||||
sourceTree = "<group>";
|
||||
@@ -1149,6 +1221,7 @@
|
||||
children = (
|
||||
4CE8794729941DA700F758CC /* RelayFilters.swift */,
|
||||
4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */,
|
||||
4CC6193929DC777C006A86D1 /* RelayBootstrap.swift */,
|
||||
);
|
||||
path = Relays;
|
||||
sourceTree = "<group>";
|
||||
@@ -1333,8 +1406,7 @@
|
||||
"es-419",
|
||||
"es-ES",
|
||||
fa,
|
||||
"fr-CA",
|
||||
"fr-FR",
|
||||
fr,
|
||||
"hu-HU",
|
||||
id,
|
||||
"it-IT",
|
||||
@@ -1422,9 +1494,11 @@
|
||||
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */,
|
||||
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */,
|
||||
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */,
|
||||
4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */,
|
||||
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
|
||||
4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */,
|
||||
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
|
||||
F757933A29D7AECD007DEAC1 /* ImagePicker.swift in Sources */,
|
||||
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */,
|
||||
4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */,
|
||||
4CCEB7A929B29DD50078AA28 /* SearchResultsModel.swift in Sources */,
|
||||
@@ -1435,9 +1509,11 @@
|
||||
4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */,
|
||||
4C363AA228296A7E006E126D /* SearchView.swift in Sources */,
|
||||
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */,
|
||||
4CC6193A29DC777C006A86D1 /* RelayBootstrap.swift in Sources */,
|
||||
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
|
||||
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */,
|
||||
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
|
||||
4C8D00C829DF791C0036AF10 /* CompatibleAttribute.swift in Sources */,
|
||||
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */,
|
||||
F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */,
|
||||
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */,
|
||||
@@ -1450,7 +1526,7 @@
|
||||
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */,
|
||||
4C3D52B6298DB4E6001C5831 /* ZapEvent.swift in Sources */,
|
||||
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */,
|
||||
F7F0BA272978E54D009531F3 /* ParicipantsView.swift in Sources */,
|
||||
F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */,
|
||||
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
|
||||
4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */,
|
||||
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */,
|
||||
@@ -1462,8 +1538,10 @@
|
||||
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
|
||||
4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */,
|
||||
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */,
|
||||
3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */,
|
||||
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */,
|
||||
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */,
|
||||
4C8D00CA29DF80350036AF10 /* TruncatedText.swift in Sources */,
|
||||
4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */,
|
||||
4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */,
|
||||
4C649844285A952100EAE2B3 /* LocalUserConfig.swift in Sources */,
|
||||
@@ -1501,7 +1579,6 @@
|
||||
4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */,
|
||||
4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */,
|
||||
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
|
||||
4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */,
|
||||
4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */,
|
||||
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */,
|
||||
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
|
||||
@@ -1513,6 +1590,7 @@
|
||||
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */,
|
||||
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */,
|
||||
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */,
|
||||
4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */,
|
||||
4C3EA64928FF597700C48A62 /* bech32.c in Sources */,
|
||||
4CE879522996B68900F758CC /* RelayType.swift in Sources */,
|
||||
4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */,
|
||||
@@ -1536,9 +1614,11 @@
|
||||
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
|
||||
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
|
||||
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */,
|
||||
4CE4F0F829DB7399005914DB /* ThiccDivider.swift in Sources */,
|
||||
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */,
|
||||
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
|
||||
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
|
||||
F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */,
|
||||
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
|
||||
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
||||
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
|
||||
@@ -1554,11 +1634,13 @@
|
||||
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */,
|
||||
4C3EA64F28FF59F200C48A62 /* tal.c in Sources */,
|
||||
4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */,
|
||||
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.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 */,
|
||||
4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */,
|
||||
4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */,
|
||||
4CF0ABD82981980C00D66079 /* Lists.swift in Sources */,
|
||||
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */,
|
||||
@@ -1567,12 +1649,15 @@
|
||||
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
|
||||
6439E014296790CF0020672B /* ProfilePicImageView.swift in Sources */,
|
||||
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */,
|
||||
4CDA128C29EB19C40006FA5A /* LocalNotification.swift in Sources */,
|
||||
4C3BEFD6281D995700B3DE84 /* ActionBarModel.swift in Sources */,
|
||||
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */,
|
||||
4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */,
|
||||
4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */,
|
||||
4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */,
|
||||
4CEE2AF9280B2EAC00AB5EEF /* PowView.swift in Sources */,
|
||||
3165648B295B70D500C64604 /* LinkView.swift in Sources */,
|
||||
4C8D00CF29E38B950036AF10 /* nostr_bech32.c in Sources */,
|
||||
4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */,
|
||||
4C3EA66028FF5E7700C48A62 /* node_id.c in Sources */,
|
||||
4CE6DEE727F7A08100C66700 /* damusApp.swift in Sources */,
|
||||
@@ -1599,6 +1684,7 @@
|
||||
4CE4F0F229D4FCFA005914DB /* DebouncedOnChange.swift in Sources */,
|
||||
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
|
||||
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
|
||||
4C1A9A1A29DCA17E00516EAC /* ReplyCounter.swift in Sources */,
|
||||
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */,
|
||||
4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */,
|
||||
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
|
||||
@@ -1611,6 +1697,7 @@
|
||||
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
|
||||
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
|
||||
4C06670B28FDE64700038D2A /* damus.c in Sources */,
|
||||
4C1A9A2529DDDF2600516EAC /* ZapSettingsView.swift in Sources */,
|
||||
4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */,
|
||||
3AAA95CC298E07E900F3D526 /* DeepLPlan.swift in Sources */,
|
||||
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
|
||||
@@ -1621,15 +1708,19 @@
|
||||
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
|
||||
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
|
||||
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */,
|
||||
4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */,
|
||||
4C1A9A1D29DDCF9B00516EAC /* NotificationSettingsView.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 */,
|
||||
4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */,
|
||||
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */,
|
||||
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
|
||||
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
|
||||
4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -1638,6 +1729,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
|
||||
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */,
|
||||
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */,
|
||||
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */,
|
||||
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */,
|
||||
@@ -1687,7 +1779,6 @@
|
||||
3A5C4575296A879E0032D398 /* es-419 */,
|
||||
3A2B8B0A296A8982009CC16D /* en-US */,
|
||||
3AEB8005297CCEA900713A25 /* tr-TR */,
|
||||
3A4F3322297CCFEE004B5F72 /* fr-FR */,
|
||||
3A185A06297F2C3800F4BDC0 /* lv-LV */,
|
||||
3A929C22297F2CF80090925E /* it-IT */,
|
||||
3AB5B86C2986D8A3006599D2 /* de */,
|
||||
@@ -1709,10 +1800,10 @@
|
||||
3AD5663229C0DA4B00BF77C5 /* ko */,
|
||||
3AD14EB529C40F38009D2D9C /* hu-HU */,
|
||||
3AD14EB829C40F3F009D2D9C /* sv-SE */,
|
||||
3AD14EBC29C40F47009D2D9C /* fr-CA */,
|
||||
3A325AC629C9E0B8002BE7ED /* vi */,
|
||||
3A325AC929C9E0CF002BE7ED /* es-ES */,
|
||||
3AC59CA929CDDB78007E04A6 /* pt-BR */,
|
||||
3A821C4029E819D500B4BCA7 /* fr */,
|
||||
);
|
||||
name = Localizable.stringsdict;
|
||||
sourceTree = "<group>";
|
||||
@@ -1722,7 +1813,6 @@
|
||||
children = (
|
||||
3ACB685B297633BC00C46468 /* es-419 */,
|
||||
3AEB8003297CCEA800713A25 /* tr-TR */,
|
||||
3A4F3320297CCFEE004B5F72 /* fr-FR */,
|
||||
3A185A04297F2C3800F4BDC0 /* lv-LV */,
|
||||
3A929C20297F2CF80090925E /* it-IT */,
|
||||
3AB5B86A2986D8A3006599D2 /* de */,
|
||||
@@ -1744,10 +1834,10 @@
|
||||
3AD5663329C0DA4B00BF77C5 /* ko */,
|
||||
3AD14EB629C40F38009D2D9C /* hu-HU */,
|
||||
3AD14EB929C40F3F009D2D9C /* sv-SE */,
|
||||
3AD14EBB29C40F47009D2D9C /* fr-CA */,
|
||||
3A325AC529C9E0B8002BE7ED /* vi */,
|
||||
3A325AC829C9E0CF002BE7ED /* es-ES */,
|
||||
3AC59CA829CDDB78007E04A6 /* pt-BR */,
|
||||
3A821C3F29E819D500B4BCA7 /* fr */,
|
||||
);
|
||||
name = InfoPlist.strings;
|
||||
sourceTree = "<group>";
|
||||
@@ -1757,7 +1847,6 @@
|
||||
children = (
|
||||
3ACB685E297633BC00C46468 /* es-419 */,
|
||||
3AEB8004297CCEA800713A25 /* tr-TR */,
|
||||
3A4F3321297CCFEE004B5F72 /* fr-FR */,
|
||||
3A185A05297F2C3800F4BDC0 /* lv-LV */,
|
||||
3A929C21297F2CF80090925E /* it-IT */,
|
||||
3AB5B86B2986D8A3006599D2 /* de */,
|
||||
@@ -1780,10 +1869,10 @@
|
||||
3AD5663129C0DA4B00BF77C5 /* ko */,
|
||||
3AD14EB729C40F38009D2D9C /* hu-HU */,
|
||||
3AD14EBA29C40F3F009D2D9C /* sv-SE */,
|
||||
3AD14EBD29C40F47009D2D9C /* fr-CA */,
|
||||
3A325AC429C9E0B8002BE7ED /* vi */,
|
||||
3A325AC729C9E0CF002BE7ED /* es-ES */,
|
||||
3AC59CA729CDDB78007E04A6 /* pt-BR */,
|
||||
3A821C3E29E819D500B4BCA7 /* fr */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
@@ -1919,7 +2008,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1927,7 +2016,9 @@
|
||||
INFOPLIST_FILE = damus/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Damus;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "Damus needs access to your camera if you want to upload photos from it";
|
||||
INFOPLIST_KEY_NSFaceIDUsageDescription = "Local authentication to access private key";
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Damus needs access to your microphone if you want to upload recorded videos from it";
|
||||
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Granting Damus access to your photos allows you to save images.";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
@@ -1942,9 +2033,12 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 1.4.0;
|
||||
MARKETING_VERSION = 1.4.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "damus-c/damus-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -1961,7 +2055,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1969,7 +2063,9 @@
|
||||
INFOPLIST_FILE = damus/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Damus;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "Damus needs access to your camera if you want to upload photos from it";
|
||||
INFOPLIST_KEY_NSFaceIDUsageDescription = "Local authentication to access private key";
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Damus needs access to your microphone if you want to upload recorded videos from it";
|
||||
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Granting Damus access to your photos allows you to save images.";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
@@ -1984,9 +2080,12 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 1.4.0;
|
||||
MARKETING_VERSION = 1.4.3;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "damus-c/damus-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"red" : "0xBE",
|
||||
"green" : "0x5F",
|
||||
"blue" : "0x00"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xED",
|
||||
"green" : "0x26",
|
||||
"red" : "0xBF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x05",
|
||||
"green" : "0xDF",
|
||||
"red" : "0xFA"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
6
damus/Assets.xcassets/Hashtags/Contents.json
Normal file
6
damus/Assets.xcassets/Hashtags/Contents.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
23
damus/Assets.xcassets/Hashtags/bitcoin-hashtag.imageset/Contents.json
vendored
Normal file
23
damus/Assets.xcassets/Hashtags/bitcoin-hashtag.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "bitcoin-hashtag.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "bitcoin-hashtag.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "bitcoin-hashtag.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
43
damus/Assets.xcassets/Hashtags/bitcoin-hashtag.imageset/bitcoin-hashtag.svg
vendored
Normal file
43
damus/Assets.xcassets/Hashtags/bitcoin-hashtag.imageset/bitcoin-hashtag.svg
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
width="12.843903"
|
||||
height="17"
|
||||
viewBox="0 0 12.843902 16.999999"
|
||||
version="1.1"
|
||||
id="svg2"
|
||||
sodipodi:docname="bitcoin-hashtag.svg"
|
||||
inkscape:version="1.3-dev (77bc73e, 2022-05-18)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs2" />
|
||||
<sodipodi:namedview
|
||||
id="namedview2"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="31.12"
|
||||
inkscape:cx="4.2577121"
|
||||
inkscape:cy="7.535347"
|
||||
inkscape:window-width="1526"
|
||||
inkscape:window-height="957"
|
||||
inkscape:window-x="1637"
|
||||
inkscape:window-y="10"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg2" />
|
||||
<g
|
||||
id="surface1"
|
||||
transform="matrix(0.94507527,0,0,0.94507527,-4.5943665,-3.2875042)">
|
||||
<path
|
||||
style="fill:#f59119;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.40637"
|
||||
d="M 18.388175,10.742602 C 18.668352,8.874761 17.240002,7.8694225 15.295251,7.193703 L 15.927019,4.6611305 14.383304,4.2765743 13.768015,6.7432244 C 13.361486,6.644338 12.943967,6.545453 12.531944,6.4520613 L 13.152726,3.9689298 11.609011,3.584375 10.977241,6.1169476 C 10.642129,6.0400371 10.312509,5.9686201 9.988384,5.886215 L 9.993834,5.880715 7.8623408,5.3478364 7.4558114,6.9959316 c 0,0 1.1426793,0.2636953 1.1207046,0.2801765 0.6207823,0.1538223 0.7361485,0.5658464 0.714174,0.8954659 l -0.7196673,2.889659 c 0.043949,0.01099 0.098885,0.02747 0.1648089,0.04944 L 8.5710227,11.072223 7.5601911,15.11555 c -0.076912,0.192277 -0.26919,0.477948 -0.7031873,0.368073 0.010984,0.02198 -1.1261994,-0.280174 -1.1261994,-0.280174 l -0.7636164,1.76346 2.0106754,0.505417 c 0.3735682,0.0934 0.7361485,0.186784 1.0987301,0.280176 l -0.6427568,2.565534 1.5437155,0.384556 0.6317701,-2.538067 c 0.4230107,0.115364 0.8295407,0.219746 1.2305777,0.318632 l -0.63177,2.527079 1.543716,0.384557 0.637263,-2.560042 c 2.631459,0.499922 4.614667,0.296656 5.444208,-2.082094 0.670226,-1.917284 -0.03296,-3.021507 -1.417363,-3.741176 1.010832,-0.236227 1.768957,-0.895465 1.972221,-2.268877 z m -3.526922,4.944286 c -0.477949,1.917283 -3.702721,0.884477 -4.752008,0.620782 l 0.851514,-3.395075 c 1.043795,0.2582 4.394922,0.774604 3.900494,2.774293 z M 15.3392,10.715134 c -0.433999,1.74698 -3.120394,0.857008 -3.993884,0.642757 l 0.769111,-3.081939 c 0.87349,0.219746 3.675252,0.620784 3.224773,2.439182 z m 0,0"
|
||||
id="path2" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
23
damus/Assets.xcassets/Hashtags/coffee-hashtag.imageset/Contents.json
vendored
Normal file
23
damus/Assets.xcassets/Hashtags/coffee-hashtag.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "coffee.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "coffee.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "coffee.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
55
damus/Assets.xcassets/Hashtags/coffee-hashtag.imageset/coffee.svg
vendored
Normal file
55
damus/Assets.xcassets/Hashtags/coffee-hashtag.imageset/coffee.svg
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon -->
|
||||
|
||||
<svg
|
||||
version="1.1"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 13.999999 18"
|
||||
enable-background="new 0 0 1000 1000"
|
||||
xml:space="preserve"
|
||||
id="svg4"
|
||||
sodipodi:docname="coffee.svg"
|
||||
width="14"
|
||||
height="18"
|
||||
inkscape:version="1.3-dev (77bc73e, 2022-05-18)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs4" /><sodipodi:namedview
|
||||
id="namedview4"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="10.680141"
|
||||
inkscape:cx="-17.181421"
|
||||
inkscape:cy="4.07298"
|
||||
inkscape:window-width="1368"
|
||||
inkscape:window-height="947"
|
||||
inkscape:window-x="1764"
|
||||
inkscape:window-y="58"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg4" />
|
||||
<metadata
|
||||
id="metadata1"> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
|
||||
<g
|
||||
id="g4"
|
||||
transform="matrix(0.01779387,0,0,0.01779387,-1.8340539,0.04465199)"><g
|
||||
transform="matrix(0.1,0,0,-0.1,0,511)"
|
||||
id="g3"><path
|
||||
d="m 4302.6,4870.6 c 149.5,-177.8 240.5,-319.3 347.6,-545.6 119.2,-254.6 169.7,-448.6 183.9,-699.2 22.2,-462.7 -137.4,-778 -539.5,-1060.9 -474.9,-335.4 -685,-739.6 -687,-1315.5 0,-260.7 38.4,-501.1 115.2,-739.6 50.5,-149.5 56.6,-159.6 60.6,-97 18.2,234.4 56.6,476.9 101,626.4 121.2,422.3 305.1,622.4 885.1,959.8 424.3,248.5 575.9,487 575.9,905.3 -2,501.1 -359.7,1295.3 -798.2,1768.1 -82.8,88.9 -198,202.1 -256.6,250.6 l -105.1,86.9 z"
|
||||
id="path1"
|
||||
style="fill:#be5f00;fill-opacity:1" /><path
|
||||
d="m 5981.8,3577.3 c 272.8,-369.8 309.2,-846.7 90.9,-1192.2 -147.5,-232.4 -373.8,-406.2 -822.4,-638.5 -592,-303.1 -854.7,-683 -854.7,-1232.6 0,-276.8 14.2,-343.5 72.7,-343.5 38.4,0 48.5,16.2 68.7,111.1 34.4,167.7 135.4,349.6 262.7,476.9 147.5,145.5 349.6,244.5 838.6,412.2 503.2,171.8 725.4,280.9 846.7,416.3 210.1,232.4 276.8,535.5 202.1,903.2 -76.8,373.8 -216.2,618.3 -537.5,943.7 -155.7,155.6 -214.3,206.1 -167.8,143.4 z"
|
||||
id="path2"
|
||||
style="fill:#be5f00;fill-opacity:1" /><path
|
||||
d="M 2748.7,592.8 C 2158.7,507.9 1732.3,352.3 1542.4,156.3 1415.1,25 1409,-11.4 1427.2,-445.8 c 34.3,-832.5 181.9,-1729.7 462.7,-2829 153.6,-600.1 309.2,-1113.4 351.6,-1159.9 56.6,-62.6 272.8,-157.6 476.9,-210.1 668.8,-173.8 2172.2,-196 2960.3,-42.4 357.7,68.7 604.2,163.7 731.5,278.9 68.7,60.6 84.9,92.9 107.1,198 64.7,335.4 56.6,319.3 131.3,319.3 107.1,0 438.5,92.9 602.2,167.7 220.3,103.1 363.7,202.1 567.8,398.1 470.8,452.6 759.8,1149.8 761.8,1840.8 0,398.1 -72.7,614.3 -274.8,818.4 -220.3,222.3 -466.8,309.2 -937.6,325.4 l -309.2,12.1 14.2,218.2 14.2,218.2 -56.6,52.5 C 6866.9,314 6274.9,499.9 5696.9,582.7 L 5614,594.8 5533.2,457.4 5450.4,322 5565.6,305.8 c 588,-84.9 868.9,-159.6 978,-256.6 56.6,-50.5 60.6,-62.6 44.5,-113.2 -80.8,-226.3 -1000.2,-371.8 -2329.8,-371.8 -1220.5,0 -2097.5,123.3 -2295.5,321.3 -52.5,54.5 -56.6,66.7 -36.4,111.1 48.5,107.1 341.5,206.1 822.4,276.8 143.5,20.2 301.1,38.4 349.6,38.4 46.5,0 84.9,4 84.9,8.1 0,8.1 -175.8,295 -185.9,305.1 -4.2,2.1 -115.3,-12 -248.7,-32.2 z m 4797.1,-1614.5 c 153.6,-26.3 272.8,-82.8 317.2,-153.6 56.6,-84.9 60.6,-400.1 8.1,-652.7 -111.1,-545.6 -404.1,-996.2 -788.1,-1220.5 -139.4,-80.8 -333.4,-151.5 -355.6,-129.3 -8.1,8.1 18.2,216.2 58.6,462.7 78.8,482.9 159.6,1073 202.1,1450.8 l 24.2,232.4 70.7,12.1 c 115.3,20.3 325.4,20.3 462.8,-1.9 z"
|
||||
id="path3"
|
||||
style="fill:#be5f00;fill-opacity:1" /></g></g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
23
damus/Assets.xcassets/Hashtags/nostr-hashtag.imageset/Contents.json
vendored
Normal file
23
damus/Assets.xcassets/Hashtags/nostr-hashtag.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "nostr-hashtag.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "nostr-hashtag.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "nostr-hashtag.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
51
damus/Assets.xcassets/Hashtags/nostr-hashtag.imageset/nostr-hashtag.svg
vendored
Normal file
51
damus/Assets.xcassets/Hashtags/nostr-hashtag.imageset/nostr-hashtag.svg
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
version="1.0"
|
||||
width="18pt"
|
||||
height="18.199053pt"
|
||||
viewBox="0 0 18 18.199053"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
id="svg1"
|
||||
sodipodi:docname="nostr-hashtag.svg"
|
||||
inkscape:version="1.3-dev (77bc73e, 2022-05-18)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="pt"
|
||||
showgrid="false"
|
||||
inkscape:zoom="5.4017383"
|
||||
inkscape:cx="50.354161"
|
||||
inkscape:cy="-13.514168"
|
||||
inkscape:window-width="1497"
|
||||
inkscape:window-height="866"
|
||||
inkscape:window-x="1747"
|
||||
inkscape:window-y="96"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg1" />
|
||||
<metadata
|
||||
id="metadata1">
|
||||
Created by potrace 1.15, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g
|
||||
transform="matrix(0.00138509,0,0,-0.00138509,0.3,17.927982)"
|
||||
fill="#000000"
|
||||
stroke="none"
|
||||
id="g1">
|
||||
<path
|
||||
d="m 11315,12756 c -49,-23 -135,-71 -190,-106 -128,-81 -170,-100 -222,-100 -37,0 -43,-3 -43,-22 0,-11 12,-36 26,-55 l 26,-34 -20,-37 c -128,-248 -171,-359 -212,-547 -56,-262 -43,-645 36,-1045 48,-244 104,-451 234,-865 158,-503 280,-942 320,-1145 27,-140 60,-448 60,-559 0,-164 -39,-366 -92,-478 -57,-123 -176,-222 -286,-239 -49,-8 -50,-23 -5,-38 18,-6 34,-14 38,-18 13,-13 -25,-66 -97,-134 -121,-115 -267,-174 -432,-174 -200,0 -327,87 -732,507 -233,240 -541,529 -604,567 -31,18 -32,18 -27,0 3,-11 9,-36 13,-56 8,-49 -5,-49 -65,1 -75,62 -168,118 -250,150 -134,51 -214,63 -481,70 l -245,6 -65,41 c -100,63 -125,68 -295,66 -186,-3 -262,7 -484,64 -279,71 -341,67 -780,-61 -334,-97 -442,-118 -761,-150 -200,-19 -365,-19 -496,1 -58,9 -107,14 -111,11 -3,-4 6,-22 21,-41 l 27,-35 -23,-5 c -13,-3 -61,-8 -106,-11 -113,-9 -157,-26 -219,-87 -50,-49 -54,-51 -140,-64 -60,-10 -108,-25 -153,-48 -304,-157 -331,-166 -471,-166 -48,0 -120,7 -160,15 -40,9 -74,13 -78,10 -3,-3 0,-20 7,-37 6,-18 10,-34 7,-36 -2,-3 -24,0 -47,7 -24,6 -103,16 -176,23 -163,15 -350,1 -521,-40 l -114,-26 51,-23 c 53,-23 76,-50 66,-77 -3,-8 -51,-38 -105,-66 -251,-129 -515,-384 -727,-703 -110,-166 -208,-325 -217,-355 -6,-21 -5,-22 34,-16 36,6 41,4 41,-12 0,-11 -15,-45 -34,-76 -46,-75 -76,-161 -127,-359 -78,-307 -137,-444 -243,-569 -38,-44 -51,-67 -43,-72 7,-4 40,-8 75,-8 37,0 62,-4 62,-11 0,-6 -12,-26 -27,-45 -32,-43 -24,-53 48,-60 108,-11 103,-6 95,-82 -4,-37 -11,-128 -15,-202 -5,-74 -16,-164 -26,-200 -22,-85 -75,-190 -136,-269 -27,-35 -49,-67 -49,-72 0,-4 17,-11 38,-14 20,-3 49,-8 64,-10 64,-10 285,31 408,76 101,37 249,117 348,188 54,38 110,75 126,80 15,6 70,11 123,11 267,0 582,98 766,239 34,26 72,53 83,59 25,13 31,8 58,-46 21,-40 38,-32 57,26 30,93 97,200 141,223 25,14 38,3 98,-86 27,-38 73,-95 104,-125 31,-30 56,-63 56,-75 0,-11 -20,-60 -45,-110 -42,-84 -81,-202 -70,-213 3,-3 27,16 54,41 50,47 81,59 81,34 0,-8 -10,-49 -21,-93 -26,-97 -32,-275 -10,-313 l 14,-25 36,41 c 42,48 118,88 251,131 102,33 185,47 345,57 55,4 159,13 230,21 292,34 285,35 368,-56 66,-72 122,-115 170,-130 20,-6 37,-15 37,-19 0,-12 -22,-35 -75,-77 -27,-21 -151,-133 -275,-248 -424,-394 -462,-415 -864,-486 -65,-12 -145,-29 -177,-39 -191,-60 -348,-213 -554,-541 -103,-165 -162,-241 -292,-383 -176,-190 -332,-333 -778,-711 -497,-422 -750,-737 -895,-1113 -64,-165 -104,-203 -216,-203 -38,0 -94,7 -124,15 -74,20 -244,20 -301,0 -27,-10 -61,-33 -87,-62 -41,-45 -43,-46 -89,-39 -25,3 -67,18 -92,33 C 457,781 369,812 175,813 49,814 0,786 0,713 0,669 31,628 122,557 204,491 254,434 469,153 527,77 549,57 597,33 652,7 662,5 780,6 c 69,0 150,6 180,14 30,7 132,32 225,56 281,72 308,76 511,84 181,6 195,8 247,34 76,37 137,107 231,267 230,392 287,481 394,626 219,293 469,581 756,869 284,284 368,342 641,439 164,59 295,123 410,199 101,68 257,219 295,286 14,24 56,76 93,116 100,106 177,142 437,203 165,40 229,62 390,133 191,85 220,94 460,143 118,24 239,49 267,56 29,7 56,10 59,6 4,-3 15,-50 26,-104 14,-73 21,-159 25,-331 6,-265 -3,-385 -47,-605 -50,-247 -105,-393 -222,-592 -59,-98 -74,-167 -58,-256 23,-126 61,-177 174,-232 95,-47 169,-52 357,-28 125,16 244,22 505,26 667,12 1011,-35 1799,-241 277,-73 308,-86 376,-161 72,-79 113,-170 175,-388 19,-66 42,-130 50,-143 42,-63 155,-112 261,-112 109,0 124,57 54,206 -48,102 -53,149 -17,165 31,15 73,-5 102,-48 12,-18 45,-87 75,-153 57,-132 106,-197 173,-233 35,-18 56,-21 136,-20 73,2 110,-3 159,-19 89,-29 333,-32 403,-5 114,44 140,145 60,231 -23,24 -112,72 -272,143 -71,32 -199,124 -250,179 -71,78 -111,140 -224,343 -132,236 -172,297 -262,391 -58,61 -94,89 -148,115 -113,56 -85,53 -866,85 -223,9 -616,36 -656,45 -11,2 -86,12 -165,20 -179,20 -595,95 -712,130 -145,42 -319,139 -371,208 -32,42 -53,102 -67,187 -22,144 48,316 261,640 125,190 193,317 230,428 31,95 60,282 60,392 0,76 4,102 16,114 20,20 25,18 192,-102 73,-52 168,-114 210,-137 83,-47 132,-82 132,-95 0,-4 -14,-17 -32,-28 l -32,-21 29,-11 c 108,-41 305,-52 707,-39 186,6 345,9 353,5 8,-3 15,-15 15,-27 0,-20 3,-21 54,-15 110,13 266,81 426,187 89,58 99,63 170,69 161,14 229,78 523,495 88,124 143,185 169,185 2,0 2,-27 0,-60 -2,-33 0,-60 5,-60 23,0 140,145 212,263 117,192 165,317 271,712 106,394 111,410 135,410 15,0 22,-10 29,-40 13,-53 12,-52 42,-24 40,36 103,133 154,234 25,50 50,90 55,90 6,0 11,-17 13,-37 2,-21 7,-38 12,-38 19,0 93,101 139,190 29,56 87,206 140,363 50,147 114,318 142,380 132,288 199,526 246,867 25,190 25,756 0,975 -23,195 -53,379 -78,485 -46,194 -152,515 -233,706 -130,306 -218,564 -260,764 -48,228 -48,572 1,770 31,128 82,204 166,246 64,33 118,83 187,174 120,158 184,199 503,321 250,96 347,153 347,204 0,41 -42,57 -174,68 -112,9 -385,70 -439,99 -10,5 -27,29 -37,54 -23,53 -99,137 -159,176 -56,35 -72,36 -40,1 24,-26 23,-26 -13,3 -21,16 -42,41 -47,56 -9,26 -14,28 -60,28 -50,0 -101,16 -101,32 0,4 -42,8 -92,7 -88,0 -98,-3 -183,-43 z m 550,-126 c 10,-11 16,-20 13,-20 -3,0 -13,9 -23,20 -10,11 -16,20 -13,20 3,0 13,-9 23,-20 z"
|
||||
id="path1"
|
||||
style="fill:#cc43c5;fill-opacity:1" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
23
damus/Assets.xcassets/Hashtags/plebchain-hashtag.imageset/Contents.json
vendored
Normal file
23
damus/Assets.xcassets/Hashtags/plebchain-hashtag.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "plebchain.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "plebchain.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "plebchain.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
51
damus/Assets.xcassets/Hashtags/plebchain-hashtag.imageset/plebchain.svg
vendored
Normal file
51
damus/Assets.xcassets/Hashtags/plebchain-hashtag.imageset/plebchain.svg
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
viewBox="0 0 21.315096 18"
|
||||
width="21.315096"
|
||||
height="18"
|
||||
version="1.1"
|
||||
id="svg21"
|
||||
sodipodi:docname="plebchain.svg"
|
||||
inkscape:version="1.3-dev (77bc73e, 2022-05-18)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs21" />
|
||||
<sodipodi:namedview
|
||||
id="namedview21"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
showgrid="false"
|
||||
inkscape:zoom="19.666667"
|
||||
inkscape:cx="12.762712"
|
||||
inkscape:cy="12.991525"
|
||||
inkscape:window-width="1418"
|
||||
inkscape:window-height="883"
|
||||
inkscape:window-x="1745"
|
||||
inkscape:window-y="10"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg21" />
|
||||
<path
|
||||
d="M 18.625339,11.886754 C 17.668676,9.87076 15.553749,8.748431 12.298294,7.531368 12.258627,7.5164347 11.514296,7.2747022 10.649566,6.9942364 9.6532356,6.6713042 8.5682391,6.7520372 7.6316422,7.220569 L 5.7999816,8.136166 13.616623,17.9338 h 4.316652 l 0.703264,-1.662727 c 0.593598,-1.401862 0.641198,-3.009057 -0.0112,-4.384319 z"
|
||||
id="path5"
|
||||
style="fill:#bf26ed;fill-opacity:1;stroke-width:0.466665" />
|
||||
<path
|
||||
d="M 16.627546,5.1089093 C 16.710146,4.5484447 16.797412,3.95578 16.838479,3.6790475 17.083011,2.0219197 15.937348,0.48005806 14.28022,0.23552546 12.623092,-0.00900704 11.081231,1.1366559 10.836698,2.7937838 c -0.0406,0.2762657 -0.128333,0.8689305 -0.210932,1.4298619 -0.282799,0.058333 -0.513332,0.2851324 -0.558132,0.5870648 -0.0658,0.441465 0.084,1.0145298 0.370999,1.2072625 0.09707,1.4303286 1.118596,2.778524 2.547992,2.989457 1.429395,0.210933 2.796257,-0.785398 3.302589,-2.1265932 0.330399,-0.1021996 0.639331,-0.6071313 0.703731,-1.0490632 0.0434,-0.302399 -0.111533,-0.5856647 -0.365399,-0.7228643 z"
|
||||
id="path10"
|
||||
style="fill:#bf26ed;fill-opacity:1;stroke-width:0.466665" />
|
||||
<path
|
||||
d="m 21.193864,14.872477 c -0.0392,-0.501665 -0.310799,-0.956663 -0.684131,-1.294529 l -1.900727,-1.723394 c 0,0 0.172666,0.346732 0.332732,1.276796 L 15.53415,11.550288 c -0.878731,-0.405999 -1.835861,-0.616465 -2.804191,-0.616465 h -0.807331 c -0.87733,0 -1.681394,-0.553464 -1.9338602,-1.393928 C 9.8926348,9.220696 9.819835,8.832897 9.7904351,8.363432 V 8.362965 C 10.503966,7.7651672 10.944031,6.8285703 10.968765,5.8700401 11.247364,5.6651742 11.372897,5.0865094 11.28843,4.648311 11.230564,4.3482453 10.990698,4.1317127 10.706032,4.0850462 10.599632,3.5283147 10.487633,2.93985 10.435366,2.6654509 10.121301,1.0199896 8.5327726,-0.05940684 6.8873113,0.25419216 5.2418501,0.56779106 4.1624536,2.1567859 4.4760526,3.8022471 4.5283191,4.0766462 4.6407854,4.665111 4.7467184,5.2218423 4.4989192,5.3697751 4.355653,5.6595742 4.4125861,5.9596399 4.4956525,6.3983051 4.8255848,6.8901701 5.159717,6.9779032 5.2950499,7.2952355 5.4798493,7.5892345 5.7015152,7.8510336 L 5.7999816,8.136166 5.2171168,11.699621 3.2925898,12.11822 C 3.041524,12.177953 2.8109914,12.278752 2.6061254,12.411752 1.8342613,12.912484 1.5458622,13.865414 1.7731282,14.725478 1.8309946,14.943877 2.8833245,17.9338 2.8833245,17.9338 H 15.133284 l 0.408332,-1.691661 3.480855,0.605265 c 1.184397,0.211399 2.267993,-0.748064 2.171393,-1.974927 z"
|
||||
id="path16"
|
||||
style="fill:#f59119;fill-opacity:1;stroke-width:0.466665" />
|
||||
<path
|
||||
d="m 12.589026,15.06801 -2.251659,-0.776531 c 0,0 0.96693,-0.1106 0.95853,-0.625798 -4.67e-4,-0.03687 -0.01213,-0.240332 -0.307999,-0.210932 -0.0812,0.0079 -0.734064,0.07327 -1.1927962,0.118999 -0.3028657,0.03033 -0.6015314,0.09007 -0.8927304,0.178733 l -0.1591328,0.04853 -3.5256551,-2.101393 c 0,0 0,0 0.3495322,-0.763464 C 6.1779803,9.600561 5.8004482,8.136166 5.8004482,8.136166 L 1.324663,10.373825 C 0.63539857,10.718224 0.2,11.422889 0.2,12.193819 c 0,0.939397 0.64306455,1.756528 1.5558615,1.977727 l 6.4357789,1.371062 2.5834586,0.955731 c 0.167999,0.0532 0.329932,0.0602 0.517065,0.0033 0.187599,-0.05693 0.246399,-0.09847 0.337398,-0.165199 0.2464,-0.180133 0.706065,-0.536665 1.037864,-0.795664 0.170799,-0.133933 0.125533,-0.402266 -0.0784,-0.472732 z"
|
||||
id="path21"
|
||||
style="fill:#bf26ed;fill-opacity:1;stroke-width:0.466665" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
23
damus/Assets.xcassets/Hashtags/zap-hashtag.imageset/Contents.json
vendored
Normal file
23
damus/Assets.xcassets/Hashtags/zap-hashtag.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "zapathon.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "zapathon.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "zapathon.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
60
damus/Assets.xcassets/Hashtags/zap-hashtag.imageset/zapathon.svg
vendored
Normal file
60
damus/Assets.xcassets/Hashtags/zap-hashtag.imageset/zapathon.svg
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="11.106925"
|
||||
height="18"
|
||||
viewBox="0 0 2.9387073 4.7624999"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.3-dev (77bc73e, 2022-05-18)"
|
||||
sodipodi:docname="zapathon.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="16"
|
||||
inkscape:cx="16.96875"
|
||||
inkscape:cy="9.96875"
|
||||
inkscape:window-width="1406"
|
||||
inkscape:window-height="767"
|
||||
inkscape:window-x="1741"
|
||||
inkscape:window-y="214"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1" />
|
||||
<defs
|
||||
id="defs1" />
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-10.993855,-8.058313)">
|
||||
<g
|
||||
transform="matrix(0.01604881,0,0,-0.01604881,10.573102,13.422443)"
|
||||
id="g10"
|
||||
inkscape:label="Bolt"
|
||||
style="display:inline">
|
||||
<path
|
||||
id="path14"
|
||||
style="fill:#c98f19;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1"
|
||||
d="m 94.833712,155.13322 -8.129016,18.00903 -55.167069,-3.59001 18.868958,-24.19503 44.825648,2.77303 z m 83.691438,80.98476 -8.89814,-11.93085 -11.755,-16.519 -1.505,-2.114 -43.191,-60.691 L 63.081111,74.14633 79.705774,38.853232 C 121.34411,95.870625 162.2814,153.39008 203.27601,210.87013 Z m -22.57414,11.14115 12.32197,61.15287 -21.22997,22.33399 -17.432,-69.53486 -7.0256,-30.70796 27.52164,-9.9823 5.84396,26.73826"
|
||||
sodipodi:nodetypes="cccccccccccccccccccccc" />
|
||||
<path
|
||||
id="path16"
|
||||
style="fill:#fadf05;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1"
|
||||
d="m 178.78514,236.43412 -5.679,-0.377 -8.065,-0.533 -5.9,-0.389 -33.394,-2.212 6.162,29.388 6.223,29.667 8.95029,38.92409 -54.426393,-75.90909 -61.5969,-86.55 44.1332,2.921 8.9137,0.589 -6.1723,-29.396 -0.9867,-4.71 -1.5106,-7.209 -11.7273,-55.917704 16.3074,22.9175 37.169603,52.234204 43.189,60.693 1.505,2.113 16.907,23.756 h -0.002"
|
||||
sodipodi:nodetypes="cccccccccccccccccccccc" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -12,11 +12,14 @@ class DamusColors {
|
||||
static let adaptableGrey = Color("DamusAdaptableGrey")
|
||||
static let white = Color("DamusWhite")
|
||||
static let black = Color("DamusBlack")
|
||||
static let brown = Color("DamusBrown")
|
||||
static let yellow = Color("DamusYellow")
|
||||
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 deepPurple = Color("DamusDeepPurple")
|
||||
static let blue = Color("DamusBlue")
|
||||
}
|
||||
|
||||
|
||||
44
damus/Components/IconLabel.swift
Normal file
44
damus/Components/IconLabel.swift
Normal file
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// IconLabel.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-05.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct IconLabel: View {
|
||||
let text: String
|
||||
let img_name: String
|
||||
let img_color: Color
|
||||
|
||||
init(_ text: String, img_name: String, color: Color) {
|
||||
self.text = text
|
||||
self.img_name = img_name
|
||||
self.img_color = color
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
Image(systemName: img_name)
|
||||
.foregroundColor(img_color)
|
||||
.frame(width: 20)
|
||||
.padding([.trailing], 20)
|
||||
Text(text)
|
||||
}
|
||||
}}
|
||||
|
||||
struct IconLabel_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Form {
|
||||
Section {
|
||||
IconLabel(NSLocalizedString("Keys", comment: "Settings section for managing keys"), img_name: "key.fill", color: .orange)
|
||||
|
||||
IconLabel(NSLocalizedString("Local Notifications", comment: "Section header for damus local notifications user configuration"), img_name: "bell.fill", color: .blue)
|
||||
|
||||
IconLabel(NSLocalizedString("Appearance", comment: "Section header for text and appearance settings"), img_name: "textformat", color: .red)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,12 +32,42 @@ struct ShareSheet: UIViewControllerRepresentable {
|
||||
}
|
||||
|
||||
|
||||
enum ImageShape {
|
||||
case square
|
||||
case landscape
|
||||
case portrait
|
||||
case unknown
|
||||
}
|
||||
|
||||
|
||||
struct ImageCarousel: View {
|
||||
var urls: [URL]
|
||||
|
||||
@State var open_sheet: Bool = false
|
||||
@State var current_url: URL? = nil
|
||||
let evid: String
|
||||
let previews: PreviewCache
|
||||
|
||||
@State private var open_sheet: Bool = false
|
||||
@State private var current_url: URL? = nil
|
||||
@State private var image_fill: ImageFill? = nil
|
||||
@State private var fillHeight: CGFloat = 350
|
||||
@State private var maxHeight: CGFloat = UIScreen.main.bounds.height * 0.85
|
||||
|
||||
init(previews: PreviewCache, evid: String, urls: [URL]) {
|
||||
_open_sheet = State(initialValue: false)
|
||||
_current_url = State(initialValue: nil)
|
||||
_image_fill = State(initialValue: previews.lookup_image_meta(evid))
|
||||
self.urls = urls
|
||||
self.evid = evid
|
||||
self.previews = previews
|
||||
}
|
||||
|
||||
var filling: Bool {
|
||||
image_fill?.filling == true
|
||||
}
|
||||
|
||||
var height: CGFloat {
|
||||
image_fill?.height ?? 0
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
TabView {
|
||||
@@ -45,30 +75,32 @@ struct ImageCarousel: View {
|
||||
Rectangle()
|
||||
.foregroundColor(Color.clear)
|
||||
.overlay {
|
||||
KFAnimatedImage(url)
|
||||
.imageContext(.note)
|
||||
.cancelOnDisappear(true)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.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
|
||||
// }
|
||||
// }
|
||||
GeometryReader { geo in
|
||||
KFAnimatedImage(url)
|
||||
.callbackQueue(.dispatch(.global(qos:.background)))
|
||||
.backgroundDecode(true)
|
||||
.imageContext(.note)
|
||||
.cancelOnDisappear(true)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in
|
||||
previews.cache_image_meta(evid: evid, image_fill: fill)
|
||||
image_fill = fill
|
||||
}
|
||||
.aspectRatio(contentMode: filling ? .fill : .fit)
|
||||
.tabItem {
|
||||
Text(url.absoluteString)
|
||||
}
|
||||
.id(url.absoluteString)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $open_sheet) {
|
||||
ImageView(urls: urls)
|
||||
}
|
||||
.frame(height: 350)
|
||||
.frame(height: height)
|
||||
.onTapGesture {
|
||||
open_sheet = true
|
||||
}
|
||||
@@ -76,8 +108,71 @@ struct ImageCarousel: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct ImageCarousel_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ImageCarousel(urls: [URL(string: "https://jb55.com/red-me.jpg")!,URL(string: "https://jb55.com/red-me.jpg")!])
|
||||
// MARK: - Image Modifier
|
||||
extension KFOptionSetter {
|
||||
/// Sets a block to get image size
|
||||
///
|
||||
/// - Parameter block: The block which is used to read the image object.
|
||||
/// - Returns: `Self` value after read size
|
||||
public func imageFill(for size: CGSize, max: CGFloat, fill: CGFloat, block: @escaping (ImageFill) throws -> Void) -> Self {
|
||||
let modifier = AnyImageModifier { image -> KFCrossPlatformImage in
|
||||
let img_size = image.size
|
||||
let geo_size = size
|
||||
let fill = ImageFill.calculate_image_fill(geo_size: geo_size,
|
||||
img_size: img_size,
|
||||
maxHeight: max,
|
||||
fillHeight: fill)
|
||||
DispatchQueue.main.async { [block, fill] in
|
||||
try? block(fill)
|
||||
}
|
||||
return image
|
||||
}
|
||||
options.imageModifier = modifier
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public struct ImageFill {
|
||||
let filling: Bool?
|
||||
let height: CGFloat
|
||||
|
||||
|
||||
static func determine_image_shape(_ size: CGSize) -> ImageShape {
|
||||
guard size.height > 0 else {
|
||||
return .unknown
|
||||
}
|
||||
let imageRatio = size.width / size.height
|
||||
switch imageRatio {
|
||||
case 1.0: return .square
|
||||
case ..<1.0: return .portrait
|
||||
case 1.0...: return .landscape
|
||||
default: return .unknown
|
||||
}
|
||||
}
|
||||
|
||||
static func calculate_image_fill(geo_size: CGSize, img_size: CGSize, maxHeight: CGFloat, fillHeight: CGFloat) -> ImageFill {
|
||||
let shape = determine_image_shape(img_size)
|
||||
|
||||
let xfactor = geo_size.width / img_size.width
|
||||
let scaled = img_size.height * xfactor
|
||||
// calculate scaled image height
|
||||
// set scale factor and constrain images to minimum 150
|
||||
// and animations to scaled factor for dynamic size adjustment
|
||||
switch shape {
|
||||
case .portrait, .landscape:
|
||||
let filling = scaled > maxHeight
|
||||
let height = filling ? fillHeight : scaled
|
||||
return ImageFill(filling: filling, height: height)
|
||||
case .square, .unknown:
|
||||
return ImageFill(filling: nil, height: scaled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ImageCarousel_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ImageCarousel(previews: test_damus_state().previews, evid: "evid", urls: [URL(string: "https://jb55.com/red-me.jpg")!,URL(string: "https://jb55.com/red-me.jpg")!])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,14 @@ struct SelectableText: View {
|
||||
@State private var selectedTextHeight: CGFloat = .zero
|
||||
@State private var selectedTextWidth: CGFloat = .zero
|
||||
|
||||
let size: EventViewKind
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
TextViewRepresentable(
|
||||
attributedString: attributedString,
|
||||
textColor: UIColor.label,
|
||||
font: UIFont.preferredFont(forTextStyle: .title2),
|
||||
font: eventviewsize_to_uifont(size),
|
||||
fixedWidth: selectedTextWidth,
|
||||
height: $selectedTextHeight
|
||||
)
|
||||
|
||||
22
damus/Components/ThiccDivider.swift
Normal file
22
damus/Components/ThiccDivider.swift
Normal file
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// ThiccDivider.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-03.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ThiccDivider: View {
|
||||
var body: some View {
|
||||
Rectangle()
|
||||
.frame(height: 4)
|
||||
.foregroundColor(DamusColors.adaptableGrey)
|
||||
}
|
||||
}
|
||||
|
||||
struct ThiccDivider_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ThiccDivider()
|
||||
}
|
||||
}
|
||||
@@ -8,117 +8,168 @@
|
||||
import SwiftUI
|
||||
import NaturalLanguage
|
||||
|
||||
|
||||
struct Translated: Equatable {
|
||||
let artifacts: NoteArtifacts
|
||||
let language: String
|
||||
}
|
||||
|
||||
enum TranslateStatus: Equatable {
|
||||
case havent_tried
|
||||
case trying
|
||||
case translating
|
||||
case translated(Translated)
|
||||
case not_needed
|
||||
}
|
||||
|
||||
struct TranslateView: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
let size: EventViewKind
|
||||
let currentLanguage: String
|
||||
|
||||
@State var checkingTranslationStatus: Bool = false
|
||||
@State var currentLanguage: String = "en"
|
||||
@State var noteLanguage: String? = nil
|
||||
@State var translated_note: String? = nil
|
||||
@State var show_translated_note: Bool = false
|
||||
@State var translated_artifacts: NoteArtifacts? = nil
|
||||
@State var translated: TranslateStatus
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind) {
|
||||
self.damus_state = damus_state
|
||||
self.event = event
|
||||
self.size = size
|
||||
|
||||
if #available(iOS 16, *) {
|
||||
self.currentLanguage = Locale.current.language.languageCode?.identifier ?? "en"
|
||||
} else {
|
||||
self.currentLanguage = Locale.current.languageCode ?? "en"
|
||||
}
|
||||
|
||||
if damus_state.pubkey == event.pubkey && damus_state.is_privkey_user {
|
||||
// Do not translate self-authored notes if logged in with a private key
|
||||
// as we can assume the user can understand their own notes.
|
||||
// The detected language prediction could be incorrect and not in the list of preferred languages.
|
||||
// Offering a translation in this case is definitely incorrect so let's avoid it altogether.
|
||||
self._translated = State(initialValue: .not_needed)
|
||||
} else if let cached = damus_state.events.lookup_translated_artifacts(evid: event.id) {
|
||||
self._translated = State(initialValue: cached)
|
||||
} else {
|
||||
let initval: TranslateStatus = self.damus_state.settings.auto_translate ? .trying : .havent_tried
|
||||
self._translated = State(initialValue: initval)
|
||||
}
|
||||
}
|
||||
|
||||
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
|
||||
|
||||
var TranslateButton: some View {
|
||||
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
|
||||
show_translated_note = true
|
||||
self.translated = .trying
|
||||
}
|
||||
.translate_button_style()
|
||||
}
|
||||
|
||||
func Translated(lang: String, artifacts: NoteArtifacts) -> some View {
|
||||
return Group {
|
||||
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()
|
||||
func TranslatedView(lang: String?, artifacts: NoteArtifacts) -> some View {
|
||||
return VStack(alignment: .leading) {
|
||||
Text(String(format: NSLocalizedString("Translated from %@", comment: "Button to indicate that the note has been translated from a different language."), lang ?? "ja"))
|
||||
.foregroundColor(.gray)
|
||||
.font(.footnote)
|
||||
.padding([.top, .bottom], 10)
|
||||
|
||||
SelectableText(attributedString: artifacts.content)
|
||||
}
|
||||
}
|
||||
|
||||
func CheckingStatus(lang: String) -> some View {
|
||||
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()
|
||||
}
|
||||
|
||||
func MainContent(note_lang: String) -> some View {
|
||||
return Group {
|
||||
let languageName = Locale.current.localizedString(forLanguageCode: note_lang)
|
||||
if let lang = languageName, show_translated_note {
|
||||
if checkingTranslationStatus {
|
||||
CheckingStatus(lang: lang)
|
||||
} else if let artifacts = translated_artifacts {
|
||||
Translated(lang: lang, artifacts: artifacts)
|
||||
}
|
||||
if self.size == .selected {
|
||||
SelectableText(attributedString: artifacts.content.attributed, size: self.size)
|
||||
} else {
|
||||
TranslateButton
|
||||
artifacts.content.text
|
||||
.font(eventviewsize_to_font(self.size))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func failed_attempt() {
|
||||
DispatchQueue.main.async {
|
||||
self.translated = .not_needed
|
||||
damus_state.events.store_translation_artifacts(evid: event.id, translated: .not_needed)
|
||||
}
|
||||
}
|
||||
|
||||
func attempt_translation() async {
|
||||
guard case .trying = translated else {
|
||||
return
|
||||
}
|
||||
|
||||
guard damus_state.settings.can_translate(damus_state.pubkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
let note_lang = event.note_language(damus_state.keypair.privkey) ?? currentLanguage
|
||||
|
||||
// Don't translate if its in our preferred languages
|
||||
guard !preferredLanguages.contains(note_lang) else {
|
||||
failed_attempt()
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.translated = .translating
|
||||
}
|
||||
|
||||
// If the note language is different from our preferred languages, send a translation request.
|
||||
let translator = Translator(damus_state.settings)
|
||||
let originalContent = event.get_content(damus_state.keypair.privkey)
|
||||
let translated_note = try? await translator.translate(originalContent, from: note_lang, to: currentLanguage)
|
||||
|
||||
guard let translated_note else {
|
||||
// if its the same, give up and don't retry
|
||||
failed_attempt()
|
||||
return
|
||||
}
|
||||
|
||||
guard originalContent != translated_note else {
|
||||
// if its the same, give up and don't retry
|
||||
failed_attempt()
|
||||
return
|
||||
}
|
||||
|
||||
// Render translated note
|
||||
let translated_blocks = event.get_blocks(content: translated_note)
|
||||
let artifacts = render_blocks(blocks: translated_blocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
|
||||
|
||||
// and cache it
|
||||
DispatchQueue.main.async {
|
||||
self.translated = .translated(Translated(artifacts: artifacts, language: note_lang))
|
||||
damus_state.events.store_translation_artifacts(evid: event.id, translated: self.translated)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let note_lang = noteLanguage, noteLanguage != currentLanguage {
|
||||
MainContent(note_lang: note_lang)
|
||||
} else {
|
||||
switch translated {
|
||||
case .havent_tried:
|
||||
if damus_state.settings.auto_translate {
|
||||
Text("")
|
||||
} else {
|
||||
TranslateButton
|
||||
}
|
||||
case .trying:
|
||||
Text("")
|
||||
case .translating:
|
||||
Text("Translating...", comment: "Text to display when waiting for the translation of a note to finish processing before showing it.")
|
||||
.foregroundColor(.gray)
|
||||
.font(.footnote)
|
||||
.padding([.top, .bottom], 10)
|
||||
case .translated(let translated):
|
||||
let languageName = Locale.current.localizedString(forLanguageCode: translated.language)
|
||||
TranslatedView(lang: languageName, artifacts: translated.artifacts)
|
||||
case .not_needed:
|
||||
Text("")
|
||||
}
|
||||
}
|
||||
.onChange(of: translated) { val in
|
||||
guard case .trying = translated else {
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
await attempt_translation()
|
||||
}
|
||||
}
|
||||
.task {
|
||||
guard noteLanguage == nil && !checkingTranslationStatus && damus_state.settings.can_translate(damus_state.pubkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
checkingTranslationStatus = true
|
||||
|
||||
if #available(iOS 16, *) {
|
||||
currentLanguage = Locale.current.language.languageCode?.identifier ?? "en"
|
||||
} else {
|
||||
currentLanguage = Locale.current.languageCode ?? "en"
|
||||
}
|
||||
|
||||
noteLanguage = event.note_language(damus_state.keypair.privkey) ?? currentLanguage
|
||||
|
||||
guard let note_lang = noteLanguage else {
|
||||
noteLanguage = currentLanguage
|
||||
translated_note = nil
|
||||
checkingTranslationStatus = false
|
||||
return
|
||||
}
|
||||
|
||||
if !preferredLanguages.contains(note_lang) {
|
||||
do {
|
||||
// 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)
|
||||
|
||||
if originalContent == translated_note {
|
||||
// If the translation is the same as the original, don't bother showing it.
|
||||
noteLanguage = currentLanguage
|
||||
translated_note = nil
|
||||
}
|
||||
} catch {
|
||||
// If for whatever reason we're not able to figure out the language of the note, or translate the note, fail gracefully and do not retry. It's not the end of the world. Don't want to take down someone's translation server with an accidental denial of service attack.
|
||||
noteLanguage = currentLanguage
|
||||
translated_note = nil
|
||||
}
|
||||
}
|
||||
|
||||
if let translated = translated_note {
|
||||
// Render translated note.
|
||||
let translatedBlocks = event.get_blocks(content: translated)
|
||||
translated_artifacts = render_blocks(blocks: translatedBlocks, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
|
||||
}
|
||||
|
||||
checkingTranslationStatus = false
|
||||
|
||||
show_translated_note = damus_state.settings.auto_translate
|
||||
await attempt_translation()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,6 +186,6 @@ extension View {
|
||||
struct TranslateView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let ds = test_damus_state()
|
||||
TranslateView(damus_state: ds, event: test_event)
|
||||
TranslateView(damus_state: ds, event: test_event, size: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
53
damus/Components/TruncatedText.swift
Normal file
53
damus/Components/TruncatedText.swift
Normal file
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// TruncatedText.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-06.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TruncatedText: View {
|
||||
let text: CompatibleText
|
||||
let maxChars: Int = 280
|
||||
|
||||
var body: some View {
|
||||
let truncatedAttributedString: AttributedString? = getTruncatedString()
|
||||
|
||||
if let truncatedAttributedString {
|
||||
Text(truncatedAttributedString)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
} else {
|
||||
text.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? {
|
||||
let nsAttributedString = NSAttributedString(text.attributed)
|
||||
if nsAttributedString.length < maxChars { return nil }
|
||||
|
||||
let range = NSRange(location: 0, length: maxChars)
|
||||
let truncatedAttributedString = nsAttributedString.attributedSubstring(from: range)
|
||||
|
||||
return AttributedString(truncatedAttributedString) + "..."
|
||||
}
|
||||
}
|
||||
|
||||
struct TruncatedText_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack(spacing: 100) {
|
||||
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven"))
|
||||
.frame(width: 200, height: 200)
|
||||
|
||||
TruncatedText(text: CompatibleText(stringLiteral: "hello\nthere\none\ntwo\nthree\nfour"))
|
||||
.frame(width: 200, height: 200)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,27 +7,51 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct UserViewRow: View {
|
||||
let damus_state: DamusState
|
||||
let pubkey: String
|
||||
|
||||
@State var navigating: Bool = false
|
||||
|
||||
var body: some View {
|
||||
let dest = ProfileView(damus_state: damus_state, pubkey: pubkey)
|
||||
|
||||
UserView(damus_state: damus_state, pubkey: pubkey)
|
||||
.contentShape(Rectangle())
|
||||
.background(
|
||||
NavigationLink(destination: dest, isActive: $navigating) {
|
||||
EmptyView()
|
||||
}
|
||||
)
|
||||
.onTapGesture {
|
||||
navigating = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct UserView: View {
|
||||
let damus_state: DamusState
|
||||
let pubkey: String
|
||||
|
||||
var body: some View {
|
||||
NavigationLink(destination: ProfileView(damus_state: damus_state, pubkey: pubkey)) {
|
||||
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
let profile = damus_state.profiles.lookup(id: pubkey)
|
||||
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
|
||||
if let about = profile?.about {
|
||||
Text(about)
|
||||
.lineLimit(3)
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
VStack {
|
||||
HStack {
|
||||
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
|
||||
|
||||
Spacer()
|
||||
VStack(alignment: .leading) {
|
||||
let profile = damus_state.profiles.lookup(id: pubkey)
|
||||
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
|
||||
if let about = profile?.about {
|
||||
Text(about)
|
||||
.lineLimit(3)
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ struct WebsiteLink: View {
|
||||
}, label: {
|
||||
Text(link_text)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(LINEAR_GRADIENT)
|
||||
.foregroundColor(.accentColor)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ struct ZapButton: View {
|
||||
|
||||
struct ZapButton_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let bar = ActionBarModel(likes: 0, boosts: 0, zaps: 10, zap_total: 15623414, our_like: nil, our_boost: nil, our_zap: nil)
|
||||
let bar = ActionBarModel(likes: 0, boosts: 0, zaps: 10, zap_total: 15623414, replies: 2, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
|
||||
ZapButton(damus_state: test_damus_state(), event: test_event, lnurl: "lnurl", bar: bar)
|
||||
}
|
||||
}
|
||||
@@ -173,7 +173,7 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
|
||||
damus_state.lnurls.endpoints[target.pubkey] = payreq
|
||||
}
|
||||
|
||||
let zap_amount = amount_sats ?? get_default_zap_amount(pubkey: damus_state.pubkey) ?? 1000
|
||||
let zap_amount = amount_sats ?? get_default_zap_amount(pubkey: damus_state.pubkey)
|
||||
|
||||
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount, zap_type: zap_type, comment: comment) else {
|
||||
DispatchQueue.main.async {
|
||||
|
||||
@@ -8,30 +8,22 @@
|
||||
import SwiftUI
|
||||
import Starscream
|
||||
|
||||
var BOOTSTRAP_RELAYS = [
|
||||
"wss://relay.damus.io",
|
||||
"wss://eden.nostr.land",
|
||||
"wss://nostr.wine",
|
||||
"wss://nos.lol",
|
||||
]
|
||||
|
||||
struct TimestampedProfile {
|
||||
let profile: Profile
|
||||
let timestamp: Int64
|
||||
let event: NostrEvent
|
||||
}
|
||||
|
||||
enum Sheets: Identifiable {
|
||||
case post
|
||||
case post(PostAction)
|
||||
case report(ReportTarget)
|
||||
case reply(NostrEvent)
|
||||
case event(NostrEvent)
|
||||
case filter
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .report: return "report"
|
||||
case .post: return "post"
|
||||
case .reply(let ev): return "reply-" + ev.id
|
||||
case .post(let action): return "post-" + (action.ev?.id ?? "")
|
||||
case .event(let ev): return "event-" + ev.id
|
||||
case .filter: return "filter"
|
||||
}
|
||||
@@ -72,7 +64,6 @@ struct ContentView: View {
|
||||
@State var active_sheet: Sheets? = nil
|
||||
@State var damus_state: DamusState? = nil
|
||||
@State var selected_timeline: Timeline? = .home
|
||||
@State var is_thread_open: Bool = false
|
||||
@State var is_deleted_account: Bool = false
|
||||
@State var is_profile_open: Bool = false
|
||||
@State var event: NostrEvent? = nil
|
||||
@@ -82,14 +73,15 @@ struct ContentView: View {
|
||||
@State var profile_open: Bool = false
|
||||
@State var thread_open: Bool = false
|
||||
@State var search_open: Bool = false
|
||||
@State var blocking: String? = nil
|
||||
@State var confirm_block: Bool = false
|
||||
@State var user_blocked_confirm: Bool = false
|
||||
@State var muting: String? = nil
|
||||
@State var confirm_mute: Bool = false
|
||||
@State var user_muted_confirm: Bool = false
|
||||
@State var confirm_overwrite_mutelist: Bool = false
|
||||
@State var current_boost: NostrEvent? = nil
|
||||
@State var filter_state : FilterState = .posts_and_replies
|
||||
@State private var isSideBarOpened = false
|
||||
@StateObject var home: HomeModel = HomeModel()
|
||||
@State var shouldShowBoostAlert = false
|
||||
|
||||
// connect retry timer
|
||||
let timer = Timer.publish(every: 4, on: .main, in: .common).autoconnect()
|
||||
@@ -97,11 +89,19 @@ struct ContentView: View {
|
||||
let sub_id = UUID().description
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
|
||||
var mystery: some View {
|
||||
Text("Are you lost?", comment: "Text asking the user if they are lost in the app.")
|
||||
.id("what")
|
||||
}
|
||||
|
||||
var PostingTimelineView: some View {
|
||||
VStack {
|
||||
ZStack {
|
||||
TabView(selection: $filter_state) {
|
||||
// This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why.
|
||||
mystery
|
||||
|
||||
contentTimelineView(filter: FilterState.posts.filter)
|
||||
.tag(FilterState.posts)
|
||||
.id(FilterState.posts)
|
||||
@@ -113,7 +113,7 @@ struct ContentView: View {
|
||||
|
||||
if privkey != nil {
|
||||
PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) {
|
||||
self.active_sheet = .post
|
||||
self.active_sheet = .post(.posting)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,22 +147,8 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
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: "")
|
||||
}
|
||||
return Text(timeline_name(selected_timeline))
|
||||
.bold()
|
||||
}
|
||||
|
||||
func MainContent(damus: DamusState) -> some View {
|
||||
@@ -196,8 +182,7 @@ struct ContentView: View {
|
||||
NotificationsView(state: damus, notifications: home.notifications)
|
||||
|
||||
case .dms:
|
||||
DirectMessagesView(damus_state: damus_state!)
|
||||
.environmentObject(home.dms)
|
||||
DirectMessagesView(damus_state: damus_state!, model: damus_state!.dms)
|
||||
|
||||
case .none:
|
||||
EmptyView()
|
||||
@@ -248,9 +233,9 @@ struct ContentView: View {
|
||||
|
||||
func MaybeReportView(target: ReportTarget) -> some View {
|
||||
Group {
|
||||
if let ds = damus_state {
|
||||
if let sec = ds.keypair.privkey {
|
||||
ReportView(pool: ds.pool, target: target, privkey: sec)
|
||||
if let damus_state {
|
||||
if let sec = damus_state.keypair.privkey {
|
||||
ReportView(postbox: damus_state.postbox, target: target, privkey: sec)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
@@ -260,6 +245,11 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func open_event(ev: NostrEvent) {
|
||||
self.active_event = ev
|
||||
self.thread_open = true
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if let damus = self.damus_state {
|
||||
@@ -280,13 +270,7 @@ struct ContentView: View {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
SignalView(state: damus_state!, signal: home.signal)
|
||||
|
||||
// maybe expand this to other timelines in the future
|
||||
if selected_timeline == .search {
|
||||
@@ -311,7 +295,7 @@ struct ContentView: View {
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
|
||||
TabBar(new_events: $home.new_events, selected: $selected_timeline, isSidebarVisible: $isSideBarOpened, action: switch_timeline)
|
||||
TabBar(new_events: $home.new_events, selected: $selected_timeline, settings: damus.settings, action: switch_timeline)
|
||||
.padding([.bottom], 8)
|
||||
.background(Color(uiColor: .systemBackground).ignoresSafeArea())
|
||||
}
|
||||
@@ -325,10 +309,8 @@ struct ContentView: View {
|
||||
switch item {
|
||||
case .report(let target):
|
||||
MaybeReportView(target: target)
|
||||
case .post:
|
||||
PostView(replying_to: nil, references: [], damus_state: damus_state!)
|
||||
case .reply(let event):
|
||||
ReplyView(replying_to: event, damus: damus_state!)
|
||||
case .post(let action):
|
||||
PostView(action: action, damus_state: damus_state!)
|
||||
case .event:
|
||||
EventDetailView()
|
||||
case .filter:
|
||||
@@ -355,10 +337,9 @@ struct ContentView: View {
|
||||
} else if ref.key == "e" {
|
||||
find_event(state: damus_state!, evid: ref.ref_id, search_type: .event, find_from: nil) { ev in
|
||||
if let ev {
|
||||
active_event = ev
|
||||
open_event(ev: ev)
|
||||
}
|
||||
}
|
||||
thread_open = true
|
||||
}
|
||||
case .filter(let filt):
|
||||
active_search = filt
|
||||
@@ -369,16 +350,16 @@ struct ContentView: View {
|
||||
|
||||
}
|
||||
.onReceive(handle_notify(.boost)) { notif in
|
||||
current_boost = (notif.object as? NostrEvent)
|
||||
}
|
||||
.onReceive(handle_notify(.open_thread)) { obj in
|
||||
//let ev = obj.object as! NostrEvent
|
||||
//thread.set_active_event(ev)
|
||||
//is_thread_open = true
|
||||
guard let ev = notif.object as? NostrEvent else {
|
||||
return
|
||||
}
|
||||
|
||||
current_boost = ev
|
||||
shouldShowBoostAlert = true
|
||||
}
|
||||
.onReceive(handle_notify(.reply)) { notif in
|
||||
let ev = notif.object as! NostrEvent
|
||||
self.active_sheet = .reply(ev)
|
||||
self.active_sheet = .post(.replying_to(ev))
|
||||
}
|
||||
.onReceive(handle_notify(.like)) { like in
|
||||
}
|
||||
@@ -389,14 +370,20 @@ struct ContentView: View {
|
||||
let target = notif.object as! ReportTarget
|
||||
self.active_sheet = .report(target)
|
||||
}
|
||||
.onReceive(handle_notify(.block)) { notif in
|
||||
.onReceive(handle_notify(.mute)) { notif in
|
||||
let pubkey = notif.object as! String
|
||||
self.blocking = pubkey
|
||||
self.confirm_block = true
|
||||
self.muting = pubkey
|
||||
self.confirm_mute = true
|
||||
}
|
||||
.onReceive(handle_notify(.broadcast_event)) { obj in
|
||||
let ev = obj.object as! NostrEvent
|
||||
self.damus_state?.pool.send(.event(ev))
|
||||
guard let ds = self.damus_state else {
|
||||
return
|
||||
}
|
||||
ds.postbox.send(ev)
|
||||
if let profile = ds.profiles.profiles[ev.pubkey] {
|
||||
ds.postbox.send(profile.event)
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.unfollow)) { notif in
|
||||
guard let privkey = self.privkey else {
|
||||
@@ -410,7 +397,7 @@ struct ContentView: View {
|
||||
let target = notif.object as! FollowTarget
|
||||
let pk = target.pubkey
|
||||
|
||||
if let ev = unfollow_user(pool: damus.pool,
|
||||
if let ev = unfollow_user(postbox: damus.postbox,
|
||||
our_contacts: damus.contacts.event,
|
||||
pubkey: damus.pubkey,
|
||||
privkey: privkey,
|
||||
@@ -461,7 +448,16 @@ struct ContentView: View {
|
||||
//let to_relays = tup.1
|
||||
print("post \(post.content)")
|
||||
let new_ev = post_to_event(post: post, privkey: privkey, pubkey: pubkey)
|
||||
self.damus_state?.pool.send(.event(new_ev))
|
||||
guard let ds = self.damus_state else {
|
||||
return
|
||||
}
|
||||
ds.postbox.send(new_ev)
|
||||
for eref in new_ev.referenced_ids.prefix(3) {
|
||||
// also broadcast at most 3 referenced events
|
||||
if let ev = ds.events.lookup(eref.ref_id) {
|
||||
ds.postbox.send(ev)
|
||||
}
|
||||
}
|
||||
case .cancel:
|
||||
active_sheet = nil
|
||||
print("post cancelled")
|
||||
@@ -471,7 +467,39 @@ struct ContentView: View {
|
||||
self.damus_state?.pool.connect_to_disconnected()
|
||||
}
|
||||
.onReceive(handle_notify(.new_mutes)) { notif in
|
||||
home.filter_muted()
|
||||
home.filter_events()
|
||||
}
|
||||
.onReceive(handle_notify(.mute_thread)) { notif in
|
||||
home.filter_events()
|
||||
}
|
||||
.onReceive(handle_notify(.unmute_thread)) { notif in
|
||||
home.filter_events()
|
||||
}
|
||||
.onReceive(handle_notify(.local_notification)) { notif in
|
||||
|
||||
guard let local = notif.object as? LossyLocalNotification,
|
||||
let damus_state else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let target = damus_state.events.lookup(local.event_id) else {
|
||||
return
|
||||
}
|
||||
|
||||
switch local.type {
|
||||
case .dm:
|
||||
selected_timeline = .dms
|
||||
damus_state.dms.open_dm_by_pk(target.pubkey)
|
||||
|
||||
case .like: fallthrough
|
||||
case .zap: fallthrough
|
||||
case .mention: fallthrough
|
||||
case .repost:
|
||||
open_event(ev: target)
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.hide_reactions)) { notif in
|
||||
home.filter_events()
|
||||
}
|
||||
.alert(NSLocalizedString("Deleted Account", comment: "Alert message to indicate this is a deleted account"), isPresented: $is_deleted_account) {
|
||||
Button(NSLocalizedString("Logout", comment: "Button to close the alert that informs that the current account has been deleted.")) {
|
||||
@@ -479,23 +507,23 @@ struct ContentView: View {
|
||||
notify(.logout, ())
|
||||
}
|
||||
}
|
||||
.alert(NSLocalizedString("User blocked", comment: "Alert message to indicate the user has been blocked"), isPresented: $user_blocked_confirm, actions: {
|
||||
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to block a user was successful.")) {
|
||||
user_blocked_confirm = false
|
||||
.alert(NSLocalizedString("User muted", comment: "Alert message to indicate the user has been muted"), isPresented: $user_muted_confirm, actions: {
|
||||
Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to muted a user was successful.")) {
|
||||
user_muted_confirm = false
|
||||
}
|
||||
}, message: {
|
||||
if let pubkey = self.blocking {
|
||||
if let pubkey = self.muting {
|
||||
let profile = damus_state!.profiles.lookup(id: pubkey)
|
||||
let name = Profile.displayName(profile: profile, pubkey: pubkey).username
|
||||
Text("\(name) has been blocked", comment: "Alert message that informs a user was blocked.")
|
||||
Text("\(name) has been muted", comment: "Alert message that informs a user was muted.")
|
||||
} else {
|
||||
Text("User has been blocked", comment: "Alert message that informs a user was blocked.")
|
||||
Text("User has been muted", comment: "Alert message that informs a user was d.")
|
||||
}
|
||||
})
|
||||
.alert(NSLocalizedString("Create new mutelist", comment: "Title of alert prompting the user to create a new mutelist."), isPresented: $confirm_overwrite_mutelist, actions: {
|
||||
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of alert that creates a new mutelist.")) {
|
||||
confirm_overwrite_mutelist = false
|
||||
confirm_block = false
|
||||
confirm_mute = false
|
||||
}
|
||||
|
||||
Button(NSLocalizedString("Yes, Overwrite", comment: "Text of button that confirms to overwrite the existing mutelist.")) {
|
||||
@@ -507,7 +535,7 @@ struct ContentView: View {
|
||||
return
|
||||
}
|
||||
|
||||
guard let pubkey = blocking else {
|
||||
guard let pubkey = muting else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -516,20 +544,20 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
damus_state?.contacts.set_mutelist(mutelist)
|
||||
ds.pool.send(.event(mutelist))
|
||||
ds.postbox.send(mutelist)
|
||||
|
||||
confirm_overwrite_mutelist = false
|
||||
confirm_block = false
|
||||
user_blocked_confirm = true
|
||||
confirm_mute = false
|
||||
user_muted_confirm = true
|
||||
}
|
||||
}, message: {
|
||||
Text("No block list found, create a new one? This will overwrite any previous block lists.", comment: "Alert message prompt that asks if the user wants to create a new block list, overwriting previous block lists.")
|
||||
Text("No mute list found, create a new one? This will overwrite any previous mute lists.", comment: "Alert message prompt that asks if the user wants to create a new mute list, overwriting previous mute lists.")
|
||||
})
|
||||
.alert(NSLocalizedString("Block User", comment: "Title of alert for blocking a user."), isPresented: $confirm_block, actions: {
|
||||
Button(NSLocalizedString("Cancel", comment: "Alert button to cancel out of alert for blocking a user."), role: .cancel) {
|
||||
confirm_block = false
|
||||
.alert(NSLocalizedString("Mute User", comment: "Title of alert for muting a user."), isPresented: $confirm_mute, actions: {
|
||||
Button(NSLocalizedString("Cancel", comment: "Alert button to cancel out of alert for muting a user."), role: .cancel) {
|
||||
confirm_mute = false
|
||||
}
|
||||
Button(NSLocalizedString("Block", comment: "Alert button to block a user."), role: .destructive) {
|
||||
Button(NSLocalizedString("Mute", comment: "Alert button to mute a user."), role: .destructive) {
|
||||
guard let ds = damus_state else {
|
||||
return
|
||||
}
|
||||
@@ -540,7 +568,7 @@ struct ContentView: View {
|
||||
guard let keypair = ds.keypair.to_full() else {
|
||||
return
|
||||
}
|
||||
guard let pubkey = blocking else {
|
||||
guard let pubkey = muting else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -548,31 +576,53 @@ struct ContentView: View {
|
||||
return
|
||||
}
|
||||
damus_state?.contacts.set_mutelist(ev)
|
||||
ds.pool.send(.event(ev))
|
||||
ds.postbox.send(ev)
|
||||
}
|
||||
}
|
||||
}, message: {
|
||||
if let pubkey = blocking {
|
||||
if let pubkey = muting {
|
||||
let profile = damus_state?.profiles.lookup(id: 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.")
|
||||
Text("Mute \(name)?", comment: "Alert message prompt to ask if a user should be muted.")
|
||||
} else {
|
||||
Text("Could not find user to block...", comment: "Alert message to indicate that the blocked user could not be found.")
|
||||
Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.")
|
||||
}
|
||||
})
|
||||
.alert(NSLocalizedString("Repost", comment: "Title of alert for confirming to repost a post."), isPresented: $current_boost.mappedToBool()) {
|
||||
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of reposting a post.")) {
|
||||
current_boost = nil
|
||||
.confirmationDialog("Repost", isPresented: $shouldShowBoostAlert) {
|
||||
Button(NSLocalizedString("Repost", comment: "Title of alert for confirming to repost a post.")) {
|
||||
guard let current_boost else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let privkey = self.damus_state?.keypair.privkey else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let damus_state else {
|
||||
return
|
||||
}
|
||||
|
||||
let boost = make_boost_event(pubkey: damus_state.keypair.pubkey, privkey: privkey, boosted: current_boost)
|
||||
damus_state.postbox.send(boost)
|
||||
}
|
||||
Button(NSLocalizedString("Repost", comment: "Button to confirm reposting a post.")) {
|
||||
self.damus_state?.pool.send(.event(current_boost!))
|
||||
|
||||
Button(NSLocalizedString("Quote", comment: "Title of alert for confirming to make a quoted post.")) {
|
||||
guard let current_boost else {
|
||||
return
|
||||
}
|
||||
self.active_sheet = .post(.quoting(current_boost))
|
||||
}
|
||||
}
|
||||
.onChange(of: shouldShowBoostAlert) { v in
|
||||
if v == false {
|
||||
self.current_boost = nil
|
||||
}
|
||||
} message: {
|
||||
Text("Are you sure you want to repost this?", comment: "Alert message to ask if user wants to repost a post.")
|
||||
}
|
||||
}
|
||||
|
||||
func switch_timeline(_ timeline: Timeline) {
|
||||
self.isSideBarOpened = false
|
||||
|
||||
self.popToRoot()
|
||||
NotificationCenter.default.post(name: .switched_timeline, object: timeline)
|
||||
|
||||
@@ -601,9 +651,10 @@ struct ContentView: View {
|
||||
let pool = RelayPool()
|
||||
let metadatas = RelayMetadatas()
|
||||
let relay_filters = RelayFilters(our_pubkey: pubkey)
|
||||
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
|
||||
|
||||
let new_relay_filters = load_relay_filters(pubkey) == nil
|
||||
for relay in BOOTSTRAP_RELAYS {
|
||||
for relay in bootstrap_relays {
|
||||
if let url = URL(string: relay) {
|
||||
add_new_relay(relay_filters: relay_filters, metadatas: metadatas, pool: pool, url: url, info: .rw, new_relay_filters: new_relay_filters)
|
||||
}
|
||||
@@ -627,7 +678,11 @@ struct ContentView: View {
|
||||
relay_metadata: metadatas,
|
||||
drafts: Drafts(),
|
||||
events: EventCache(),
|
||||
bookmarks: BookmarksManager(pubkey: pubkey)
|
||||
bookmarks: BookmarksManager(pubkey: pubkey),
|
||||
postbox: PostBox(pool: pool),
|
||||
bootstrap_relays: bootstrap_relays,
|
||||
replies: ReplyCounter(our_pubkey: pubkey),
|
||||
muted_threads: MutedThreadsManager(keypair: keypair)
|
||||
)
|
||||
home.damus_state = self.damus_state!
|
||||
|
||||
@@ -789,7 +844,7 @@ func find_event(state: DamusState, evid: String, search_type: SearchType, find_f
|
||||
var filter = search_type == .event ? NostrFilter.filter_ids([ evid ]) : NostrFilter.filter_authors([ evid ])
|
||||
|
||||
if search_type == .profile {
|
||||
filter.kinds = [0]
|
||||
filter.kinds = [NostrKind.metadata.rawValue]
|
||||
}
|
||||
|
||||
filter.limit = 1
|
||||
@@ -805,6 +860,8 @@ func find_event(state: DamusState, evid: String, search_type: SearchType, find_f
|
||||
}
|
||||
|
||||
switch ev {
|
||||
case .ok:
|
||||
break
|
||||
case .event(_, let ev):
|
||||
has_event = true
|
||||
callback(ev)
|
||||
@@ -831,12 +888,12 @@ func timeline_name(_ timeline: Timeline?) -> String {
|
||||
}
|
||||
switch timeline {
|
||||
case .home:
|
||||
return "Home"
|
||||
return NSLocalizedString("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.")
|
||||
case .notifications:
|
||||
return "Notifications"
|
||||
return NSLocalizedString("Notifications", comment: "Toolbar label for Notifications view.")
|
||||
case .search:
|
||||
return "Universe 🛸"
|
||||
return NSLocalizedString("Universe 🛸", comment: "Toolbar label for the universal view where posts from all connected relay servers appear.")
|
||||
case .dms:
|
||||
return "DMs"
|
||||
return NSLocalizedString("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,34 +11,40 @@ import Foundation
|
||||
class ActionBarModel: ObservableObject {
|
||||
@Published var our_like: NostrEvent?
|
||||
@Published var our_boost: NostrEvent?
|
||||
@Published var our_reply: NostrEvent?
|
||||
@Published var our_zap: Zap?
|
||||
@Published var likes: Int
|
||||
@Published var boosts: Int
|
||||
@Published var zaps: Int
|
||||
@Published var zap_total: Int64
|
||||
@Published var replies: Int
|
||||
|
||||
static func empty() -> ActionBarModel {
|
||||
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, our_like: nil, our_boost: nil, our_zap: nil)
|
||||
return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
|
||||
}
|
||||
|
||||
init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zap?) {
|
||||
init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, replies: Int, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zap?, our_reply: NostrEvent?) {
|
||||
self.likes = likes
|
||||
self.boosts = boosts
|
||||
self.zaps = zaps
|
||||
self.replies = replies
|
||||
self.zap_total = zap_total
|
||||
self.our_like = our_like
|
||||
self.our_boost = our_boost
|
||||
self.our_zap = our_zap
|
||||
self.our_reply = our_reply
|
||||
}
|
||||
|
||||
func update(damus: DamusState, evid: String) {
|
||||
self.likes = damus.likes.counts[evid] ?? 0
|
||||
self.boosts = damus.boosts.counts[evid] ?? 0
|
||||
self.zaps = damus.zaps.event_counts[evid] ?? 0
|
||||
self.replies = damus.replies.get_replies(evid)
|
||||
self.zap_total = damus.zaps.event_totals[evid] ?? 0
|
||||
self.our_like = damus.likes.our_events[evid]
|
||||
self.our_boost = damus.boosts.our_events[evid]
|
||||
self.our_zap = damus.zaps.our_zaps[evid]?.first
|
||||
self.our_reply = damus.replies.our_reply(evid)
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
|
||||
@@ -54,6 +60,10 @@ class ActionBarModel: ObservableObject {
|
||||
return our_like != nil
|
||||
}
|
||||
|
||||
var replied: Bool {
|
||||
return our_reply != nil
|
||||
}
|
||||
|
||||
var boosted: Bool {
|
||||
return our_boost != nil
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ class BookmarksManager: ObservableObject {
|
||||
if isBookmarked(ev) {
|
||||
bookmarks = bookmarks.filter { $0 != ev }
|
||||
} else {
|
||||
bookmarks.append(ev)
|
||||
bookmarks.insert(ev, at: 0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -140,7 +140,7 @@ func follow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, pri
|
||||
return ev
|
||||
}
|
||||
|
||||
func unfollow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, privkey: String, unfollow: String) -> NostrEvent? {
|
||||
func unfollow_user(postbox: PostBox, our_contacts: NostrEvent?, pubkey: String, privkey: String, unfollow: String) -> NostrEvent? {
|
||||
guard let cs = our_contacts else {
|
||||
return nil
|
||||
}
|
||||
@@ -149,7 +149,7 @@ func unfollow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, p
|
||||
ev.calculate_id()
|
||||
ev.sign(privkey: privkey)
|
||||
|
||||
pool.send(.event(ev))
|
||||
postbox.send(ev)
|
||||
|
||||
return ev
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ class CreateAccountModel: ObservableObject {
|
||||
@Published var about: String = ""
|
||||
@Published var pubkey: String = ""
|
||||
@Published var privkey: String = ""
|
||||
@Published var profile_image: String? = nil
|
||||
|
||||
var pubkey_bech32: String {
|
||||
return bech32_pubkey(self.pubkey) ?? ""
|
||||
|
||||
@@ -26,6 +26,10 @@ struct DamusState {
|
||||
let drafts: Drafts
|
||||
let events: EventCache
|
||||
let bookmarks: BookmarksManager
|
||||
let postbox: PostBox
|
||||
let bootstrap_relays: [String]
|
||||
let replies: ReplyCounter
|
||||
let muted_threads: MutedThreadsManager
|
||||
|
||||
var pubkey: String {
|
||||
return keypair.pubkey
|
||||
@@ -36,6 +40,5 @@ struct DamusState {
|
||||
}
|
||||
|
||||
static var empty: DamusState {
|
||||
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""))
|
||||
}
|
||||
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil))) }
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ class DirectMessageModel: ObservableObject {
|
||||
|
||||
@Published var draft: String
|
||||
|
||||
let pubkey: String
|
||||
|
||||
var is_request: Bool
|
||||
var our_pubkey: String
|
||||
|
||||
@@ -29,17 +31,19 @@ class DirectMessageModel: ObservableObject {
|
||||
return true
|
||||
}
|
||||
|
||||
init(events: [NostrEvent], our_pubkey: String) {
|
||||
init(events: [NostrEvent], our_pubkey: String, pubkey: String) {
|
||||
self.events = events
|
||||
self.is_request = false
|
||||
self.our_pubkey = our_pubkey
|
||||
self.draft = ""
|
||||
self.pubkey = pubkey
|
||||
}
|
||||
|
||||
init(our_pubkey: String) {
|
||||
init(our_pubkey: String, pubkey: String) {
|
||||
self.events = []
|
||||
self.is_request = false
|
||||
self.our_pubkey = our_pubkey
|
||||
self.draft = ""
|
||||
self.pubkey = pubkey
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,20 +8,43 @@
|
||||
import Foundation
|
||||
|
||||
class DirectMessagesModel: ObservableObject {
|
||||
@Published var dms: [(String, DirectMessageModel)] = []
|
||||
@Published var dms: [DirectMessageModel] = []
|
||||
@Published var loading: Bool = false
|
||||
@Published var open_dm: Bool = false
|
||||
@Published private(set) var active_model: DirectMessageModel = DirectMessageModel(our_pubkey: "", pubkey: "")
|
||||
let our_pubkey: String
|
||||
|
||||
init(our_pubkey: String) {
|
||||
self.our_pubkey = our_pubkey
|
||||
}
|
||||
|
||||
var message_requests: [(String, DirectMessageModel)] {
|
||||
return dms.filter { dm in dm.1.is_request }
|
||||
var message_requests: [DirectMessageModel] {
|
||||
return dms.filter { dm in dm.is_request }
|
||||
}
|
||||
|
||||
var friend_dms: [(String, DirectMessageModel)] {
|
||||
return dms.filter { dm in !dm.1.is_request }
|
||||
var friend_dms: [DirectMessageModel] {
|
||||
return dms.filter { dm in !dm.is_request }
|
||||
}
|
||||
|
||||
func set_active_dm_model(_ model: DirectMessageModel) {
|
||||
self.active_model = model
|
||||
}
|
||||
|
||||
func open_dm_by_pk(_ pubkey: String) {
|
||||
self.set_active_dm(pubkey)
|
||||
self.open_dm = true
|
||||
}
|
||||
|
||||
func open_dm_by_model(_ model: DirectMessageModel) {
|
||||
self.set_active_dm_model(model)
|
||||
self.open_dm = true
|
||||
}
|
||||
|
||||
func set_active_dm(_ pubkey: String) {
|
||||
for model in self.dms where model.pubkey == pubkey {
|
||||
self.set_active_dm_model(model)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func lookup_or_create(_ pubkey: String) -> DirectMessageModel {
|
||||
@@ -29,15 +52,15 @@ class DirectMessagesModel: ObservableObject {
|
||||
return dm
|
||||
}
|
||||
|
||||
let new = DirectMessageModel(our_pubkey: our_pubkey)
|
||||
dms.append((pubkey, new))
|
||||
let new = DirectMessageModel(our_pubkey: our_pubkey, pubkey: pubkey)
|
||||
dms.append(new)
|
||||
return new
|
||||
}
|
||||
|
||||
func lookup(_ pubkey: String) -> DirectMessageModel? {
|
||||
for dm in dms {
|
||||
if pubkey == dm.0 {
|
||||
return dm.1
|
||||
if pubkey == dm.pubkey {
|
||||
return dm
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,23 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
class Drafts: ObservableObject {
|
||||
@Published var post: NSMutableAttributedString = NSMutableAttributedString(string: "")
|
||||
@Published var replies: [NostrEvent: NSMutableAttributedString] = [:]
|
||||
class DraftArtifacts {
|
||||
var content: NSMutableAttributedString
|
||||
var media: [UploadedMedia]
|
||||
|
||||
init() {
|
||||
self.content = NSMutableAttributedString(string: "")
|
||||
self.media = []
|
||||
}
|
||||
|
||||
init(content: NSMutableAttributedString, media: [UploadedMedia]) {
|
||||
self.content = content
|
||||
self.media = media
|
||||
}
|
||||
}
|
||||
|
||||
class Drafts: ObservableObject {
|
||||
@Published var post: DraftArtifacts? = nil
|
||||
@Published var replies: [NostrEvent: DraftArtifacts] = [:]
|
||||
@Published var quotes: [NostrEvent: DraftArtifacts] = [:]
|
||||
}
|
||||
|
||||
@@ -74,8 +74,12 @@ func build_mention_indices(_ blocks: [Block], type: MentionType) -> Set<Int> {
|
||||
switch block {
|
||||
case .mention(let m):
|
||||
if m.type == type {
|
||||
acc.insert(m.index)
|
||||
if let idx = m.index {
|
||||
acc.insert(idx)
|
||||
}
|
||||
}
|
||||
case .relay:
|
||||
return
|
||||
case .text:
|
||||
return
|
||||
case .hashtag:
|
||||
|
||||
@@ -64,6 +64,8 @@ class EventsModel: ObservableObject {
|
||||
handle_event(relay_id: relay_id, ev: ev)
|
||||
case .notice(_):
|
||||
break
|
||||
case .ok:
|
||||
break
|
||||
case .eose(_):
|
||||
load_profiles(profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state)
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ class FollowersModel: ObservableObject {
|
||||
if ev.known_kind == .contacts {
|
||||
handle_contact_event(ev)
|
||||
} else if ev.known_kind == .metadata {
|
||||
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
}
|
||||
|
||||
case .notice(let msg):
|
||||
@@ -94,6 +94,9 @@ class FollowersModel: ObservableObject {
|
||||
} else if sub_id == self.profiles_id {
|
||||
damus_state.pool.unsubscribe(sub_id: profiles_id, to: [relay_id])
|
||||
}
|
||||
|
||||
case .ok:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class FollowingModel {
|
||||
}
|
||||
|
||||
func get_filter() -> NostrFilter {
|
||||
var f = NostrFilter.filter_kinds([0])
|
||||
var f = NostrFilter.filter_kinds([NostrKind.metadata.rawValue])
|
||||
f.authors = self.contacts.reduce(into: Array<String>()) { acc, pk in
|
||||
// don't fetch profiles we already have
|
||||
if damus_state.profiles.lookup(id: pk) != nil {
|
||||
@@ -58,9 +58,11 @@ class FollowingModel {
|
||||
break
|
||||
case .nostr_event(let nev):
|
||||
switch nev {
|
||||
case .ok:
|
||||
break
|
||||
case .event(_, let ev):
|
||||
if ev.kind == 0 {
|
||||
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
}
|
||||
case .notice(let msg):
|
||||
print("followingmodel notice: \(msg)")
|
||||
|
||||
@@ -8,26 +8,19 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
struct NewEventsBits {
|
||||
let bits: Int
|
||||
|
||||
init() {
|
||||
bits = 0
|
||||
}
|
||||
|
||||
init (prev: NewEventsBits, setting: Timeline) {
|
||||
self.bits = prev.bits | timeline_bit(setting)
|
||||
}
|
||||
|
||||
init (prev: NewEventsBits, unsetting: Timeline) {
|
||||
self.bits = prev.bits & ~timeline_bit(unsetting)
|
||||
}
|
||||
|
||||
func is_set(_ timeline: Timeline) -> Bool {
|
||||
let notification_bit = timeline_bit(timeline)
|
||||
return (bits & notification_bit) == notification_bit
|
||||
}
|
||||
|
||||
struct NewEventsBits: OptionSet {
|
||||
let rawValue: Int
|
||||
|
||||
static let home = NewEventsBits(rawValue: 1 << 0)
|
||||
static let zaps = NewEventsBits(rawValue: 1 << 1)
|
||||
static let mentions = NewEventsBits(rawValue: 1 << 2)
|
||||
static let reposts = NewEventsBits(rawValue: 1 << 3)
|
||||
static let likes = NewEventsBits(rawValue: 1 << 4)
|
||||
static let search = NewEventsBits(rawValue: 1 << 5)
|
||||
static let dms = NewEventsBits(rawValue: 1 << 6)
|
||||
|
||||
static let all = NewEventsBits(rawValue: 0xFFFFFFFF)
|
||||
static let notifications: NewEventsBits = [.zaps, .likes, .reposts, .mentions]
|
||||
}
|
||||
|
||||
class HomeModel: ObservableObject {
|
||||
@@ -48,34 +41,27 @@ class HomeModel: ObservableObject {
|
||||
let dms_subid = UUID().description
|
||||
let init_subid = UUID().description
|
||||
let profiles_subid = UUID().description
|
||||
|
||||
var loading: Bool = false
|
||||
|
||||
var signal = SignalModel()
|
||||
|
||||
@Published var new_events: NewEventsBits = NewEventsBits()
|
||||
@Published var notifications = NotificationsModel()
|
||||
@Published var dms: DirectMessagesModel
|
||||
@Published var events = EventHolder()
|
||||
@Published var loading: Bool = false
|
||||
@Published var signal: SignalModel = SignalModel()
|
||||
|
||||
init() {
|
||||
self.damus_state = DamusState.empty
|
||||
self.dms = DirectMessagesModel(our_pubkey: "")
|
||||
}
|
||||
|
||||
init(damus_state: DamusState) {
|
||||
self.damus_state = damus_state
|
||||
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
|
||||
filter_events()
|
||||
self.setup_debouncer()
|
||||
}
|
||||
|
||||
|
||||
var pool: RelayPool {
|
||||
return damus_state.pool
|
||||
}
|
||||
|
||||
func setup_debouncer() {
|
||||
// turn off debouncer after initial load
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
|
||||
self.should_debounce_dms = false
|
||||
}
|
||||
var dms: DirectMessagesModel {
|
||||
return damus_state.dms
|
||||
}
|
||||
|
||||
func has_sub_id_event(sub_id: String, ev_id: String) -> Bool {
|
||||
@@ -86,6 +72,13 @@ class HomeModel: ObservableObject {
|
||||
|
||||
return has_event[sub_id]!.contains(ev_id)
|
||||
}
|
||||
|
||||
func setup_debouncer() {
|
||||
// turn off debouncer after initial load
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
|
||||
self.should_debounce_dms = false
|
||||
}
|
||||
}
|
||||
|
||||
func process_event(sub_id: String, relay_id: String, ev: NostrEvent) {
|
||||
if has_sub_id_event(sub_id: sub_id, ev_id: ev.id) {
|
||||
@@ -141,7 +134,7 @@ class HomeModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
if !notifications.insert_zap(zap) {
|
||||
if !notifications.insert_zap(zap, damus_state: damus_state) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -150,8 +143,10 @@ class HomeModel: ObservableObject {
|
||||
// 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)
|
||||
if damus_state.settings.zap_notification {
|
||||
// Create in-app local notification for zap received.
|
||||
create_in_app_zap_notification(profiles: profiles, zap: zap, evId: ev.referenced_ids.first?.id ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
@@ -191,27 +186,29 @@ class HomeModel: ObservableObject {
|
||||
}
|
||||
|
||||
func handle_channel_create(_ ev: NostrEvent) {
|
||||
guard ev.is_valid else {
|
||||
return
|
||||
}
|
||||
|
||||
self.channels[ev.id] = ev
|
||||
}
|
||||
|
||||
func handle_channel_meta(_ ev: NostrEvent) {
|
||||
}
|
||||
|
||||
func filter_muted() {
|
||||
events.filter { !damus_state.contacts.is_muted($0.pubkey) }
|
||||
self.dms.dms = dms.dms.filter { !damus_state.contacts.is_muted($0.0) }
|
||||
notifications.filter { !damus_state.contacts.is_muted($0.pubkey) }
|
||||
func filter_events() {
|
||||
events.filter { ev in
|
||||
!damus_state.contacts.is_muted(ev.pubkey)
|
||||
}
|
||||
|
||||
self.dms.dms = dms.dms.filter { ev in
|
||||
!damus_state.contacts.is_muted(ev.pubkey)
|
||||
}
|
||||
|
||||
notifications.filter { ev in
|
||||
!damus_state.contacts.is_muted(ev.pubkey) &&
|
||||
!damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey) &&
|
||||
(ev.kind != NostrKind.like.rawValue || !damus_state.settings.hide_reactions)
|
||||
}
|
||||
}
|
||||
|
||||
func handle_delete_event(_ ev: NostrEvent) {
|
||||
guard ev.is_valid else {
|
||||
return
|
||||
}
|
||||
|
||||
self.deleted_events.insert(ev.id)
|
||||
}
|
||||
|
||||
@@ -233,7 +230,7 @@ class HomeModel: ObservableObject {
|
||||
if let inner_ev = ev.inner_event {
|
||||
boost_ev_id = inner_ev.id
|
||||
|
||||
guard inner_ev.is_valid else {
|
||||
guard validate_event(ev: inner_ev) == .ok else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -262,6 +259,10 @@ class HomeModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
if damus_state.settings.hide_reactions {
|
||||
return
|
||||
}
|
||||
|
||||
switch damus_state.likes.add_event(ev, target: e.ref_id) {
|
||||
case .already_counted:
|
||||
break
|
||||
@@ -302,10 +303,7 @@ class HomeModel: ObservableObject {
|
||||
break
|
||||
}
|
||||
|
||||
update_signal_from_pool(signal: signal, pool: damus_state.pool)
|
||||
|
||||
print("ws_event \(ev)")
|
||||
|
||||
update_signal_from_pool(signal: self.signal, pool: damus_state.pool)
|
||||
case .nostr_event(let ev):
|
||||
switch ev {
|
||||
case .event(let sub_id, let ev):
|
||||
@@ -324,16 +322,22 @@ class HomeModel: ObservableObject {
|
||||
case .eose(let sub_id):
|
||||
|
||||
if sub_id == dms_subid {
|
||||
var dms = dms.dms.flatMap { $0.1.events }
|
||||
var dms = dms.dms.flatMap { $0.events }
|
||||
dms.append(contentsOf: incoming_dms)
|
||||
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, load: .from_keys(notifications.uniq_pubkeys()), damus_state: damus_state)
|
||||
} else if sub_id == home_subid {
|
||||
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus_state)
|
||||
}
|
||||
|
||||
self.loading = false
|
||||
break
|
||||
|
||||
case .ok:
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,13 +358,13 @@ class HomeModel: ObservableObject {
|
||||
var friends = damus_state.contacts.get_friend_list()
|
||||
friends.append(damus_state.pubkey)
|
||||
|
||||
var contacts_filter = NostrFilter.filter_kinds([0])
|
||||
var contacts_filter = NostrFilter.filter_kinds([NostrKind.metadata.rawValue])
|
||||
contacts_filter.authors = friends
|
||||
|
||||
var our_contacts_filter = NostrFilter.filter_kinds([3, 0])
|
||||
var our_contacts_filter = NostrFilter.filter_kinds([NostrKind.contacts.rawValue, NostrKind.metadata.rawValue])
|
||||
our_contacts_filter.authors = [damus_state.pubkey]
|
||||
|
||||
var our_blocklist_filter = NostrFilter.filter_kinds([30000])
|
||||
var our_blocklist_filter = NostrFilter.filter_kinds([NostrKind.list.rawValue])
|
||||
our_blocklist_filter.parameter = ["mute"]
|
||||
our_blocklist_filter.authors = [damus_state.pubkey]
|
||||
|
||||
@@ -379,21 +383,27 @@ class HomeModel: ObservableObject {
|
||||
our_dms_filter.authors = [ damus_state.pubkey ]
|
||||
|
||||
// TODO: separate likes?
|
||||
var home_filter = NostrFilter.filter_kinds([
|
||||
var home_filter_kinds = [
|
||||
NostrKind.text.rawValue,
|
||||
NostrKind.like.rawValue,
|
||||
NostrKind.boost.rawValue,
|
||||
])
|
||||
NostrKind.boost.rawValue
|
||||
]
|
||||
if !damus_state.settings.hide_reactions {
|
||||
home_filter_kinds.append(NostrKind.like.rawValue)
|
||||
}
|
||||
var home_filter = NostrFilter.filter_kinds(home_filter_kinds)
|
||||
// include our pubkey as well even if we're not technically a friend
|
||||
home_filter.authors = friends
|
||||
home_filter.limit = 500
|
||||
|
||||
var notifications_filter = NostrFilter.filter_kinds([
|
||||
var notifications_filter_kinds = [
|
||||
NostrKind.text.rawValue,
|
||||
NostrKind.like.rawValue,
|
||||
NostrKind.boost.rawValue,
|
||||
NostrKind.zap.rawValue,
|
||||
])
|
||||
]
|
||||
if !damus_state.settings.hide_reactions {
|
||||
notifications_filter_kinds.append(NostrKind.like.rawValue)
|
||||
}
|
||||
var notifications_filter = NostrFilter.filter_kinds(notifications_filter_kinds)
|
||||
notifications_filter.pubkeys = [damus_state.pubkey]
|
||||
notifications_filter.limit = 500
|
||||
|
||||
@@ -448,7 +458,7 @@ class HomeModel: ObservableObject {
|
||||
}
|
||||
|
||||
func handle_metadata_event(_ ev: NostrEvent) {
|
||||
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
}
|
||||
|
||||
func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? {
|
||||
@@ -459,7 +469,7 @@ class HomeModel: ObservableObject {
|
||||
|
||||
return m[kind]
|
||||
}
|
||||
|
||||
|
||||
func handle_notification(ev: NostrEvent) {
|
||||
// don't show notifications from ourselves
|
||||
guard ev.pubkey != damus_state.pubkey else {
|
||||
@@ -470,20 +480,24 @@ class HomeModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
|
||||
guard should_show_event(contacts: damus_state.contacts, ev: ev) && !damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.events.insert(ev)
|
||||
|
||||
if let inner_ev = ev.inner_event {
|
||||
damus_state.events.insert(inner_ev)
|
||||
}
|
||||
|
||||
if !notifications.insert_event(ev) {
|
||||
if !notifications.insert_event(ev, damus_state: damus_state) {
|
||||
return
|
||||
}
|
||||
|
||||
handle_last_event(ev: ev, timeline: .notifications)
|
||||
if handle_last_event(ev: ev, timeline: .notifications) {
|
||||
process_local_notification(damus_state: damus_state, event: ev)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
@@ -502,11 +516,13 @@ class HomeModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
|
||||
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.replies.count_replies(ev)
|
||||
damus_state.events.insert(ev)
|
||||
|
||||
if sub_id == home_subid {
|
||||
@@ -516,15 +532,26 @@ class HomeModel: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func got_new_dm(notifs: NewEventsBits, ev: NostrEvent) {
|
||||
self.new_events = notifs
|
||||
if damus_state.settings.dm_notification {
|
||||
let convo = ev.decrypted(privkey: self.damus_state.keypair.privkey) ?? NSLocalizedString("New encrypted direct message", comment: "Notification that the user has received a new direct message")
|
||||
let notify = LocalNotification(type: .dm, event: ev, target: ev, content: convo)
|
||||
create_local_notification(profiles: damus_state.profiles, notify: notify)
|
||||
}
|
||||
}
|
||||
|
||||
func handle_dm(_ ev: NostrEvent) {
|
||||
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.events.insert(ev)
|
||||
|
||||
if !should_debounce_dms {
|
||||
self.incoming_dms.append(ev)
|
||||
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
|
||||
self.new_events = notifs
|
||||
got_new_dm(notifs: notifs, ev: ev)
|
||||
}
|
||||
self.incoming_dms = []
|
||||
return
|
||||
@@ -532,9 +559,9 @@ class HomeModel: ObservableObject {
|
||||
|
||||
incoming_dms.append(ev)
|
||||
|
||||
dm_debouncer.debounce {
|
||||
dm_debouncer.debounce { [self] in
|
||||
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
|
||||
self.new_events = notifs
|
||||
got_new_dm(notifs: notifs, ev: ev)
|
||||
}
|
||||
self.incoming_dms = []
|
||||
}
|
||||
@@ -547,8 +574,8 @@ func update_signal_from_pool(signal: SignalModel, pool: RelayPool) {
|
||||
signal.max_signal = pool.relays.count
|
||||
}
|
||||
|
||||
if signal.signal != pool.num_connecting {
|
||||
signal.signal = signal.max_signal - pool.num_connecting
|
||||
if signal.signal != pool.num_connected {
|
||||
signal.signal = pool.num_connected
|
||||
}
|
||||
}
|
||||
|
||||
@@ -642,11 +669,7 @@ func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) {
|
||||
print("-----")
|
||||
}
|
||||
|
||||
func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEvent) {
|
||||
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
|
||||
return
|
||||
}
|
||||
|
||||
func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: Profile, ev: NostrEvent) {
|
||||
if our_pubkey == ev.pubkey && (profile.deleted ?? false) {
|
||||
DispatchQueue.main.async {
|
||||
notify(.deleted_account, ())
|
||||
@@ -663,7 +686,7 @@ func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEve
|
||||
}
|
||||
}
|
||||
|
||||
let tprof = TimestampedProfile(profile: profile, timestamp: ev.created_at)
|
||||
let tprof = TimestampedProfile(profile: profile, timestamp: ev.created_at, event: ev)
|
||||
profiles.add(id: ev.pubkey, profile: tprof)
|
||||
|
||||
if let nip05 = profile.nip05, old_nip05 != profile.nip05 {
|
||||
@@ -697,6 +720,47 @@ func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEve
|
||||
}
|
||||
|
||||
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
||||
|
||||
}
|
||||
|
||||
func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping () -> Void) {
|
||||
let validated = events.is_event_valid(ev.id)
|
||||
|
||||
switch validated {
|
||||
case .unknown:
|
||||
Task {
|
||||
let result = validate_event(ev: ev)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
events.validation[ev.id] = result
|
||||
guard result == .ok else {
|
||||
return
|
||||
}
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
case .ok:
|
||||
callback()
|
||||
|
||||
case .bad_id: fallthrough
|
||||
case .bad_sig:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Profiles, ev: NostrEvent) {
|
||||
guard_valid_event(events: events, ev: ev) {
|
||||
DispatchQueue.global(qos: .background).async {
|
||||
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
process_metadata_profile(our_pubkey: our_pubkey, profiles: profiles, profile: profile, ev: ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func robohash(_ pk: String) -> String {
|
||||
@@ -729,7 +793,7 @@ func process_contact_event(state: DamusState, ev: NostrEvent) {
|
||||
|
||||
func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
||||
let bootstrap_dict: [String: RelayInfo] = [:]
|
||||
let old_decoded = m_old_ev.flatMap { decode_json_relays($0.content) } ?? BOOTSTRAP_RELAYS.reduce(into: bootstrap_dict) { (d, r) in
|
||||
let old_decoded = m_old_ev.flatMap { decode_json_relays($0.content) } ?? state.bootstrap_relays.reduce(into: bootstrap_dict) { (d, r) in
|
||||
d[r] = .rw
|
||||
}
|
||||
|
||||
@@ -764,6 +828,7 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
||||
}
|
||||
|
||||
if changed {
|
||||
save_bootstrap_relays(pubkey: state.pubkey, relays: Array(new))
|
||||
notify(.relays_changed, ())
|
||||
}
|
||||
}
|
||||
@@ -836,10 +901,10 @@ func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesM
|
||||
}
|
||||
}
|
||||
|
||||
for (pk, _) in dms.dms {
|
||||
if pk == the_pk {
|
||||
for model in dms.dms {
|
||||
if model.pubkey == the_pk {
|
||||
found = true
|
||||
inserted = insert_uniq_sorted_event(events: &(dms.dms[i].1.events), new_ev: ev) {
|
||||
inserted = insert_uniq_sorted_event(events: &(dms.dms[i].events), new_ev: ev) {
|
||||
$0.created_at < $1.created_at
|
||||
}
|
||||
|
||||
@@ -849,8 +914,8 @@ func handle_incoming_dm(ev: NostrEvent, our_pubkey: String, dms: DirectMessagesM
|
||||
}
|
||||
|
||||
if !found {
|
||||
let model = DirectMessageModel(events: [ev], our_pubkey: our_pubkey)
|
||||
dms.dms.append((the_pk, model))
|
||||
let model = DirectMessageModel(events: [ev], our_pubkey: our_pubkey, pubkey: the_pk)
|
||||
dms.dms.append(model)
|
||||
inserted = true
|
||||
}
|
||||
|
||||
@@ -877,14 +942,53 @@ func handle_incoming_dms(prev_events: NewEventsBits, dms: DirectMessagesModel, o
|
||||
}
|
||||
|
||||
if inserted {
|
||||
dms.dms = dms.dms.filter({ $0.1.events.count > 0 }).sorted { a, b in
|
||||
return a.1.events.last!.created_at > b.1.events.last!.created_at
|
||||
dms.dms = dms.dms.filter({ $0.events.count > 0 }).sorted { a, b in
|
||||
return a.events.last!.created_at > b.events.last!.created_at
|
||||
}
|
||||
}
|
||||
|
||||
return new_events
|
||||
}
|
||||
|
||||
func determine_event_notifications(_ ev: NostrEvent) -> NewEventsBits {
|
||||
guard let kind = ev.known_kind else {
|
||||
return []
|
||||
}
|
||||
|
||||
if kind == .zap {
|
||||
return [.zaps]
|
||||
}
|
||||
|
||||
if kind == .boost {
|
||||
return [.reposts]
|
||||
}
|
||||
|
||||
if kind == .text {
|
||||
return [.mentions]
|
||||
}
|
||||
|
||||
if kind == .like {
|
||||
return [.likes]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
func timeline_to_notification_bits(_ timeline: Timeline, ev: NostrEvent?) -> NewEventsBits {
|
||||
switch timeline {
|
||||
case .home:
|
||||
return [.home]
|
||||
case .notifications:
|
||||
if let ev {
|
||||
return determine_event_notifications(ev)
|
||||
}
|
||||
return [.notifications]
|
||||
case .search:
|
||||
return [.search]
|
||||
case .dms:
|
||||
return [.dms]
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper to determine if we need to notify the user of new events
|
||||
func handle_last_events(new_events: NewEventsBits, ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) -> NewEventsBits? {
|
||||
@@ -893,7 +997,7 @@ func handle_last_events(new_events: NewEventsBits, ev: NostrEvent, timeline: Tim
|
||||
if last_ev == nil || last_ev!.created_at < ev.created_at {
|
||||
save_last_event(ev, timeline: timeline)
|
||||
if shouldNotify {
|
||||
return NewEventsBits(prev: new_events, setting: timeline)
|
||||
return new_events.union(timeline_to_notification_bits(timeline, ev: ev))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -933,31 +1037,39 @@ func zap_vibrate(zap_amount: Int64) {
|
||||
vibration_generator.impactOccurred()
|
||||
}
|
||||
|
||||
func describe_zap_type(_ zap: Zap) -> String? {
|
||||
func zap_notification_title(_ zap: Zap) -> String {
|
||||
if zap.private_request != nil {
|
||||
return "Private"
|
||||
return NSLocalizedString("Private Zap", comment: "Title of notification when a private zap is received.")
|
||||
} else {
|
||||
return NSLocalizedString("Zap", comment: "Title of notification when a non-private zap is received.")
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) -> String {
|
||||
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 sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0))
|
||||
let formattedSats = 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)"
|
||||
|
||||
if src.content.isEmpty {
|
||||
let format = localizedStringFormat(key: "zap_notification_no_message", locale: locale)
|
||||
return String(format: format, locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats, name)
|
||||
} else {
|
||||
let format = localizedStringFormat(key: "zap_notification_with_message", locale: locale)
|
||||
return String(format: format, locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats, name, src.content)
|
||||
}
|
||||
}
|
||||
|
||||
func create_in_app_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: String) {
|
||||
let content = UNMutableNotificationContent()
|
||||
|
||||
content.title = zap_notification_title(zap)
|
||||
content.body = zap_notification_body(profiles: profiles, zap: zap, locale: locale)
|
||||
content.sound = UNNotificationSound.default
|
||||
content.userInfo = LossyLocalNotification(type: .zap, event_id: evId).to_user_info()
|
||||
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
|
||||
|
||||
@@ -972,3 +1084,83 @@ func create_in_app_zap_notification(profiles: Profiles, zap: Zap) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
|
||||
guard let type = ev.known_kind else {
|
||||
return
|
||||
}
|
||||
|
||||
if damus_state.settings.notification_only_from_following,
|
||||
damus_state.contacts.follow_state(ev.pubkey) != .follows
|
||||
{
|
||||
return
|
||||
}
|
||||
|
||||
// Don't show notifications from muted threads.
|
||||
if damus_state.muted_threads.isMutedThread(ev, privkey: damus_state.keypair.privkey) {
|
||||
return
|
||||
}
|
||||
|
||||
if type == .text && damus_state.settings.mention_notification {
|
||||
let blocks = ev.blocks(damus_state.keypair.privkey)
|
||||
for case .mention(let mention) in blocks where mention.ref.ref_id == damus_state.keypair.pubkey {
|
||||
let content = NSAttributedString(render_note_content(ev: ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content.attributed).string
|
||||
|
||||
let notify = LocalNotification(type: .mention, event: ev, target: ev, content: content)
|
||||
create_local_notification(profiles: damus_state.profiles, notify: notify )
|
||||
}
|
||||
} else if type == .boost && damus_state.settings.repost_notification, let inner_ev = ev.inner_event {
|
||||
let notify = LocalNotification(type: .repost, event: ev, target: inner_ev, content: inner_ev.content)
|
||||
create_local_notification(profiles: damus_state.profiles, notify: notify)
|
||||
} else if type == .like && damus_state.settings.like_notification,
|
||||
let evid = ev.referenced_ids.first?.ref_id,
|
||||
let liked_event = damus_state.events.lookup(evid)
|
||||
{
|
||||
let notify = LocalNotification(type: .like, event: ev, target: liked_event, content: liked_event.content)
|
||||
create_local_notification(profiles: damus_state.profiles, notify: notify)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func create_local_notification(profiles: Profiles, notify: LocalNotification) {
|
||||
let content = UNMutableNotificationContent()
|
||||
var title = ""
|
||||
var identifier = ""
|
||||
|
||||
let displayName = event_author_name(profiles: profiles, pubkey: notify.event.pubkey)
|
||||
|
||||
switch notify.type {
|
||||
case .mention:
|
||||
title = String(format: NSLocalizedString("Mentioned by %@", comment: "Mentioned by heading in local notification"), displayName)
|
||||
identifier = "myMentionNotification"
|
||||
case .repost:
|
||||
title = String(format: NSLocalizedString("Reposted by %@", comment: "Reposted by heading in local notification"), displayName)
|
||||
identifier = "myBoostNotification"
|
||||
case .like:
|
||||
title = String(format: NSLocalizedString("Liked by %@", comment: "Liked by heading in local notification"), displayName)
|
||||
identifier = "myLikeNotification"
|
||||
case .dm:
|
||||
title = String(format: NSLocalizedString("%@", comment: "DM by heading in local notification"), displayName)
|
||||
identifier = "myDMNotification"
|
||||
case .zap:
|
||||
// not handled here
|
||||
break
|
||||
}
|
||||
content.title = title
|
||||
content.body = notify.content
|
||||
content.sound = UNNotificationSound.default
|
||||
content.userInfo = notify.to_lossy().to_user_info()
|
||||
|
||||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
|
||||
|
||||
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
|
||||
|
||||
UNUserNotificationCenter.current().add(request) { error in
|
||||
if let error = error {
|
||||
print("Error: \(error)")
|
||||
} else {
|
||||
print("Local notification scheduled")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,15 @@ enum MediaUpload {
|
||||
return url.pathExtension
|
||||
}
|
||||
}
|
||||
|
||||
var localURL: URL {
|
||||
switch self {
|
||||
case .image(let url):
|
||||
return url
|
||||
case .video(let url):
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
var is_image: Bool {
|
||||
if case .image = self {
|
||||
|
||||
@@ -21,8 +21,8 @@ enum MentionType {
|
||||
}
|
||||
}
|
||||
|
||||
struct Mention {
|
||||
let index: Int
|
||||
struct Mention: Equatable {
|
||||
let index: Int?
|
||||
let type: MentionType
|
||||
let ref: ReferencedId
|
||||
}
|
||||
@@ -58,12 +58,30 @@ struct LightningInvoice<T> {
|
||||
}
|
||||
}
|
||||
|
||||
enum Block {
|
||||
enum Block: Equatable {
|
||||
static func == (lhs: Block, rhs: Block) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case (.text(let a), .text(let b)):
|
||||
return a == b
|
||||
case (.mention(let a), .mention(let b)):
|
||||
return a == b
|
||||
case (.hashtag(let a), .hashtag(let b)):
|
||||
return a == b
|
||||
case (.url(let a), .url(let b)):
|
||||
return a == b
|
||||
case (.invoice(let a), .invoice(let b)):
|
||||
return a.string == b.string
|
||||
case (_, _):
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
case text(String)
|
||||
case mention(Mention)
|
||||
case hashtag(String)
|
||||
case url(URL)
|
||||
case invoice(Invoice)
|
||||
case relay(String)
|
||||
|
||||
var is_invoice: Invoice? {
|
||||
if case .invoice(let invoice) = self {
|
||||
@@ -114,7 +132,17 @@ func render_blocks(blocks: [Block]) -> String {
|
||||
return blocks.reduce("") { str, block in
|
||||
switch block {
|
||||
case .mention(let m):
|
||||
return str + "#[\(m.index)]"
|
||||
if let idx = m.index {
|
||||
return str + "#[\(idx)]"
|
||||
} else if m.type == .pubkey, let pk = bech32_pubkey(m.ref.ref_id) {
|
||||
return str + "nostr:\(pk)"
|
||||
} else if let note_id = bech32_note_id(m.ref.ref_id) {
|
||||
return str + "nostr:\(note_id)"
|
||||
} else {
|
||||
return str + m.ref.ref_id
|
||||
}
|
||||
case .relay(let relay):
|
||||
return str + relay
|
||||
case .text(let txt):
|
||||
return str + txt
|
||||
case .hashtag(let htag):
|
||||
@@ -177,14 +205,16 @@ func convert_block(_ b: block_t, tags: [[String]]) -> Block? {
|
||||
return nil
|
||||
}
|
||||
return .text(str)
|
||||
} else if b.type == BLOCK_MENTION {
|
||||
return convert_mention_block(ind: b.block.mention, tags: tags)
|
||||
} else if b.type == BLOCK_MENTION_INDEX {
|
||||
return convert_mention_index_block(ind: b.block.mention_index, tags: tags)
|
||||
} else if b.type == BLOCK_URL {
|
||||
return convert_url_block(b.block.str)
|
||||
} else if b.type == BLOCK_INVOICE {
|
||||
return convert_invoice_block(b.block.invoice)
|
||||
} else if b.type == BLOCK_MENTION_BECH32 {
|
||||
return convert_mention_bech32_block(b.block.mention_bech32)
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -307,6 +337,60 @@ func convert_invoice_block(_ b: invoice_block) -> Block? {
|
||||
return .invoice(Invoice(description: description, amount: amount, string: invstr, expiry: b11.expiry, payment_hash: payment_hash, created_at: created_at))
|
||||
}
|
||||
|
||||
func convert_mention_bech32_block(_ b: mention_bech32_block) -> Block?
|
||||
{
|
||||
switch b.bech32.type {
|
||||
case NOSTR_BECH32_NOTE:
|
||||
let note = b.bech32.data.note;
|
||||
let event_id = hex_encode(Data(bytes: note.event_id, count: 32))
|
||||
let event_id_ref = ReferencedId(ref_id: event_id, relay_id: nil, key: "e")
|
||||
return .mention(Mention(index: nil, type: .event, ref: event_id_ref))
|
||||
|
||||
case NOSTR_BECH32_NEVENT:
|
||||
let nevent = b.bech32.data.nevent;
|
||||
let event_id = hex_encode(Data(bytes: nevent.event_id, count: 32))
|
||||
var relay_id: String? = nil
|
||||
if nevent.relays.num_relays > 0 {
|
||||
relay_id = strblock_to_string(nevent.relays.relays.0)
|
||||
}
|
||||
let event_id_ref = ReferencedId(ref_id: event_id, relay_id: relay_id, key: "e")
|
||||
return .mention(Mention(index: nil, type: .event, ref: event_id_ref))
|
||||
|
||||
case NOSTR_BECH32_NPUB:
|
||||
let npub = b.bech32.data.npub
|
||||
let pubkey = hex_encode(Data(bytes: npub.pubkey, count: 32))
|
||||
let pubkey_ref = ReferencedId(ref_id: pubkey, relay_id: nil, key: "p")
|
||||
return .mention(Mention(index: nil, type: .pubkey, ref: pubkey_ref))
|
||||
|
||||
case NOSTR_BECH32_NPROFILE:
|
||||
let nprofile = b.bech32.data.nprofile
|
||||
let pubkey = hex_encode(Data(bytes: nprofile.pubkey, count: 32))
|
||||
var relay_id: String? = nil
|
||||
if nprofile.relays.num_relays > 0 {
|
||||
relay_id = strblock_to_string(nprofile.relays.relays.0)
|
||||
}
|
||||
let pubkey_ref = ReferencedId(ref_id: pubkey, relay_id: relay_id, key: "p")
|
||||
return .mention(Mention(index: nil, type: .pubkey, ref: pubkey_ref))
|
||||
|
||||
case NOSTR_BECH32_NRELAY:
|
||||
let nrelay = b.bech32.data.nrelay
|
||||
guard let relay_str = strblock_to_string(nrelay.relay) else {
|
||||
return nil
|
||||
}
|
||||
return .relay(relay_str)
|
||||
|
||||
case NOSTR_BECH32_NADDR:
|
||||
// TODO: wtf do I do with this
|
||||
guard let naddr = strblock_to_string(b.str) else {
|
||||
return nil
|
||||
}
|
||||
return .text("nostr:" + naddr)
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
|
||||
if let desc = b11.description {
|
||||
return .description(String(cString: desc))
|
||||
@@ -319,7 +403,7 @@ func convert_invoice_description(b11: bolt11) -> InvoiceDescription? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func convert_mention_block(ind: Int32, tags: [[String]]) -> Block?
|
||||
func convert_mention_index_block(ind: Int32, tags: [[String]]) -> Block?
|
||||
{
|
||||
let ind = Int(ind)
|
||||
|
||||
@@ -557,7 +641,7 @@ func parse_mention_type(_ c: String) -> MentionType? {
|
||||
}
|
||||
|
||||
/// Convert
|
||||
func make_post_tags(post_blocks: [PostBlock], tags: [[String]]) -> PostTags {
|
||||
func make_post_tags(post_blocks: [PostBlock], tags: [[String]], silent_mentions: Bool) -> PostTags {
|
||||
var new_tags = tags
|
||||
var blocks: [Block] = []
|
||||
|
||||
@@ -567,6 +651,14 @@ func make_post_tags(post_blocks: [PostBlock], tags: [[String]]) -> PostTags {
|
||||
guard let mention_type = parse_mention_type(ref.key) else {
|
||||
continue
|
||||
}
|
||||
|
||||
if silent_mentions || mention_type == .event {
|
||||
let mention = Mention(index: nil, type: mention_type, ref: ref)
|
||||
let block = Block.mention(mention)
|
||||
blocks.append(block)
|
||||
continue
|
||||
}
|
||||
|
||||
if let ind = find_tag_ref(type: ref.key, id: ref.ref_id, tags: tags) {
|
||||
let mention = Mention(index: ind, type: mention_type, ref: ref)
|
||||
let block = Block.mention(mention)
|
||||
@@ -592,7 +684,7 @@ func make_post_tags(post_blocks: [PostBlock], tags: [[String]]) -> PostTags {
|
||||
func post_to_event(post: NostrPost, privkey: String, pubkey: String) -> NostrEvent {
|
||||
let tags = post.references.map(refid_to_tag)
|
||||
let post_blocks = parse_post_blocks(content: post.content)
|
||||
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags)
|
||||
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags, silent_mentions: false)
|
||||
let content = render_blocks(blocks: post_tags.blocks)
|
||||
let new_ev = NostrEvent(content: content, pubkey: pubkey, kind: post.kind.rawValue, tags: post_tags.tags)
|
||||
new_ev.calculate_id()
|
||||
|
||||
76
damus/Models/MutedThreadsManager.swift
Normal file
76
damus/Models/MutedThreadsManager.swift
Normal file
@@ -0,0 +1,76 @@
|
||||
//
|
||||
// MutedThreadsManager.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 4/6/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
fileprivate func getMutedThreadsKey(pubkey: String) -> String {
|
||||
pk_setting_key(pubkey, key: "muted_threads")
|
||||
}
|
||||
|
||||
func loadMutedThreads(pubkey: String) -> [String] {
|
||||
let key = getMutedThreadsKey(pubkey: pubkey)
|
||||
return UserDefaults.standard.stringArray(forKey: key) ?? []
|
||||
}
|
||||
|
||||
func saveMutedThreads(pubkey: String, currentValue: [String], value: [String]) -> Bool {
|
||||
let uniqueMutedThreads = Array(Set(value))
|
||||
|
||||
if uniqueMutedThreads != currentValue {
|
||||
UserDefaults.standard.set(uniqueMutedThreads, forKey: getMutedThreadsKey(pubkey: pubkey))
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
class MutedThreadsManager: ObservableObject {
|
||||
|
||||
private let userDefaults = UserDefaults.standard
|
||||
private let keypair: Keypair
|
||||
|
||||
private var _mutedThreadsSet: Set<String>
|
||||
private var _mutedThreads: [String]
|
||||
var mutedThreads: [String] {
|
||||
get {
|
||||
return _mutedThreads
|
||||
}
|
||||
set {
|
||||
if saveMutedThreads(pubkey: keypair.pubkey, currentValue: _mutedThreads, value: newValue) {
|
||||
self._mutedThreads = newValue
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(keypair: Keypair) {
|
||||
self._mutedThreads = loadMutedThreads(pubkey: keypair.pubkey)
|
||||
self._mutedThreadsSet = Set(_mutedThreads)
|
||||
self.keypair = keypair
|
||||
}
|
||||
|
||||
func isMutedThread(_ ev: NostrEvent, privkey: String?) -> Bool {
|
||||
return _mutedThreadsSet.contains(ev.thread_id(privkey: privkey))
|
||||
}
|
||||
|
||||
func updateMutedThread(_ ev: NostrEvent) {
|
||||
let threadId = ev.thread_id(privkey: nil)
|
||||
if isMutedThread(ev, privkey: keypair.privkey) {
|
||||
mutedThreads = mutedThreads.filter { $0 != threadId }
|
||||
_mutedThreadsSet.remove(threadId)
|
||||
notify(.unmute_thread, ev)
|
||||
} else {
|
||||
mutedThreads.append(threadId)
|
||||
_mutedThreadsSet.insert(threadId)
|
||||
notify(.mute_thread, ev)
|
||||
}
|
||||
}
|
||||
|
||||
func clearAll() {
|
||||
mutedThreads = []
|
||||
_mutedThreadsSet.removeAll()
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
for el in zaps {
|
||||
let evid = el.key
|
||||
let zapgrp = el.value
|
||||
|
||||
|
||||
let notif: NotificationItem = .event_zap(evid, zapgrp)
|
||||
notifs.append(notif)
|
||||
}
|
||||
@@ -233,7 +233,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
}
|
||||
}
|
||||
|
||||
func insert_event(_ ev: NostrEvent) -> Bool {
|
||||
func insert_event(_ ev: NostrEvent, damus_state: DamusState) -> Bool {
|
||||
if should_queue {
|
||||
return insert_uniq_sorted_event_created(events: &incoming_events, new_ev: ev)
|
||||
}
|
||||
@@ -246,7 +246,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
return false
|
||||
}
|
||||
|
||||
func insert_zap(_ zap: Zap) -> Bool {
|
||||
func insert_zap(_ zap: Zap, damus_state: DamusState) -> Bool {
|
||||
if should_queue {
|
||||
return insert_uniq_sorted_zap_by_created(zaps: &incoming_zaps, new_zap: zap)
|
||||
}
|
||||
@@ -300,7 +300,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
}
|
||||
}
|
||||
|
||||
func flush() -> Bool {
|
||||
func flush(_ damus_state: DamusState) -> Bool {
|
||||
var inserted = false
|
||||
|
||||
for zap in incoming_zaps {
|
||||
|
||||
@@ -119,7 +119,7 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
} else if ev.known_kind == .contacts {
|
||||
handle_profile_contact_event(ev)
|
||||
} else if ev.known_kind == .metadata {
|
||||
process_metadata_event(our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev)
|
||||
process_metadata_event(events: damus.events, our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev)
|
||||
}
|
||||
seen_event.insert(ev.id)
|
||||
}
|
||||
@@ -133,11 +133,16 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
return
|
||||
}
|
||||
switch resp {
|
||||
case .ok:
|
||||
break
|
||||
case .event(_, let ev):
|
||||
add_event(ev)
|
||||
case .notice(let notice):
|
||||
notify(.notice, notice)
|
||||
case .eose:
|
||||
if resp.subid == sub_id {
|
||||
load_profiles(profiles_subid: prof_subid, relay_id: relay_id, load: .from_events(events.events), damus_state: damus)
|
||||
}
|
||||
progress += 1
|
||||
break
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ class SearchHomeModel: ObservableObject {
|
||||
}
|
||||
|
||||
func get_base_filter() -> NostrFilter {
|
||||
var filter = NostrFilter.filter_kinds([1, 42])
|
||||
var filter = NostrFilter.filter_kinds([NostrKind.text.rawValue, NostrKind.chat.rawValue])
|
||||
filter.limit = self.limit
|
||||
filter.until = Int64(Date.now.timeIntervalSince1970)
|
||||
return filter
|
||||
@@ -68,6 +68,8 @@ class SearchHomeModel: ObservableObject {
|
||||
}
|
||||
case .notice(let msg):
|
||||
print("search home notice: \(msg)")
|
||||
case .ok:
|
||||
break
|
||||
case .eose(let sub_id):
|
||||
loading = false
|
||||
|
||||
@@ -126,11 +128,14 @@ func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent]
|
||||
var pubkeys = Set<String>()
|
||||
|
||||
for ev in events {
|
||||
if profiles.lookup(id: ev.pubkey) != nil {
|
||||
continue
|
||||
// lookup profiles from boosted events
|
||||
if ev.known_kind == .boost, let bev = ev.inner_event, profiles.lookup(id: bev.pubkey) == nil {
|
||||
pubkeys.insert(bev.pubkey)
|
||||
}
|
||||
|
||||
pubkeys.insert(ev.pubkey)
|
||||
if profiles.lookup(id: ev.pubkey) == nil {
|
||||
pubkeys.insert(ev.pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
return Array(pubkeys)
|
||||
@@ -159,7 +164,7 @@ func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad
|
||||
}
|
||||
|
||||
if ev.known_kind == .metadata {
|
||||
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class SearchModel: ObservableObject {
|
||||
func subscribe() {
|
||||
// since 1 month
|
||||
search.limit = self.limit
|
||||
search.kinds = [1,5,7]
|
||||
search.kinds = [NostrKind.text.rawValue, NostrKind.delete.rawValue, NostrKind.like.rawValue]
|
||||
|
||||
//likes_filter.ids = ref_events.referenced_ids!
|
||||
|
||||
@@ -131,6 +131,9 @@ func handle_subid_event(pool: RelayPool, relay_id: String, ev: NostrConnectionEv
|
||||
case .event(let ev_subid, let ev):
|
||||
handle(ev_subid, ev)
|
||||
return (ev_subid, false)
|
||||
|
||||
case .ok:
|
||||
return (nil, false)
|
||||
|
||||
case .notice(let note):
|
||||
if note.contains("Too many subscription filters") {
|
||||
|
||||
@@ -77,18 +77,23 @@ class ThreadModel: ObservableObject {
|
||||
var meta_events = NostrFilter()
|
||||
var event_filter = NostrFilter()
|
||||
var ref_events = NostrFilter()
|
||||
//var likes_filter = NostrFilter.filter_kinds(7])
|
||||
|
||||
let thread_id = event.thread_id(privkey: nil)
|
||||
|
||||
ref_events.referenced_ids = [thread_id, event.id]
|
||||
ref_events.kinds = [1]
|
||||
ref_events.kinds = [NostrKind.text.rawValue]
|
||||
ref_events.limit = 1000
|
||||
|
||||
event_filter.ids = [thread_id, event.id]
|
||||
|
||||
meta_events.referenced_ids = [event.id]
|
||||
meta_events.kinds = [9735, 1, 6, 7]
|
||||
|
||||
var kinds = [NostrKind.zap.rawValue, NostrKind.text.rawValue, NostrKind.boost.rawValue]
|
||||
if !damus_state.settings.hide_reactions {
|
||||
kinds.append(NostrKind.like.rawValue)
|
||||
}
|
||||
meta_events.kinds = kinds
|
||||
|
||||
meta_events.limit = 1000
|
||||
|
||||
/*
|
||||
@@ -114,6 +119,7 @@ class ThreadModel: ObservableObject {
|
||||
}
|
||||
|
||||
let the_ev = damus_state.events.upsert(ev)
|
||||
damus_state.replies.count_replies(the_ev)
|
||||
damus_state.events.add_replies(ev: the_ev)
|
||||
|
||||
event_map.insert(ev)
|
||||
@@ -128,7 +134,7 @@ class ThreadModel: ObservableObject {
|
||||
}
|
||||
|
||||
if ev.known_kind == .metadata {
|
||||
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
} else if ev.is_textlike {
|
||||
self.add_event(ev, privkey: self.damus_state.keypair.privkey)
|
||||
}
|
||||
|
||||
@@ -26,11 +26,13 @@ func set_default_zap_amount(pubkey: String, amount: Int) {
|
||||
UserDefaults.standard.setValue(amount, forKey: key)
|
||||
}
|
||||
|
||||
func get_default_zap_amount(pubkey: String) -> Int? {
|
||||
let fallback_zap_amount = 1000
|
||||
|
||||
func get_default_zap_amount(pubkey: String) -> Int {
|
||||
let key = default_zap_setting_key(pubkey: pubkey)
|
||||
let amt = UserDefaults.standard.integer(forKey: key)
|
||||
if amt == 0 {
|
||||
return nil
|
||||
return fallback_zap_amount
|
||||
}
|
||||
return amt
|
||||
}
|
||||
@@ -128,6 +130,66 @@ class UserSettingsStore: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
@Published var zap_notification: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(zap_notification, forKey: "zap_notification")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var mention_notification: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(mention_notification, forKey: "mention_notification")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var repost_notification: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(repost_notification, forKey: "repost_notification")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var dm_notification: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(dm_notification, forKey: "dm_notification")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var like_notification: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(like_notification, forKey: "like_notification")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var notification_only_from_following: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(notification_only_from_following, forKey: "notification_only_from_following")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var translate_dms: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(translate_dms, forKey: "translate_dms")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var truncate_timeline_text: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(truncate_timeline_text, forKey: "truncate_timeline_text")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var notification_indicators: Int {
|
||||
didSet {
|
||||
UserDefaults.standard.set(notification_indicators, forKey: "notification_indicators")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var truncate_mention_text: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(truncate_mention_text, forKey: "truncate_mention_text")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var auto_translate: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(auto_translate, forKey: "auto_translate")
|
||||
@@ -140,6 +202,12 @@ class UserSettingsStore: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
@Published var hide_reactions: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(hide_reactions, forKey: "hide_reactions")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var translation_service: TranslationService {
|
||||
didSet {
|
||||
UserDefaults.standard.set(translation_service.rawValue, forKey: "translation_service")
|
||||
@@ -221,9 +289,20 @@ class UserSettingsStore: ObservableObject {
|
||||
|
||||
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
|
||||
zap_vibration = UserDefaults.standard.object(forKey: "zap_vibration") as? Bool ?? false
|
||||
zap_notification = UserDefaults.standard.object(forKey: "zap_notification") as? Bool ?? true
|
||||
mention_notification = UserDefaults.standard.object(forKey: "mention_notification") as? Bool ?? true
|
||||
repost_notification = UserDefaults.standard.object(forKey: "repost_notification") as? Bool ?? true
|
||||
like_notification = UserDefaults.standard.object(forKey: "like_notification") as? Bool ?? true
|
||||
dm_notification = UserDefaults.standard.object(forKey: "dm_notification") as? Bool ?? true
|
||||
notification_indicators = UserDefaults.standard.object(forKey: "notification_indicators") as? Int ?? NewEventsBits.all.rawValue
|
||||
notification_only_from_following = UserDefaults.standard.object(forKey: "notification_only_from_following") as? Bool ?? false
|
||||
translate_dms = UserDefaults.standard.object(forKey: "translate_dms") as? Bool ?? false
|
||||
truncate_timeline_text = UserDefaults.standard.object(forKey: "truncate_timeline_text") as? Bool ?? false
|
||||
truncate_mention_text = UserDefaults.standard.object(forKey: "truncate_mention_text") as? Bool ?? false
|
||||
disable_animation = should_disable_image_animation()
|
||||
auto_translate = UserDefaults.standard.object(forKey: "auto_translate") as? Bool ?? true
|
||||
show_only_preferred_languages = UserDefaults.standard.object(forKey: "show_only_preferred_languages") as? Bool ?? false
|
||||
hide_reactions = UserDefaults.standard.object(forKey: "hide_reactions") 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.
|
||||
|
||||
@@ -42,42 +42,42 @@ enum Wallet: String, CaseIterable, Identifiable {
|
||||
return .init(index: -1, tag: "systemdefaultwallet", displayName: NSLocalizedString("Local default", comment: "Dropdown option label for system default for Lightning wallet."),
|
||||
link: "lightning:", appStoreLink: "lightning:", image: "")
|
||||
case .strike:
|
||||
return .init(index: 0, tag: "strike", displayName: NSLocalizedString("Strike", comment: "Dropdown option label for Lightning wallet, Strike."), link: "strike:",
|
||||
return .init(index: 0, tag: "strike", displayName: "Strike", link: "strike:",
|
||||
appStoreLink: "https://apps.apple.com/us/app/strike-bitcoin-payments/id1488724463", image: "strike")
|
||||
case .cashapp:
|
||||
return .init(index: 1, tag: "cashapp", displayName: NSLocalizedString("Cash App", comment: "Dropdown option label for Lightning wallet, Cash App."), link: "https://cash.app/launch/lightning/",
|
||||
return .init(index: 1, tag: "cashapp", displayName: "Cash App", link: "https://cash.app/launch/lightning/",
|
||||
appStoreLink: "https://apps.apple.com/us/app/cash-app/id711923939", image: "cashapp")
|
||||
case .muun:
|
||||
return .init(index: 2, tag: "muun", displayName: NSLocalizedString("Muun", comment: "Dropdown option label for Lightning wallet, Muun."), link: "muun:", appStoreLink: "https://apps.apple.com/us/app/muun-wallet/id1482037683", image: "muun")
|
||||
return .init(index: 2, tag: "muun", displayName: "Muun", link: "muun:", appStoreLink: "https://apps.apple.com/us/app/muun-wallet/id1482037683", image: "muun")
|
||||
case .bluewallet:
|
||||
return .init(index: 3, tag: "bluewallet", displayName: NSLocalizedString("Blue Wallet", comment: "Dropdown option label for Lightning wallet, Blue Wallet."), link: "bluewallet:lightning:",
|
||||
return .init(index: 3, tag: "bluewallet", displayName: "Blue Wallet", link: "bluewallet:lightning:",
|
||||
appStoreLink: "https://apps.apple.com/us/app/bluewallet-bitcoin-wallet/id1376878040", image: "bluewallet")
|
||||
case .walletofsatoshi:
|
||||
return .init(index: 4, tag: "walletofsatoshi", displayName: NSLocalizedString("Wallet of Satoshi", comment: "Dropdown option label for Lightning wallet, Wallet of Satoshi."), link: "walletofsatoshi:lightning:",
|
||||
return .init(index: 4, tag: "walletofsatoshi", displayName: "Wallet of Satoshi", link: "walletofsatoshi:lightning:",
|
||||
appStoreLink: "https://apps.apple.com/us/app/wallet-of-satoshi/id1438599608", image: "walletofsatoshi")
|
||||
case .zebedee:
|
||||
return .init(index: 5, tag: "zebedee", displayName: NSLocalizedString("Zebedee", comment: "Dropdown option label for Lightning wallet, Zebedee."), link: "zebedee:lightning:",
|
||||
return .init(index: 5, tag: "zebedee", displayName: "Zebedee", link: "zebedee:lightning:",
|
||||
appStoreLink: "https://apps.apple.com/us/app/zebedee-wallet/id1484394401", image: "zebedee")
|
||||
case .zeusln:
|
||||
return .init(index: 6, tag: "zeusln", displayName: NSLocalizedString("Zeus LN", comment: "Dropdown option label for Lightning wallet, Zeus LN."), link: "zeusln:lightning:",
|
||||
return .init(index: 6, tag: "zeusln", displayName: "Zeus LN", link: "zeusln:lightning:",
|
||||
appStoreLink: "https://apps.apple.com/us/app/zeus-ln/id1456038895", image: "zeusln")
|
||||
case .lnlink:
|
||||
return .init(index: 7, tag: "lnlink", displayName: NSLocalizedString("LNLink", comment: "Dropdown option label for Lightning wallet, LNLink."), link: "lnlink:lightning:",
|
||||
return .init(index: 7, tag: "lnlink", displayName: "LNLink", link: "lnlink:lightning:",
|
||||
appStoreLink: "https://testflight.apple.com/join/aNY4yuuZ", image: "lnlink")
|
||||
case .phoenix:
|
||||
return .init(index: 8, tag: "phoenix", displayName: NSLocalizedString("Phoenix", comment: "Dropdown option label for Lightning wallet, Phoenix."), link: "phoenix://",
|
||||
return .init(index: 8, tag: "phoenix", displayName: "Phoenix", link: "phoenix://",
|
||||
appStoreLink: "https://apps.apple.com/us/app/phoenix-wallet/id1544097028", image: "phoenix")
|
||||
case .breez:
|
||||
return .init(index: 9, tag: "breez", displayName: NSLocalizedString("Breez", comment: "Dropdown option label for Lightning wallet, Breez."), link: "breez:",
|
||||
return .init(index: 9, tag: "breez", displayName: "Breez", link: "breez:",
|
||||
appStoreLink: "https://apps.apple.com/us/app/breez-lightning-client-pos/id1463604142", image: "breez")
|
||||
case .bitcoinbeach:
|
||||
return .init(index: 10, tag: "bitcoinbeach", displayName: NSLocalizedString("Bitcoin Beach", comment: "Dropdown option label for Lightning wallet, Bitcoin Beach."), link: "bitcoinbeach://",
|
||||
return .init(index: 10, tag: "bitcoinbeach", displayName: "Bitcoin Beach", link: "bitcoinbeach://",
|
||||
appStoreLink: "https://apps.apple.com/sv/app/bitcoin-beach-wallet/id1531383905", image: "bbw")
|
||||
case .blixtwallet:
|
||||
return .init(index: 11, tag: "blixtwallet", displayName: NSLocalizedString("Blixt Wallet", comment: "Dropdown option label for Lightning wallet, Blixt Wallet"), link: "blixtwallet:lightning:",
|
||||
return .init(index: 11, tag: "blixtwallet", displayName: "Blixt Wallet", link: "blixtwallet:lightning:",
|
||||
appStoreLink: "https://testflight.apple.com/join/EXvGhRzS", image: "blixt-wallet")
|
||||
case .river:
|
||||
return .init(index: 12, tag: "river", displayName: NSLocalizedString("River", comment: "Dropdown option label for Lightning wallet, River"), link: "river://",
|
||||
return .init(index: 12, tag: "river", displayName: "River", link: "river://",
|
||||
appStoreLink: "https://apps.apple.com/us/app/river-buy-mine-bitcoin/id1536176542", image: "river")
|
||||
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ class ZapsModel: ObservableObject {
|
||||
}
|
||||
|
||||
func subscribe() {
|
||||
var filter = NostrFilter.filter_kinds([9735])
|
||||
var filter = NostrFilter.filter_kinds([NostrKind.zap.rawValue])
|
||||
switch target {
|
||||
case .profile(let profile_id):
|
||||
filter.pubkeys = [profile_id]
|
||||
@@ -46,6 +46,8 @@ class ZapsModel: ObservableObject {
|
||||
}
|
||||
|
||||
switch resp {
|
||||
case .ok:
|
||||
break
|
||||
case .notice:
|
||||
break
|
||||
case .eose:
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct Profile: Codable {
|
||||
class Profile: Codable {
|
||||
var value: [String: AnyCodable]
|
||||
|
||||
init (name: String?, display_name: String?, about: String?, picture: String?, banner: String?, website: String?, lud06: String?, lud16: String?, nip05: String?) {
|
||||
@@ -39,7 +39,7 @@ struct Profile: Codable {
|
||||
return s
|
||||
}
|
||||
|
||||
private mutating func set_val<T>(_ key: String, _ val: T?) {
|
||||
private func set_val<T>(_ key: String, _ val: T?) {
|
||||
if val == nil {
|
||||
self.value.removeValue(forKey: key)
|
||||
return
|
||||
@@ -48,7 +48,7 @@ struct Profile: Codable {
|
||||
self.value[key] = AnyCodable.init(val)
|
||||
}
|
||||
|
||||
private mutating func set_str(_ key: String, _ val: String?) {
|
||||
private func set_str(_ key: String, _ val: String?) {
|
||||
set_val(key, val)
|
||||
}
|
||||
|
||||
@@ -98,16 +98,33 @@ struct Profile: Codable {
|
||||
}
|
||||
|
||||
var website_url: URL? {
|
||||
return self.website.flatMap { URL(string: $0) }
|
||||
if self.website?.trimmingCharacters(in: .whitespacesAndNewlines) == "" {
|
||||
return nil
|
||||
}
|
||||
return self.website.flatMap { url in
|
||||
let trim = url.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !(trim.hasPrefix("http://") || trim.hasPrefix("https://")) {
|
||||
return URL(string: "https://" + trim)
|
||||
}
|
||||
return URL(string: trim)
|
||||
}
|
||||
}
|
||||
|
||||
private var _lnurl: String? = nil
|
||||
var lnurl: String? {
|
||||
if let _lnurl {
|
||||
return _lnurl
|
||||
}
|
||||
|
||||
guard let addr = lud16 ?? lud06 else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if addr.contains("@") {
|
||||
return lnaddress_to_lnurl(addr);
|
||||
// this is a heavy op and is used a lot in views, cache it!
|
||||
let addr = lnaddress_to_lnurl(addr);
|
||||
self._lnurl = addr
|
||||
return addr
|
||||
}
|
||||
|
||||
if !addr.lowercased().hasPrefix("lnurl") {
|
||||
@@ -130,7 +147,7 @@ struct Profile: Codable {
|
||||
self.value = [:]
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
required init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
self.value = try container.decode([String: AnyCodable].self)
|
||||
}
|
||||
|
||||
@@ -13,11 +13,15 @@ import CryptoKit
|
||||
import NaturalLanguage
|
||||
|
||||
|
||||
|
||||
enum ValidationResult: Decodable {
|
||||
case unknown
|
||||
case ok
|
||||
case bad_id
|
||||
case bad_sig
|
||||
|
||||
var is_bad: Bool {
|
||||
return self == .bad_id || self == .bad_sig
|
||||
}
|
||||
}
|
||||
|
||||
struct OtherEvent {
|
||||
@@ -82,7 +86,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
||||
}
|
||||
|
||||
var too_big: Bool {
|
||||
return self.content.count > 16000
|
||||
return self.content.utf8.count > 16000
|
||||
}
|
||||
|
||||
var should_show_event: Bool {
|
||||
@@ -93,14 +97,6 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
||||
return calculate_event_id(ev: self) == self.id
|
||||
}
|
||||
|
||||
var is_valid: Bool {
|
||||
return validity == .ok
|
||||
}
|
||||
|
||||
lazy var validity: ValidationResult = {
|
||||
return .ok //validate_event(ev: self)
|
||||
}()
|
||||
|
||||
private var _blocks: [Block]? = nil
|
||||
func blocks(_ privkey: String?) -> [Block] {
|
||||
if let bs = _blocks {
|
||||
@@ -538,16 +534,21 @@ func make_first_contact_event(keypair: Keypair) -> NostrEvent? {
|
||||
guard let privkey = keypair.privkey else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
let bootstrap_relays = load_bootstrap_relays(pubkey: keypair.pubkey)
|
||||
let rw_relay_info = RelayInfo(read: true, write: true)
|
||||
var relays: [String: RelayInfo] = [:]
|
||||
for relay in BOOTSTRAP_RELAYS {
|
||||
|
||||
for relay in bootstrap_relays {
|
||||
relays[relay] = rw_relay_info
|
||||
}
|
||||
|
||||
let relay_json = encode_json(relays)!
|
||||
let damus_pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
|
||||
let jb55_pubkey = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245" // lol
|
||||
let tags = [
|
||||
["p", damus_pubkey],
|
||||
["p", jb55_pubkey],
|
||||
["p", keypair.pubkey] // you're a friend of yourself!
|
||||
]
|
||||
let ev = NostrEvent(content: relay_json,
|
||||
@@ -719,11 +720,34 @@ func make_zap_request_event(keypair: FullKeypair, content: String, relays: [Rela
|
||||
return ev
|
||||
}
|
||||
|
||||
func uniq<T: Hashable>(_ xs: [T]) -> [T] {
|
||||
var s = Set<T>()
|
||||
var ys: [T] = []
|
||||
|
||||
for x in xs {
|
||||
if s.contains(x) {
|
||||
continue
|
||||
}
|
||||
s.insert(x)
|
||||
ys.append(x)
|
||||
}
|
||||
|
||||
return ys
|
||||
}
|
||||
|
||||
func gather_reply_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
|
||||
var ids = get_referenced_ids(tags: from.tags, key: "e").first.map { [$0] } ?? []
|
||||
|
||||
ids.append(ReferencedId(ref_id: from.id, relay_id: nil, key: "e"))
|
||||
ids.append(contentsOf: from.referenced_pubkeys.filter { $0.ref_id != our_pubkey })
|
||||
ids.append(contentsOf: uniq(from.referenced_pubkeys.filter { $0.ref_id != our_pubkey }))
|
||||
if from.pubkey != our_pubkey {
|
||||
ids.append(ReferencedId(ref_id: from.pubkey, relay_id: nil, key: "p"))
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func gather_quote_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
|
||||
var ids: [ReferencedId] = []
|
||||
if from.pubkey != our_pubkey {
|
||||
ids.append(ReferencedId(ref_id: from.pubkey, relay_id: nil, key: "p"))
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ struct NostrFilter: Codable, Equatable {
|
||||
}
|
||||
|
||||
public static var filter_text: NostrFilter {
|
||||
return filter_kinds([1])
|
||||
return filter_kinds([NostrKind.text.rawValue])
|
||||
}
|
||||
|
||||
public static func filter_ids(_ ids: [String]) -> NostrFilter {
|
||||
@@ -49,11 +49,11 @@ struct NostrFilter: Codable, Equatable {
|
||||
}
|
||||
|
||||
public static var filter_profiles: NostrFilter {
|
||||
return filter_kinds([0])
|
||||
return filter_kinds([NostrKind.metadata.rawValue])
|
||||
}
|
||||
|
||||
public static var filter_contacts: NostrFilter {
|
||||
return filter_kinds([3])
|
||||
return filter_kinds([NostrKind.contacts.rawValue])
|
||||
}
|
||||
|
||||
public static func filter_authors(_ authors: [String]) -> NostrFilter {
|
||||
|
||||
@@ -21,5 +21,5 @@ struct NostrMetadata: Codable {
|
||||
}
|
||||
|
||||
func create_account_to_metadata(_ model: CreateAccountModel) -> NostrMetadata {
|
||||
return NostrMetadata(display_name: model.real_name, name: model.nick_name, about: model.about, website: nil, nip05: nil, picture: nil, banner: nil, lud06: nil, lud16: nil)
|
||||
return NostrMetadata(display_name: model.real_name, name: model.nick_name, about: model.about, website: nil, nip05: nil, picture: model.profile_image, banner: nil, lud06: nil, lud16: nil)
|
||||
}
|
||||
|
||||
@@ -7,13 +7,22 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct CommandResult {
|
||||
let event_id: String
|
||||
let ok: Bool
|
||||
let msg: String
|
||||
}
|
||||
|
||||
enum NostrResponse: Decodable {
|
||||
case event(String, NostrEvent)
|
||||
case notice(String)
|
||||
case eose(String)
|
||||
case ok(CommandResult)
|
||||
|
||||
var subid: String? {
|
||||
switch self {
|
||||
case .ok(_):
|
||||
return nil
|
||||
case .event(let sub_id, _):
|
||||
return sub_id
|
||||
case .eose(let sub_id):
|
||||
@@ -48,9 +57,23 @@ enum NostrResponse: Decodable {
|
||||
let sub_id = try container.decode(String.self)
|
||||
self = .eose(sub_id)
|
||||
return
|
||||
} else if typ == "OK" {
|
||||
var cr: CommandResult
|
||||
do {
|
||||
let event_id = try container.decode(String.self)
|
||||
let ok = try container.decode(Bool.self)
|
||||
let msg = try container.decode(String.self)
|
||||
cr = CommandResult(event_id: event_id, ok: ok, msg: msg)
|
||||
} catch {
|
||||
print(error)
|
||||
throw error
|
||||
}
|
||||
self = .ok(cr)
|
||||
return
|
||||
//ev.pow = count_hash_leading_zero_bits(ev.id)
|
||||
}
|
||||
|
||||
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "expected EVENT or NOTICE, got \(typ)"))
|
||||
throw DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "expected EVENT, NOTICE or OK, got \(typ)"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ final class RelayConnection: WebSocketDelegate {
|
||||
self.isConnected = false
|
||||
|
||||
case .text(let txt):
|
||||
if txt.count > 2000 {
|
||||
if txt.utf8.count > 2000 {
|
||||
DispatchQueue.global(qos: .default).async {
|
||||
if let ev = decode_nostr_event(txt: txt) {
|
||||
DispatchQueue.main.async {
|
||||
@@ -105,8 +105,6 @@ final class RelayConnection: WebSocketDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
print("decode failed for \(txt)")
|
||||
// TODO: trigger event error
|
||||
|
||||
default:
|
||||
break
|
||||
|
||||
@@ -52,6 +52,10 @@ class RelayPool {
|
||||
var num_connecting: Int {
|
||||
return relays.reduce(0) { n, r in n + (r.connection.isConnecting ? 1 : 0) }
|
||||
}
|
||||
|
||||
var num_connected: Int {
|
||||
return relays.reduce(0) { n, r in n + (r.connection.isConnected ? 1 : 0) }
|
||||
}
|
||||
|
||||
func remove_handler(sub_id: String) {
|
||||
self.handlers = handlers.filter { $0.sub_id != sub_id }
|
||||
@@ -256,7 +260,6 @@ class RelayPool {
|
||||
}
|
||||
}
|
||||
|
||||
// handle reconnect logic, etc?
|
||||
for handler in handlers {
|
||||
handler.callback(relay_id, event)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import Foundation
|
||||
|
||||
|
||||
func created_deleted_account_profile(keypair: FullKeypair) -> NostrEvent {
|
||||
var profile = Profile()
|
||||
let profile = Profile()
|
||||
profile.deleted = true
|
||||
profile.about = "account deleted"
|
||||
profile.name = "nobody"
|
||||
|
||||
44
damus/Util/CompatibleAttribute.swift
Normal file
44
damus/Util/CompatibleAttribute.swift
Normal file
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// CompatibleAttribute.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-06.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
class CompatibleText: Equatable {
|
||||
var text: Text
|
||||
var attributed: AttributedString
|
||||
|
||||
init() {
|
||||
self.text = Text("")
|
||||
self.attributed = AttributedString(stringLiteral: "")
|
||||
}
|
||||
|
||||
init(stringLiteral: String) {
|
||||
self.text = Text(stringLiteral)
|
||||
self.attributed = AttributedString(stringLiteral: stringLiteral)
|
||||
}
|
||||
|
||||
init(text: Text, attributed: AttributedString) {
|
||||
self.text = text
|
||||
self.attributed = attributed
|
||||
}
|
||||
|
||||
init(attributed: AttributedString) {
|
||||
self.text = Text(attributed)
|
||||
self.attributed = attributed
|
||||
}
|
||||
|
||||
static func == (lhs: CompatibleText, rhs: CompatibleText) -> Bool {
|
||||
return lhs.attributed == rhs.attributed
|
||||
}
|
||||
|
||||
static func +(lhs: CompatibleText, rhs: CompatibleText) -> CompatibleText {
|
||||
let combinedText = lhs.text + rhs.text
|
||||
let combinedAttributes = lhs.attributed + rhs.attributed
|
||||
return CompatibleText(text: combinedText, attributed: combinedAttributes)
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ enum DisplayName {
|
||||
|
||||
func parse_display_name(profile: Profile?, pubkey: String) -> DisplayName {
|
||||
if pubkey == "anon" {
|
||||
return .one("Anonymous")
|
||||
return .one(NSLocalizedString("Anonymous", comment: "Placeholder display name of anonymous user."))
|
||||
}
|
||||
|
||||
guard let profile else {
|
||||
|
||||
@@ -13,6 +13,9 @@ class EventCache {
|
||||
private var events: [String: NostrEvent] = [:]
|
||||
private var replies = ReplyMap()
|
||||
private var cancellable: AnyCancellable?
|
||||
private var translations: [String: TranslateStatus] = [:]
|
||||
private var artifacts: [String: NoteArtifacts] = [:]
|
||||
var validation: [String: ValidationResult] = [:]
|
||||
|
||||
//private var thread_latest: [String: Int64]
|
||||
|
||||
@@ -24,6 +27,30 @@ class EventCache {
|
||||
}
|
||||
}
|
||||
|
||||
func is_event_valid(_ evid: String) -> ValidationResult {
|
||||
guard let result = validation[evid] else {
|
||||
return .unknown
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func store_translation_artifacts(evid: String, translated: TranslateStatus) {
|
||||
self.translations[evid] = translated
|
||||
}
|
||||
|
||||
func store_artifacts(evid: String, artifacts: NoteArtifacts) {
|
||||
self.artifacts[evid] = artifacts
|
||||
}
|
||||
|
||||
func lookup_artifacts(evid: String) -> NoteArtifacts? {
|
||||
return self.artifacts[evid]
|
||||
}
|
||||
|
||||
func lookup_translated_artifacts(evid: String) -> TranslateStatus? {
|
||||
return self.translations[evid]
|
||||
}
|
||||
|
||||
func parent_events(event: NostrEvent) -> [NostrEvent] {
|
||||
var parents: [NostrEvent] = []
|
||||
|
||||
@@ -87,6 +114,8 @@ class EventCache {
|
||||
|
||||
private func prune() {
|
||||
events = [:]
|
||||
translations = [:]
|
||||
artifacts = [:]
|
||||
replies.replies = [:]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ extension KFOptionSetter {
|
||||
maxSize: imageContext.maxMebibyteSize(),
|
||||
downsampleSize: imageContext.downsampleSize()
|
||||
)
|
||||
options.loadDiskFileSynchronously = false
|
||||
options.backgroundDecode = true
|
||||
options.cacheOriginalImage = true
|
||||
options.scaleFactor = UIScreen.main.scale
|
||||
|
||||
68
damus/Util/Hashtags.swift
Normal file
68
damus/Util/Hashtags.swift
Normal file
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// Hashtags.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-06.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct CustomHashtag {
|
||||
let name: String
|
||||
let offset: CGFloat?
|
||||
let color: Color?
|
||||
|
||||
init(name: String, color: Color? = nil, offset: CGFloat? = nil) {
|
||||
self.name = name
|
||||
self.color = color
|
||||
self.offset = offset
|
||||
}
|
||||
|
||||
static let coffee = CustomHashtag(name: "coffee", color: DamusColors.brown, offset: -1.0)
|
||||
static let bitcoin = CustomHashtag(name: "bitcoin", color: Color.orange, offset: -3.0)
|
||||
static let nostr = CustomHashtag(name: "nostr", color: DamusColors.purple, offset: -2.0)
|
||||
static let plebchain = CustomHashtag(name: "plebchain", color: DamusColors.deepPurple, offset: -3.0)
|
||||
static let zap = CustomHashtag(name: "zap", color: DamusColors.yellow, offset: -4.0)
|
||||
}
|
||||
|
||||
|
||||
let custom_hashtags: [String: CustomHashtag] = [
|
||||
"bitcoin": CustomHashtag.bitcoin,
|
||||
"btc": CustomHashtag.bitcoin,
|
||||
"nostr": CustomHashtag.nostr,
|
||||
"coffee": CustomHashtag.coffee,
|
||||
"coffeechain": CustomHashtag.coffee,
|
||||
"plebchain": CustomHashtag.plebchain,
|
||||
"zap": CustomHashtag.zap,
|
||||
"zapathon": CustomHashtag.zap,
|
||||
]
|
||||
|
||||
func hashtag_str(_ htag: String) -> CompatibleText {
|
||||
var attributedString = AttributedString(stringLiteral: "#\(htag)")
|
||||
attributedString.link = URL(string: "damus:t:\(htag)")
|
||||
|
||||
let lowertag = htag.lowercased()
|
||||
|
||||
var text = Text(attributedString)
|
||||
if let custom_hashtag = custom_hashtags[lowertag] {
|
||||
if let col = custom_hashtag.color {
|
||||
attributedString.foregroundColor = col
|
||||
}
|
||||
|
||||
let name = custom_hashtag.name
|
||||
|
||||
if let img = UIImage(named: "\(name)-hashtag") {
|
||||
attributedString = attributedString + " "
|
||||
attributed_string_attach_icon(&attributedString, img: img)
|
||||
}
|
||||
text = Text(attributedString)
|
||||
let img = Image("\(name)-hashtag")
|
||||
text = text + Text("\(img)").baselineOffset(custom_hashtag.offset ?? 0.0)
|
||||
} else {
|
||||
attributedString.foregroundColor = DamusColors.purple
|
||||
}
|
||||
|
||||
return CompatibleText(text: text, attributed: attributedString)
|
||||
}
|
||||
|
||||
47
damus/Util/LocalNotification.swift
Normal file
47
damus/Util/LocalNotification.swift
Normal file
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// LocalNotification.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-15.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct LossyLocalNotification {
|
||||
let type: LocalNotificationType
|
||||
let event_id: String
|
||||
|
||||
func to_user_info() -> [AnyHashable: Any] {
|
||||
return [
|
||||
"type": self.type.rawValue,
|
||||
"evid": self.event_id
|
||||
]
|
||||
}
|
||||
|
||||
static func from_user_info(user_info: [AnyHashable: Any]) -> LossyLocalNotification {
|
||||
let target_id = user_info["evid"] as! String
|
||||
let typestr = user_info["type"] as! String
|
||||
let type = LocalNotificationType(rawValue: typestr)!
|
||||
|
||||
return LossyLocalNotification(type: type, event_id: target_id)
|
||||
}
|
||||
}
|
||||
|
||||
struct LocalNotification {
|
||||
let type: LocalNotificationType
|
||||
let event: NostrEvent
|
||||
let target: NostrEvent
|
||||
let content: String
|
||||
|
||||
func to_lossy() -> LossyLocalNotification {
|
||||
return LossyLocalNotification(type: self.type, event_id: self.target.id)
|
||||
}
|
||||
}
|
||||
|
||||
enum LocalNotificationType: String {
|
||||
case dm
|
||||
case like
|
||||
case mention
|
||||
case repost
|
||||
case zap
|
||||
}
|
||||
@@ -86,8 +86,8 @@ extension Notification.Name {
|
||||
static var report: Notification.Name {
|
||||
return Notification.Name("report")
|
||||
}
|
||||
static var block: Notification.Name {
|
||||
return Notification.Name("block")
|
||||
static var mute: Notification.Name {
|
||||
return Notification.Name("mute")
|
||||
}
|
||||
static var new_mutes: Notification.Name {
|
||||
return Notification.Name("new_mutes")
|
||||
@@ -104,6 +104,18 @@ extension Notification.Name {
|
||||
static var zapping: Notification.Name {
|
||||
return Notification.Name("zapping")
|
||||
}
|
||||
static var mute_thread: Notification.Name {
|
||||
return Notification.Name("mute_thread")
|
||||
}
|
||||
static var unmute_thread: Notification.Name {
|
||||
return Notification.Name("unmute_thread")
|
||||
}
|
||||
static var local_notification: Notification.Name {
|
||||
return Notification.Name("local_notification")
|
||||
}
|
||||
static var hide_reactions: Notification.Name {
|
||||
return Notification.Name("hide_reactions")
|
||||
}
|
||||
}
|
||||
|
||||
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {
|
||||
|
||||
@@ -6,3 +6,116 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
class Relayer {
|
||||
let relay: String
|
||||
var attempts: Int
|
||||
var retry_after: Double
|
||||
var last_attempt: Int64?
|
||||
|
||||
init(relay: String, attempts: Int, retry_after: Double) {
|
||||
self.relay = relay
|
||||
self.attempts = attempts
|
||||
self.retry_after = retry_after
|
||||
self.last_attempt = nil
|
||||
}
|
||||
}
|
||||
|
||||
class PostedEvent {
|
||||
let event: NostrEvent
|
||||
var remaining: [Relayer]
|
||||
|
||||
init(event: NostrEvent, remaining: [String]) {
|
||||
self.event = event
|
||||
self.remaining = remaining.map {
|
||||
Relayer(relay: $0, attempts: 0, retry_after: 2.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PostBox {
|
||||
let pool: RelayPool
|
||||
var events: [String: PostedEvent]
|
||||
|
||||
init(pool: RelayPool) {
|
||||
self.pool = pool
|
||||
self.events = [:]
|
||||
pool.register_handler(sub_id: "postbox", handler: handle_event)
|
||||
}
|
||||
|
||||
func try_flushing_events() {
|
||||
let now = Int64(Date().timeIntervalSince1970)
|
||||
for kv in events {
|
||||
let event = kv.value
|
||||
for relayer in event.remaining {
|
||||
if relayer.last_attempt == nil || (now >= (relayer.last_attempt! + Int64(relayer.retry_after))) {
|
||||
print("attempt #\(relayer.attempts) to flush event '\(event.event.content)' to \(relayer.relay) after \(relayer.retry_after) seconds")
|
||||
flush_event(event, to_relay: relayer)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handle_event(relay_id: String, _ ev: NostrConnectionEvent) {
|
||||
try_flushing_events()
|
||||
|
||||
guard case .nostr_event(let resp) = ev else {
|
||||
return
|
||||
}
|
||||
|
||||
guard case .ok(let cr) = resp else {
|
||||
return
|
||||
}
|
||||
|
||||
remove_relayer(relay_id: relay_id, event_id: cr.event_id)
|
||||
}
|
||||
|
||||
func remove_relayer(relay_id: String, event_id: String) {
|
||||
guard let ev = self.events[event_id] else {
|
||||
return
|
||||
}
|
||||
ev.remaining = ev.remaining.filter {
|
||||
$0.relay != relay_id
|
||||
}
|
||||
if ev.remaining.count == 0 {
|
||||
self.events.removeValue(forKey: event_id)
|
||||
}
|
||||
}
|
||||
|
||||
private func flush_event(_ event: PostedEvent, to_relay: Relayer? = nil) {
|
||||
var relayers = event.remaining
|
||||
if let to_relay {
|
||||
relayers = [to_relay]
|
||||
}
|
||||
|
||||
for relayer in relayers {
|
||||
relayer.attempts += 1
|
||||
relayer.last_attempt = Int64(Date().timeIntervalSince1970)
|
||||
relayer.retry_after *= 1.5
|
||||
pool.send(.event(event.event), to: [relayer.relay])
|
||||
}
|
||||
}
|
||||
|
||||
func flush() {
|
||||
for event in events {
|
||||
flush_event(event.value)
|
||||
}
|
||||
}
|
||||
|
||||
func send(_ event: NostrEvent) {
|
||||
// Don't add event if we already have it
|
||||
if events[event.id] != nil {
|
||||
return
|
||||
}
|
||||
|
||||
let remaining = pool.descriptors.map {
|
||||
$0.url.absoluteString
|
||||
}
|
||||
|
||||
let posted_ev = PostedEvent(event: event, remaining: remaining)
|
||||
events[event.id] = posted_ev
|
||||
|
||||
flush_event(posted_ev)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,12 +24,21 @@ enum Preview {
|
||||
}
|
||||
|
||||
class PreviewCache {
|
||||
var previews: [String: Preview]
|
||||
private var previews: [String: Preview]
|
||||
private var image_meta: [String: ImageFill]
|
||||
|
||||
func lookup(_ evid: String) -> Preview? {
|
||||
return previews[evid]
|
||||
}
|
||||
|
||||
func lookup_image_meta(_ evid: String) -> ImageFill? {
|
||||
return image_meta[evid]
|
||||
}
|
||||
|
||||
func cache_image_meta(evid: String, image_fill: ImageFill) {
|
||||
self.image_meta[evid] = image_fill
|
||||
}
|
||||
|
||||
func store(evid: String, preview: LPLinkMetadata?) {
|
||||
switch preview {
|
||||
case .none:
|
||||
@@ -41,5 +50,6 @@ class PreviewCache {
|
||||
|
||||
init() {
|
||||
self.previews = [:]
|
||||
self.image_meta = [:]
|
||||
}
|
||||
}
|
||||
|
||||
44
damus/Util/Relays/RelayBootstrap.swift
Normal file
44
damus/Util/Relays/RelayBootstrap.swift
Normal file
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// RelayBootstrap.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-04.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
let BOOTSTRAP_RELAYS = [
|
||||
"wss://relay.damus.io",
|
||||
"wss://eden.nostr.land",
|
||||
"wss://nostr.wine",
|
||||
"wss://nos.lol",
|
||||
]
|
||||
|
||||
func bootstrap_relays_setting_key(pubkey: String) -> String {
|
||||
return pk_setting_key(pubkey, key: "bootstrap_relays")
|
||||
}
|
||||
|
||||
func save_bootstrap_relays(pubkey: String, relays: [String]) {
|
||||
let key = bootstrap_relays_setting_key(pubkey: pubkey)
|
||||
|
||||
UserDefaults.standard.set(relays, forKey: key)
|
||||
}
|
||||
|
||||
func load_bootstrap_relays(pubkey: String) -> [String] {
|
||||
let key = bootstrap_relays_setting_key(pubkey: pubkey)
|
||||
|
||||
guard let relays = UserDefaults.standard.stringArray(forKey: key) else {
|
||||
print("loading default bootstrap relays")
|
||||
return BOOTSTRAP_RELAYS.map { $0 }
|
||||
}
|
||||
|
||||
if relays.count == 0 {
|
||||
print("loading default bootstrap relays")
|
||||
return BOOTSTRAP_RELAYS.map { $0 }
|
||||
}
|
||||
|
||||
let loaded_relays = Array(Set(relays + BOOTSTRAP_RELAYS))
|
||||
print("Loading custom bootstrap relays: \(loaded_relays)")
|
||||
return loaded_relays
|
||||
}
|
||||
|
||||
54
damus/Util/ReplyCounter.swift
Normal file
54
damus/Util/ReplyCounter.swift
Normal file
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// ReplyCounter.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-04-04.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class ReplyCounter {
|
||||
private var replies: [String: Int]
|
||||
private var counted: Set<String>
|
||||
private var our_replies: [String: NostrEvent]
|
||||
private let our_pubkey: String
|
||||
|
||||
init(our_pubkey: String) {
|
||||
self.our_pubkey = our_pubkey
|
||||
replies = [:]
|
||||
counted = Set()
|
||||
our_replies = [:]
|
||||
}
|
||||
|
||||
func our_reply(_ evid: String) -> NostrEvent? {
|
||||
return our_replies[evid]
|
||||
}
|
||||
|
||||
func get_replies(_ evid: String) -> Int {
|
||||
return replies[evid] ?? 0
|
||||
}
|
||||
|
||||
func count_replies(_ event: NostrEvent) {
|
||||
guard event.is_textlike else {
|
||||
return
|
||||
}
|
||||
|
||||
if counted.contains(event.id) {
|
||||
return
|
||||
}
|
||||
|
||||
counted.insert(event.id)
|
||||
|
||||
for reply in event.direct_replies(nil) {
|
||||
if event.pubkey == our_pubkey {
|
||||
self.our_replies[reply.ref_id] = event
|
||||
}
|
||||
|
||||
if replies[reply.ref_id] != nil {
|
||||
replies[reply.ref_id] = replies[reply.ref_id]! + 1
|
||||
} else {
|
||||
replies[reply.ref_id] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ struct EventActionBar: View {
|
||||
@State var show_share_action: Bool = false
|
||||
|
||||
@ObservedObject var bar: ActionBarModel
|
||||
@ObservedObject var settings: UserSettingsStore
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
@@ -38,6 +39,7 @@ struct EventActionBar: View {
|
||||
self.event = event
|
||||
self.test_lnurl = test_lnurl
|
||||
_bar = ObservedObject(wrappedValue: bar ?? make_actionbar_model(ev: event.id, damus: damus_state))
|
||||
_settings = ObservedObject(wrappedValue: damus_state.settings)
|
||||
}
|
||||
|
||||
var lnurl: String? {
|
||||
@@ -47,10 +49,15 @@ struct EventActionBar: View {
|
||||
var body: some View {
|
||||
HStack {
|
||||
if damus_state.keypair.privkey != nil {
|
||||
EventActionButton(img: "bubble.left", col: nil) {
|
||||
notify(.reply, event)
|
||||
HStack(spacing: 4) {
|
||||
EventActionButton(img: "bubble.left", col: bar.replied ? DamusColors.purple : Color.gray) {
|
||||
notify(.reply, event)
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
|
||||
Text(verbatim: "\(bar.replies > 0 ? "\(bar.replies)" : "")")
|
||||
.font(.footnote.weight(.medium))
|
||||
.foregroundColor(bar.replied ? DamusColors.purple : Color.gray)
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
|
||||
}
|
||||
Spacer()
|
||||
HStack(spacing: 4) {
|
||||
@@ -67,22 +74,25 @@ struct EventActionBar: View {
|
||||
.font(.footnote.weight(.medium))
|
||||
.foregroundColor(bar.boosted ? Color.green : Color.gray)
|
||||
}
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 4) {
|
||||
LikeButton(liked: bar.liked) {
|
||||
if bar.liked {
|
||||
notify(.delete, bar.our_like)
|
||||
} else {
|
||||
send_like()
|
||||
|
||||
if !settings.hide_reactions {
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 4) {
|
||||
LikeButton(liked: bar.liked) {
|
||||
if bar.liked {
|
||||
notify(.delete, bar.our_like)
|
||||
} else {
|
||||
send_like()
|
||||
}
|
||||
}
|
||||
|
||||
Text(verbatim: "\(bar.likes > 0 ? "\(bar.likes)" : "")")
|
||||
.font(.footnote.weight(.medium))
|
||||
.nip05_colorized(gradient: bar.liked)
|
||||
}
|
||||
|
||||
Text(verbatim: "\(bar.likes > 0 ? "\(bar.likes)" : "")")
|
||||
.font(.footnote.weight(.medium))
|
||||
.nip05_colorized(gradient: bar.liked)
|
||||
}
|
||||
|
||||
|
||||
if let lnurl = self.lnurl {
|
||||
Spacer()
|
||||
ZapButton(damus_state: damus_state, event: event, lnurl: lnurl, bar: bar)
|
||||
@@ -132,15 +142,7 @@ struct EventActionBar: View {
|
||||
}
|
||||
|
||||
func send_boost() {
|
||||
guard let privkey = self.damus_state.keypair.privkey else {
|
||||
return
|
||||
}
|
||||
|
||||
let boost = make_boost_event(pubkey: damus_state.keypair.pubkey, privkey: privkey, boosted: self.event)
|
||||
|
||||
self.bar.our_boost = boost
|
||||
|
||||
notify(.boost, boost)
|
||||
notify(.boost, self.event)
|
||||
}
|
||||
|
||||
func send_like() {
|
||||
@@ -154,7 +156,7 @@ struct EventActionBar: View {
|
||||
|
||||
generator.impactOccurred()
|
||||
|
||||
damus_state.pool.send(.event(like_ev))
|
||||
damus_state.postbox.send(like_ev)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,12 +227,12 @@ struct EventActionBar_Previews: PreviewProvider {
|
||||
let ev = NostrEvent(content: "hi", pubkey: pk)
|
||||
|
||||
let bar = ActionBarModel.empty()
|
||||
let likedbar = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, our_like: nil, our_boost: nil, our_zap: nil)
|
||||
let likedbar_ours = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: nil, our_zap: nil)
|
||||
let maxed_bar = ActionBarModel(likes: 999, boosts: 999, zaps: 999, zap_total: 99999999, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_zap: nil)
|
||||
let extra_max_bar = ActionBarModel(likes: 9999, boosts: 9999, zaps: 9999, zap_total: 99999999, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_zap: nil)
|
||||
let mega_max_bar = ActionBarModel(likes: 9999999, boosts: 99999, zaps: 9999, zap_total: 99999999, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_zap: nil)
|
||||
let zapbar = ActionBarModel(likes: 0, boosts: 0, zaps: 5, zap_total: 10000000, our_like: nil, our_boost: nil, our_zap: nil)
|
||||
let likedbar = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
|
||||
let likedbar_ours = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: test_event, our_boost: nil, our_zap: nil, our_reply: nil)
|
||||
let maxed_bar = ActionBarModel(likes: 999, boosts: 999, zaps: 999, zap_total: 99999999, replies: 999, our_like: test_event, our_boost: test_event, our_zap: nil, our_reply: nil)
|
||||
let extra_max_bar = ActionBarModel(likes: 9999, boosts: 9999, zaps: 9999, zap_total: 99999999, replies: 9999, our_like: test_event, our_boost: test_event, our_zap: nil, our_reply: test_event)
|
||||
let mega_max_bar = ActionBarModel(likes: 9999999, boosts: 99999, zaps: 9999, zap_total: 99999999, replies: 9999999, our_like: test_event, our_boost: test_event, our_zap: test_zap, our_reply: test_event)
|
||||
let zapbar = ActionBarModel(likes: 0, boosts: 0, zaps: 5, zap_total: 10000000, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
|
||||
|
||||
VStack(spacing: 50) {
|
||||
EventActionBar(damus_state: ds, event: ev, bar: bar)
|
||||
|
||||
@@ -32,7 +32,7 @@ struct EventDetailBar: View {
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
|
||||
if bar.likes > 0 {
|
||||
if bar.likes > 0 && !state.settings.hide_reactions {
|
||||
NavigationLink(destination: ReactionsView(damus_state: state, model: ReactionsModel(state: state, target: target))) {
|
||||
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'.")
|
||||
|
||||
@@ -40,9 +40,9 @@ func create_upload_request(mediaToUpload: MediaUpload, mediaUploader: MediaUploa
|
||||
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
switch mediaToUpload {
|
||||
case .image(let img):
|
||||
case .image(let url):
|
||||
do {
|
||||
mediaData = try Data(contentsOf: img)
|
||||
mediaData = try Data(contentsOf: url)
|
||||
} catch {
|
||||
return .failed(error)
|
||||
}
|
||||
@@ -80,81 +80,6 @@ func create_upload_request(mediaToUpload: MediaUpload, mediaUploader: MediaUploa
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -197,9 +122,9 @@ enum MediaUploader: String, CaseIterable, Identifiable {
|
||||
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."))
|
||||
return .init(index: -1, tag: "nostrBuild", displayName: "nostr.build")
|
||||
case .nostrImg:
|
||||
return .init(index: 0, tag: "nostrImg", displayName: NSLocalizedString("NostrImg", comment: "Dropdown option label for system default for NostrImg image uploader."))
|
||||
return .init(index: 0, tag: "nostrImg", displayName: "nostrimg.com")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,26 +17,14 @@ struct ConfigView: View {
|
||||
@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
|
||||
@State var show_api_key: Bool = false
|
||||
@State var privkey: String
|
||||
@State var privkey_copied: Bool = false
|
||||
@State var pubkey_copied: Bool = false
|
||||
@State var delete_text: String = ""
|
||||
@State var default_zap_amount: String
|
||||
|
||||
@ObservedObject var settings: UserSettingsStore
|
||||
|
||||
let generator = UIImpactFeedbackGenerator(style: .light)
|
||||
|
||||
private let DELETE_KEYWORD = "DELETE"
|
||||
|
||||
init(state: DamusState) {
|
||||
self.state = state
|
||||
let zap_amt = get_default_zap_amount(pubkey: state.pubkey).map({ "\($0)" }) ?? "1000"
|
||||
_default_zap_amount = State(initialValue: zap_amt)
|
||||
_privkey = State(initialValue: self.state.keypair.privkey_bech32 ?? "")
|
||||
_settings = ObservedObject(initialValue: state.settings)
|
||||
}
|
||||
|
||||
@@ -44,200 +32,32 @@ struct ConfigView: View {
|
||||
colorScheme == .light ? DamusColors.black : DamusColors.white
|
||||
}
|
||||
|
||||
func authenticateLocally(completion: @escaping (Bool) -> Void) {
|
||||
// Need to authenticate only once while ConfigView is presented
|
||||
guard !has_authenticated_locally else {
|
||||
completion(true)
|
||||
return
|
||||
}
|
||||
let context = LAContext()
|
||||
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: nil) {
|
||||
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: NSLocalizedString("Local authentication to access private key", comment: "Face ID usage description shown when trying to access private key")) { success, error in
|
||||
DispatchQueue.main.async {
|
||||
has_authenticated_locally = success
|
||||
completion(success)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If there's no authentication set up on the device, let the user copy the key without it
|
||||
has_authenticated_locally = true
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: (jb55) could be more general but not gonna worry about it atm
|
||||
func CopyButton(is_pk: Bool) -> some View {
|
||||
return Button(action: {
|
||||
let copyKey = {
|
||||
UIPasteboard.general.string = is_pk ? self.state.keypair.pubkey_bech32 : self.privkey
|
||||
self.privkey_copied = !is_pk
|
||||
self.pubkey_copied = is_pk
|
||||
generator.impactOccurred()
|
||||
}
|
||||
if is_pk {
|
||||
// When trying to copy npub
|
||||
copyKey()
|
||||
} else {
|
||||
// When trying to copy nsec
|
||||
if has_authenticated_locally {
|
||||
copyKey()
|
||||
} else {
|
||||
authenticateLocally { success in
|
||||
if success {
|
||||
copyKey()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}) {
|
||||
let copied = is_pk ? self.pubkey_copied : self.privkey_copied
|
||||
Image(systemName: copied ? "checkmark.circle" : "doc.on.doc")
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .leading) {
|
||||
Form {
|
||||
Section(NSLocalizedString("Public Account ID", comment: "Section title for the user's public account ID.")) {
|
||||
HStack {
|
||||
Text(state.keypair.pubkey_bech32)
|
||||
|
||||
CopyButton(is_pk: true)
|
||||
}
|
||||
.clipShape(RoundedRectangle(cornerRadius: 5))
|
||||
}
|
||||
|
||||
if let sec = state.keypair.privkey_bech32 {
|
||||
Section(NSLocalizedString("Secret Account Login Key", comment: "Section title for user's secret account login key.")) {
|
||||
HStack {
|
||||
if show_privkey == false || !has_authenticated_locally {
|
||||
SecureField(NSLocalizedString("Private Key", comment: "Title of the secure field that holds the user's private key."), text: $privkey)
|
||||
.disabled(true)
|
||||
} else {
|
||||
Text(sec)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 5))
|
||||
}
|
||||
|
||||
CopyButton(is_pk: false)
|
||||
}
|
||||
|
||||
Toggle(NSLocalizedString("Show", comment: "Toggle to show or hide user's secret account login key."), isOn: $show_privkey)
|
||||
.onChange(of: show_privkey) { newValue in
|
||||
if newValue {
|
||||
authenticateLocally { success in
|
||||
show_privkey = success
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(NSLocalizedString("Wallet Selector", comment: "Section title for selection of wallet.")) {
|
||||
Toggle(NSLocalizedString("Show wallet selector", comment: "Toggle to show or hide selection of wallet."), isOn: $settings.show_wallet_selector).toggleStyle(.switch)
|
||||
Picker(NSLocalizedString("Select default wallet", comment: "Prompt selection of user's default wallet"),
|
||||
selection: $settings.default_wallet) {
|
||||
ForEach(Wallet.allCases, id: \.self) { wallet in
|
||||
Text(wallet.model.displayName)
|
||||
.tag(wallet.model.tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
.tag(server.model.tag)
|
||||
}
|
||||
}
|
||||
|
||||
if settings.translation_service == .libretranslate {
|
||||
Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $settings.libretranslate_server) {
|
||||
ForEach(LibreTranslateServer.allCases, id: \.self) { server in
|
||||
Text(server.model.displayName)
|
||||
.tag(server.model.tag)
|
||||
}
|
||||
}
|
||||
|
||||
if settings.libretranslate_server == .custom {
|
||||
TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_url)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(UITextAutocapitalizationType.none)
|
||||
}
|
||||
|
||||
SecureField(NSLocalizedString("API Key (optional)", comment: "Prompt for optional entry of API Key to use translation server."), text: $settings.libretranslate_api_key)
|
||||
.disableAutocorrection(true)
|
||||
.disabled(settings.translation_service != .libretranslate)
|
||||
.autocapitalization(UITextAutocapitalizationType.none)
|
||||
}
|
||||
|
||||
if settings.translation_service == .deepl {
|
||||
Picker(NSLocalizedString("Plan", comment: "Prompt selection of DeepL subscription plan to perform machine translations on notes"), selection: $settings.deepl_plan) {
|
||||
ForEach(DeepLPlan.allCases, id: \.self) { server in
|
||||
Text(server.model.displayName)
|
||||
.tag(server.model.tag)
|
||||
}
|
||||
}
|
||||
|
||||
SecureField(NSLocalizedString("API Key (required)", comment: "Prompt for required entry of API Key to use translation server."), text: $settings.deepl_api_key)
|
||||
.disableAutocorrection(true)
|
||||
.disabled(settings.translation_service != .deepl)
|
||||
.autocapitalization(UITextAutocapitalizationType.none)
|
||||
|
||||
if settings.deepl_api_key == "" {
|
||||
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")!)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
Section {
|
||||
NavigationLink(destination: KeySettingsView(keypair: state.keypair)) {
|
||||
IconLabel(NSLocalizedString("Keys", comment: "Settings section for managing keys"), img_name: "key.fill", color: .purple)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
NavigationLink(destination: AppearanceSettingsView(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("Appearance", comment: "Section header for text and appearance settings"), img_name: "textformat", color: .red)
|
||||
}
|
||||
|
||||
NavigationLink(destination: NotificationSettingsView(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("Notifications", comment: "Section header for Damus notifications"), img_name: "bell.fill", color: .blue)
|
||||
}
|
||||
|
||||
NavigationLink(destination: ZapSettingsView(pubkey: state.pubkey, settings: settings)) {
|
||||
IconLabel(NSLocalizedString("Zaps", comment: "Section header for zap settings"), img_name: "bolt.fill", color: .orange)
|
||||
}
|
||||
|
||||
NavigationLink(destination: TranslationSettingsView(settings: settings)) {
|
||||
IconLabel(NSLocalizedString("Translation", comment: "Section header for text and appearance settings"), img_name: "globe.americas.fill", color: .green)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Section(NSLocalizedString("Sign Out", comment: "Section title for signing out")) {
|
||||
Button(action: {
|
||||
if state.keypair.privkey == nil {
|
||||
@@ -293,7 +113,7 @@ struct ConfigView: View {
|
||||
}
|
||||
|
||||
let ev = created_deleted_account_profile(keypair: full_kp)
|
||||
state.pool.send(.event(ev))
|
||||
state.postbox.send(ev)
|
||||
notify(.logout, ())
|
||||
}
|
||||
}
|
||||
@@ -312,80 +132,6 @@ struct ConfigView: View {
|
||||
}
|
||||
}
|
||||
|
||||
var libretranslate_view: some View {
|
||||
VStack {
|
||||
Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $settings.libretranslate_server) {
|
||||
ForEach(LibreTranslateServer.allCases, id: \.self) { server in
|
||||
Text(server.model.displayName)
|
||||
.tag(server.model.tag)
|
||||
}
|
||||
}
|
||||
|
||||
TextField(NSLocalizedString("URL", comment: "Example URL to LibreTranslate server"), text: $settings.libretranslate_url)
|
||||
.disableAutocorrection(true)
|
||||
.disabled(settings.libretranslate_server != .custom)
|
||||
.autocapitalization(UITextAutocapitalizationType.none)
|
||||
HStack {
|
||||
let libretranslate_api_key_placeholder = NSLocalizedString("API Key (optional)", comment: "Prompt for optional entry of API Key to use translation server.")
|
||||
if show_api_key {
|
||||
TextField(libretranslate_api_key_placeholder, text: $settings.libretranslate_api_key)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(UITextAutocapitalizationType.none)
|
||||
if settings.libretranslate_api_key != "" {
|
||||
Button(NSLocalizedString("Hide API Key", comment: "Button to hide the LibreTranslate server API key.")) {
|
||||
show_api_key = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SecureField(libretranslate_api_key_placeholder, text: $settings.libretranslate_api_key)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(UITextAutocapitalizationType.none)
|
||||
if settings.libretranslate_api_key != "" {
|
||||
Button(NSLocalizedString("Show API Key", comment: "Button to show the LibreTranslate server API key.")) {
|
||||
show_api_key = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var deepl_view: some View {
|
||||
VStack {
|
||||
Picker(NSLocalizedString("Plan", comment: "Prompt selection of DeepL subscription plan to perform machine translations on notes"), selection: $settings.deepl_plan) {
|
||||
ForEach(DeepLPlan.allCases, id: \.self) { server in
|
||||
Text(server.model.displayName)
|
||||
.tag(server.model.tag)
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
let deepl_api_key_placeholder = NSLocalizedString("API Key (required)", comment: "Prompt for required entry of API Key to use translation server.")
|
||||
if show_api_key {
|
||||
TextField(deepl_api_key_placeholder, text: $settings.deepl_api_key)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(UITextAutocapitalizationType.none)
|
||||
if settings.deepl_api_key != "" {
|
||||
Button(NSLocalizedString("Hide API Key", comment: "Button to hide the DeepL translation API key.")) {
|
||||
show_api_key = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SecureField(deepl_api_key_placeholder, text: $settings.deepl_api_key)
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(UITextAutocapitalizationType.none)
|
||||
if settings.deepl_api_key != "" {
|
||||
Button(NSLocalizedString("Show API Key", comment: "Button to show the DeepL translation API key.")) {
|
||||
show_api_key = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if settings.deepl_api_key == "" {
|
||||
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")!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ConfigView_Previews: PreviewProvider {
|
||||
@@ -398,14 +144,15 @@ struct ConfigView_Previews: PreviewProvider {
|
||||
|
||||
|
||||
func handle_string_amount(new_value: String) -> Int? {
|
||||
let digits = Set("0123456789")
|
||||
let filtered = new_value.filter { digits.contains($0) }
|
||||
let filtered = new_value.filter {
|
||||
$0.isNumber
|
||||
}
|
||||
|
||||
if filtered == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let amt = Int(filtered) else {
|
||||
guard let amt = NumberFormatter().number(from: filtered) as? Int else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,12 @@ import SwiftUI
|
||||
|
||||
struct CreateAccountView: View {
|
||||
@StateObject var account: CreateAccountModel = CreateAccountModel()
|
||||
@StateObject var profileUploadViewModel = ProfileUploadingViewModel()
|
||||
|
||||
@State var is_light: Bool = false
|
||||
@State var is_done: Bool = false
|
||||
@State var reading_eula: Bool = false
|
||||
@State var profile_image: URL? = nil
|
||||
|
||||
func SignupForm<FormContent: View>(@ViewBuilder content: () -> FormContent) -> some View {
|
||||
return VStack(alignment: .leading, spacing: 10.0, content: content)
|
||||
@@ -32,7 +35,7 @@ struct CreateAccountView: View {
|
||||
.font(.title.bold())
|
||||
.foregroundColor(.white)
|
||||
|
||||
ProfilePictureSelector(pubkey: account.pubkey)
|
||||
ProfilePictureSelector(pubkey: account.pubkey, viewModel: profileUploadViewModel, callback: uploadedProfilePicture(image_url:))
|
||||
|
||||
HStack(alignment: .top) {
|
||||
VStack {
|
||||
@@ -81,6 +84,8 @@ struct CreateAccountView: View {
|
||||
self.is_done = true
|
||||
}
|
||||
.padding()
|
||||
.disabled(profileUploadViewModel.isLoading)
|
||||
.opacity(profileUploadViewModel.isLoading ? 0.5 : 1)
|
||||
}
|
||||
.padding(.leading, 14.0)
|
||||
.padding(.trailing, 20.0)
|
||||
@@ -91,6 +96,10 @@ struct CreateAccountView: View {
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.navigationBarItems(leading: BackNav())
|
||||
}
|
||||
|
||||
func uploadedProfilePicture(image_url: URL?) {
|
||||
account.profile_image = image_url?.absoluteString
|
||||
}
|
||||
}
|
||||
|
||||
struct BackNav: View {
|
||||
|
||||
@@ -9,17 +9,20 @@ import SwiftUI
|
||||
|
||||
struct DMChatView: View {
|
||||
let damus_state: DamusState
|
||||
let pubkey: String
|
||||
@EnvironmentObject var dms: DirectMessageModel
|
||||
@ObservedObject var dms: DirectMessageModel
|
||||
@State var showPrivateKeyWarning: Bool = false
|
||||
|
||||
|
||||
var pubkey: String {
|
||||
dms.pubkey
|
||||
}
|
||||
|
||||
var Messages: some View {
|
||||
ScrollViewReader { scroller in
|
||||
ScrollView {
|
||||
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)
|
||||
.contextMenu{MenuItems(event: ev, keypair: damus_state.keypair, target_pubkey: ev.pubkey, bookmarks: damus_state.bookmarks)}
|
||||
.contextMenu{MenuItems(event: ev, keypair: damus_state.keypair, target_pubkey: ev.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads)}
|
||||
}
|
||||
EndBlock(height: 80)
|
||||
}
|
||||
@@ -120,7 +123,7 @@ struct DMChatView: View {
|
||||
func send_message() {
|
||||
let tags = [["p", pubkey]]
|
||||
let post_blocks = parse_post_blocks(content: dms.draft)
|
||||
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags)
|
||||
let post_tags = make_post_tags(post_blocks: post_blocks, tags: tags, silent_mentions: true)
|
||||
let content = render_blocks(blocks: post_tags.blocks)
|
||||
|
||||
guard let dm = create_dm(content, to_pk: pubkey, tags: post_tags.tags, keypair: damus_state.keypair) else {
|
||||
@@ -130,7 +133,10 @@ struct DMChatView: View {
|
||||
|
||||
dms.draft = ""
|
||||
|
||||
damus_state.pool.send(.event(dm))
|
||||
damus_state.postbox.send(dm)
|
||||
|
||||
handle_incoming_dm(ev: dm, our_pubkey: damus_state.pubkey, dms: damus_state.dms, prev_events: NewEventsBits())
|
||||
|
||||
end_editing()
|
||||
}
|
||||
|
||||
@@ -174,10 +180,9 @@ struct DMChatView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let ev = NostrEvent(content: "hi", pubkey: "pubkey", kind: 1, tags: [])
|
||||
|
||||
let model = DirectMessageModel(events: [ev], our_pubkey: "pubkey")
|
||||
let model = DirectMessageModel(events: [ev], our_pubkey: "pubkey", pubkey: "the_pk")
|
||||
|
||||
DMChatView(damus_state: test_damus_state(), pubkey: "pubkey")
|
||||
.environmentObject(model)
|
||||
DMChatView(damus_state: test_damus_state(), dms: model)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,26 @@ struct DMView: View {
|
||||
var is_ours: Bool {
|
||||
event.pubkey == damus_state.pubkey
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
var Mention: some View {
|
||||
Group {
|
||||
if let mention = first_eref_mention(ev: event, privkey: damus_state.keypair.privkey) {
|
||||
BuilderEventView(damus: damus_state, event_id: mention.ref.id)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var dm_options: EventViewOptions {
|
||||
if self.damus_state.settings.translate_dms {
|
||||
return []
|
||||
}
|
||||
|
||||
return [.no_translate]
|
||||
}
|
||||
|
||||
var DM: some View {
|
||||
HStack {
|
||||
if is_ours {
|
||||
Spacer(minLength: UIScreen.main.bounds.width * 0.2)
|
||||
@@ -23,7 +41,7 @@ struct DMView: View {
|
||||
|
||||
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)), options: [])
|
||||
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: dm_options)
|
||||
.padding([.top, .leading, .trailing], 10)
|
||||
.padding([.bottom], 25)
|
||||
.background(VisualEffectView(effect: UIBlurEffect(style: .prominent))
|
||||
@@ -36,11 +54,20 @@ struct DMView: View {
|
||||
.foregroundColor(.gray)
|
||||
.opacity(0.8)
|
||||
.offset(x: -10, y: -5), alignment: .bottomTrailing)
|
||||
|
||||
if !is_ours {
|
||||
Spacer(minLength: UIScreen.main.bounds.width * 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Mention
|
||||
DM
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct DMView_Previews: PreviewProvider {
|
||||
|
||||
@@ -16,21 +16,12 @@ struct DirectMessagesView: View {
|
||||
let damus_state: DamusState
|
||||
|
||||
@State var dm_type: DMType = .friend
|
||||
@State var open_dm: Bool = false
|
||||
@State var pubkey: String = ""
|
||||
@EnvironmentObject var model: DirectMessagesModel
|
||||
@State var active_model: DirectMessageModel
|
||||
|
||||
init(damus_state: DamusState) {
|
||||
self.damus_state = damus_state
|
||||
self._active_model = State(initialValue: DirectMessageModel(our_pubkey: damus_state.pubkey))
|
||||
}
|
||||
@ObservedObject var model: DirectMessagesModel
|
||||
|
||||
func MainContent(requests: Bool) -> some View {
|
||||
ScrollView {
|
||||
let chat = DMChatView(damus_state: damus_state, pubkey: pubkey)
|
||||
.environmentObject(active_model)
|
||||
NavigationLink(destination: chat, isActive: $open_dm) {
|
||||
let chat = DMChatView(damus_state: damus_state, dms: model.active_model)
|
||||
NavigationLink(destination: chat, isActive: $model.open_dm) {
|
||||
EmptyView()
|
||||
}
|
||||
LazyVStack(spacing: 0) {
|
||||
@@ -38,7 +29,7 @@ struct DirectMessagesView: View {
|
||||
EmptyTimelineView()
|
||||
} else {
|
||||
let dms = requests ? model.message_requests : model.friend_dms
|
||||
ForEach(dms, id: \.0) { tup in
|
||||
ForEach(dms, id: \.pubkey) { tup in
|
||||
MaybeEvent(tup)
|
||||
.padding(.top, 10)
|
||||
|
||||
@@ -51,14 +42,20 @@ struct DirectMessagesView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func MaybeEvent(_ tup: (String, DirectMessageModel)) -> some View {
|
||||
var options: EventViewOptions {
|
||||
if self.damus_state.settings.translate_dms {
|
||||
return [.truncate_content, .no_action_bar]
|
||||
}
|
||||
|
||||
return [.truncate_content, .no_action_bar, .no_translate]
|
||||
}
|
||||
|
||||
func MaybeEvent(_ model: DirectMessageModel) -> some View {
|
||||
Group {
|
||||
if let ev = tup.1.events.last {
|
||||
EventView(damus: damus_state, event: ev, pubkey: tup.0)
|
||||
if let ev = model.events.last {
|
||||
EventView(damus: damus_state, event: ev, pubkey: model.pubkey, options: options)
|
||||
.onTapGesture {
|
||||
pubkey = tup.0
|
||||
active_model = tup.1
|
||||
open_dm = true
|
||||
self.model.open_dm_by_model(model)
|
||||
}
|
||||
} else {
|
||||
EmptyView()
|
||||
@@ -98,8 +95,6 @@ struct DirectMessagesView_Previews: PreviewProvider {
|
||||
kind: 4,
|
||||
tags: [])
|
||||
let ds = test_damus_state()
|
||||
let model = DirectMessageModel(events: [ev], our_pubkey: ds.pubkey)
|
||||
DirectMessagesView(damus_state: ds)
|
||||
.environmentObject(model)
|
||||
DirectMessagesView(damus_state: ds, model: ds.dms)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
let PPM_SIZE: CGFloat = 80.0
|
||||
let BANNER_HEIGHT: CGFloat = 150.0;
|
||||
@@ -67,6 +68,7 @@ struct EditMetadataView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
@State var confirm_ln_address: Bool = false
|
||||
@StateObject var profileUploadViewModel = ProfileUploadingViewModel()
|
||||
|
||||
init (damus_state: DamusState) {
|
||||
self.damus_state = damus_state
|
||||
@@ -102,7 +104,7 @@ struct EditMetadataView: View {
|
||||
let m_metadata_ev = make_metadata_event(keypair: damus_state.keypair, metadata: metadata)
|
||||
|
||||
if let metadata_ev = m_metadata_ev {
|
||||
damus_state.pool.send(.event(metadata_ev))
|
||||
damus_state.postbox.send(metadata_ev)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +128,7 @@ struct EditMetadataView: View {
|
||||
let pfp_size: CGFloat = 90.0
|
||||
|
||||
HStack(alignment: .center) {
|
||||
ProfilePicView(pubkey: damus_state.pubkey, size: pfp_size, highlight: .custom(imageBorderColor(), 4.0), profiles: damus_state.profiles)
|
||||
ProfilePictureSelector(pubkey: damus_state.pubkey, damus_state: damus_state, viewModel: profileUploadViewModel, callback: uploadedProfilePicture(image_url:))
|
||||
.offset(y: -(pfp_size/2.0)) // Increase if set a frame
|
||||
|
||||
Spacer()
|
||||
@@ -196,13 +198,18 @@ struct EditMetadataView: View {
|
||||
TextField(NSLocalizedString("jb55@jb55.com", comment: "Placeholder example text for identifier used for NIP-05 verification."), text: $nip05)
|
||||
.autocorrectionDisabled(true)
|
||||
.textInputAutocapitalization(.never)
|
||||
.onReceive(Just(nip05)) { newValue in
|
||||
self.nip05 = newValue.trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
}, header: {
|
||||
Text("NIP-05 Verification", comment: "Label for NIP-05 Verification section of user profile form.")
|
||||
}, footer: {
|
||||
if let parts = nip05_parts {
|
||||
Text("'\(parts.username)' at '\(parts.host)' will be used for verification", comment: "Description of how the nip05 identifier would be used for verification.")
|
||||
} else {
|
||||
} else if !nip05.isEmpty {
|
||||
Text("'\(nip05)' is an invalid NIP-05 identifier. It should look like an email.", comment: "Description of why the nip05 identifier is invalid.")
|
||||
} else {
|
||||
Text("") // without this, the keyboard dismisses unnecessarily when the footer changes state
|
||||
}
|
||||
})
|
||||
|
||||
@@ -214,6 +221,7 @@ struct EditMetadataView: View {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
.disabled(profileUploadViewModel.isLoading)
|
||||
.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.")) {
|
||||
}
|
||||
@@ -223,6 +231,11 @@ struct EditMetadataView: View {
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea(edges: .top)
|
||||
.background(Color(.systemGroupedBackground))
|
||||
}
|
||||
|
||||
func uploadedProfilePicture(image_url: URL?) {
|
||||
picture = image_url?.absoluteString ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
37
damus/Views/Empty Views/EmptyUserSearchView.swift
Normal file
37
damus/Views/Empty Views/EmptyUserSearchView.swift
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// EmptyUserSearchView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 4/3/23.
|
||||
//
|
||||
|
||||
//
|
||||
// EmptyUserSearchView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 4/3/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EmptyUserSearchView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Image(systemName: "person.fill.questionmark")
|
||||
.font(.system(size: 35))
|
||||
.padding()
|
||||
Text("Could not find the user you're looking for", comment: "Indicates that there are no users found.")
|
||||
.multilineTextAlignment(.center)
|
||||
.font(.callout.weight(.medium))
|
||||
}
|
||||
.foregroundColor(.gray)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct EmptyUserSearchView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
EmptyUserSearchView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,19 +14,6 @@ enum EventViewKind {
|
||||
case selected
|
||||
}
|
||||
|
||||
func eventviewsize_to_font(_ size: EventViewKind) -> Font {
|
||||
switch size {
|
||||
case .small:
|
||||
return .body
|
||||
case .normal:
|
||||
return .body
|
||||
case .selected:
|
||||
return .custom("selected", size: 21.0)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct EventView: View {
|
||||
let event: NostrEvent
|
||||
let options: EventViewOptions
|
||||
@@ -35,25 +22,11 @@ struct EventView: View {
|
||||
|
||||
@EnvironmentObject var action_bar: ActionBarModel
|
||||
|
||||
init(damus: DamusState, event: NostrEvent, options: EventViewOptions) {
|
||||
init(damus: DamusState, event: NostrEvent, pubkey: String? = nil, options: EventViewOptions = []) {
|
||||
self.event = event
|
||||
self.options = options
|
||||
self.damus = damus
|
||||
self.pubkey = event.pubkey
|
||||
}
|
||||
|
||||
init(damus: DamusState, event: NostrEvent) {
|
||||
self.event = event
|
||||
self.options = []
|
||||
self.damus = damus
|
||||
self.pubkey = event.pubkey
|
||||
}
|
||||
|
||||
init(damus: DamusState, event: NostrEvent, pubkey: String) {
|
||||
self.event = event
|
||||
self.options = [.no_action_bar]
|
||||
self.damus = damus
|
||||
self.pubkey = pubkey
|
||||
self.pubkey = pubkey ?? event.pubkey
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -96,19 +69,6 @@ func should_show_images(settings: UserSettingsStore, contacts: Contacts, ev: Nos
|
||||
return false
|
||||
}
|
||||
|
||||
func event_validity_color(_ validation: ValidationResult) -> some View {
|
||||
Group {
|
||||
switch validation {
|
||||
case .ok:
|
||||
EmptyView()
|
||||
case .bad_id:
|
||||
Color.orange.opacity(0.4)
|
||||
case .bad_sig:
|
||||
Color.red.opacity(0.4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func pubkey_context_menu(bech32_pubkey: String) -> some View {
|
||||
return self.contextMenu {
|
||||
@@ -120,9 +80,9 @@ extension View {
|
||||
}
|
||||
}
|
||||
|
||||
func event_context_menu(_ event: NostrEvent, keypair: Keypair, target_pubkey: String, bookmarks: BookmarksManager) -> some View {
|
||||
func event_context_menu(_ event: NostrEvent, keypair: Keypair, target_pubkey: String, bookmarks: BookmarksManager, muted_threads: MutedThreadsManager) -> some View {
|
||||
return self.contextMenu {
|
||||
EventMenuContext(event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks)
|
||||
EventMenuContext(event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks, muted_threads: muted_threads)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -142,22 +102,31 @@ func format_date(_ created_at: Int64) -> String {
|
||||
}
|
||||
|
||||
func make_actionbar_model(ev: String, damus: DamusState) -> ActionBarModel {
|
||||
let likes = damus.likes.counts[ev]
|
||||
let boosts = damus.boosts.counts[ev]
|
||||
let zaps = damus.zaps.event_counts[ev]
|
||||
let zap_total = damus.zaps.event_totals[ev]
|
||||
let our_like = damus.likes.our_events[ev]
|
||||
let our_boost = damus.boosts.our_events[ev]
|
||||
let our_zap = damus.zaps.our_zaps[ev]
|
||||
let model = ActionBarModel.empty()
|
||||
model.update(damus: damus, evid: ev)
|
||||
return model
|
||||
}
|
||||
|
||||
return ActionBarModel(likes: likes ?? 0,
|
||||
boosts: boosts ?? 0,
|
||||
zaps: zaps ?? 0,
|
||||
zap_total: zap_total ?? 0,
|
||||
our_like: our_like,
|
||||
our_boost: our_boost,
|
||||
our_zap: our_zap?.first
|
||||
)
|
||||
func eventviewsize_to_font(_ size: EventViewKind) -> Font {
|
||||
switch size {
|
||||
case .small:
|
||||
return .body
|
||||
case .normal:
|
||||
return .body
|
||||
case .selected:
|
||||
return .custom("selected", size: 21.0)
|
||||
}
|
||||
}
|
||||
|
||||
func eventviewsize_to_uifont(_ size: EventViewKind) -> UIFont {
|
||||
switch size {
|
||||
case .small:
|
||||
return .preferredFont(forTextStyle: .body)
|
||||
case .normal:
|
||||
return .preferredFont(forTextStyle: .body)
|
||||
case .selected:
|
||||
return .preferredFont(forTextStyle: .title2)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -48,10 +48,6 @@ struct BuilderEventView: View {
|
||||
return
|
||||
}
|
||||
|
||||
guard nostr_event.known_kind == .text else {
|
||||
return
|
||||
}
|
||||
|
||||
if event != nil {
|
||||
return
|
||||
}
|
||||
@@ -73,13 +69,13 @@ struct BuilderEventView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if let event = event {
|
||||
if let event {
|
||||
let ev = event.inner_event ?? event
|
||||
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)
|
||||
EventView(damus: damus, event: event, options: .embedded)
|
||||
.padding([.top, .bottom], 8)
|
||||
}.buttonStyle(.plain)
|
||||
} else {
|
||||
ProgressView().padding()
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
//
|
||||
// EmbeddedEventView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-01-23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EmbeddedEventView: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
|
||||
var pubkey: String {
|
||||
event.pubkey
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
let profile = damus_state.profiles.lookup(id: pubkey)
|
||||
HStack {
|
||||
EventProfile(damus_state: damus_state, pubkey: pubkey, profile: profile, size: .small)
|
||||
|
||||
Spacer()
|
||||
|
||||
EventMenuContext(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks)
|
||||
.padding([.bottom], 4)
|
||||
|
||||
}
|
||||
.minimumScaleFactor(0.75)
|
||||
.lineLimit(1)
|
||||
|
||||
EventBody(damus_state: damus_state, event: event, size: .small, options: [.truncate_content])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EmbeddedEventView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
EmbeddedEventView(damus_state: test_damus_state(), event: test_event)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ struct EventMenuContext: View {
|
||||
let keypair: Keypair
|
||||
let target_pubkey: String
|
||||
let bookmarks: BookmarksManager
|
||||
let muted_threads: MutedThreadsManager
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
@@ -19,7 +20,7 @@ struct EventMenuContext: View {
|
||||
HStack {
|
||||
Menu {
|
||||
|
||||
MenuItems(event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks)
|
||||
MenuItems(event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks, muted_threads: muted_threads)
|
||||
|
||||
} label: {
|
||||
Label("", systemImage: "ellipsis")
|
||||
@@ -36,14 +37,20 @@ struct MenuItems: View {
|
||||
let keypair: Keypair
|
||||
let target_pubkey: String
|
||||
let bookmarks: BookmarksManager
|
||||
let muted_threads: MutedThreadsManager
|
||||
|
||||
@State private var isBookmarked: Bool = false
|
||||
@State private var isMutedThread: Bool = false
|
||||
|
||||
init(event: NostrEvent, keypair: Keypair, target_pubkey: String, bookmarks: BookmarksManager) {
|
||||
init(event: NostrEvent, keypair: Keypair, target_pubkey: String, bookmarks: BookmarksManager, muted_threads: MutedThreadsManager) {
|
||||
let bookmarked = bookmarks.isBookmarked(event)
|
||||
self._isBookmarked = State(initialValue: bookmarked)
|
||||
|
||||
let muted_thread = muted_threads.isMutedThread(event, privkey: keypair.privkey)
|
||||
self._isMutedThread = State(initialValue: muted_thread)
|
||||
|
||||
self.bookmarks = bookmarks
|
||||
self.muted_threads = muted_threads
|
||||
self.event = event
|
||||
self.keypair = keypair
|
||||
self.target_pubkey = target_pubkey
|
||||
@@ -86,6 +93,19 @@ struct MenuItems: View {
|
||||
Label(isBookmarked ? removeBookmarkString : addBookmarkString, systemImage: imageName)
|
||||
}
|
||||
|
||||
if event.known_kind != .dm {
|
||||
Button {
|
||||
self.muted_threads.updateMutedThread(event)
|
||||
let muted = self.muted_threads.isMutedThread(event, privkey: self.keypair.privkey)
|
||||
isMutedThread = muted
|
||||
} label: {
|
||||
let imageName = isMutedThread ? "speaker" : "speaker.slash"
|
||||
let unmuteThreadString = NSLocalizedString("Unmute conversation", comment: "Context menu option for unmuting a conversation.")
|
||||
let muteThreadString = NSLocalizedString("Mute conversation", comment: "Context menu option for muting a conversation.")
|
||||
Label(isMutedThread ? unmuteThreadString : muteThreadString, systemImage: imageName)
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
NotificationCenter.default.post(name: .broadcast_event, object: event)
|
||||
} label: {
|
||||
@@ -102,9 +122,9 @@ struct MenuItems: View {
|
||||
}
|
||||
|
||||
Button(role: .destructive) {
|
||||
notify(.block, target_pubkey)
|
||||
notify(.mute, target_pubkey)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Block", comment: "Context menu option for blocking users."), systemImage: "exclamationmark.octagon")
|
||||
Label(NSLocalizedString("Mute User", comment: "Context menu option for muting users."), systemImage: "exclamationmark.octagon")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,15 +10,13 @@ import SwiftUI
|
||||
struct MutedEventView: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
let scroller: ScrollViewProxy?
|
||||
|
||||
let selected: Bool
|
||||
@State var shown: Bool
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent, scroller: ScrollViewProxy?, selected: Bool) {
|
||||
init(damus_state: DamusState, event: NostrEvent, selected: Bool) {
|
||||
self.damus_state = damus_state
|
||||
self.event = event
|
||||
self.scroller = scroller
|
||||
self.selected = selected
|
||||
self._shown = State(initialValue: should_show_event(contacts: damus_state.contacts, ev: event))
|
||||
}
|
||||
@@ -89,7 +87,7 @@ struct MutedEventView_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
MutedEventView(damus_state: test_damus_state(), event: test_event, scroller: nil, selected: false)
|
||||
MutedEventView(damus_state: test_damus_state(), event: test_event, selected: false)
|
||||
.frame(width: .infinity, height: 50)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,21 +35,28 @@ struct SelectedEventView: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks)
|
||||
EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks, muted_threads: damus.muted_threads)
|
||||
.padding([.bottom], 4)
|
||||
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.minimumScaleFactor(0.75)
|
||||
.lineLimit(1)
|
||||
|
||||
EventBody(damus_state: damus, event: event, size: size, options: [])
|
||||
if event_is_reply(event, privkey: damus.keypair.privkey) {
|
||||
ReplyDescription(event: event, profiles: damus.profiles)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
EventBody(damus_state: damus, event: event, size: size, options: [.pad_content])
|
||||
|
||||
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
|
||||
BuilderEventView(damus: damus, event_id: mention.ref.id)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
Text(verbatim: "\(format_date(event.created_at))")
|
||||
.padding(.top, 10)
|
||||
.padding([.top, .leading, .trailing])
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
|
||||
@@ -58,11 +65,13 @@ struct SelectedEventView: View {
|
||||
|
||||
if !bar.is_empty {
|
||||
EventDetailBar(state: damus, target: event.id, target_pk: event.pubkey)
|
||||
.padding(.horizontal)
|
||||
Divider()
|
||||
}
|
||||
|
||||
EventActionBar(damus_state: damus, event: event)
|
||||
.padding([.top], 4)
|
||||
.padding(.horizontal)
|
||||
|
||||
Divider()
|
||||
.padding([.top], 4)
|
||||
@@ -72,7 +81,6 @@ struct SelectedEventView: View {
|
||||
guard target == self.event.id else { return }
|
||||
self.bar.update(damus: self.damus, evid: target)
|
||||
}
|
||||
.padding([.leading], 2)
|
||||
.compositingGroup()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,19 @@
|
||||
import SwiftUI
|
||||
|
||||
struct EventViewOptions: OptionSet {
|
||||
let rawValue: UInt8
|
||||
let rawValue: UInt32
|
||||
|
||||
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)
|
||||
static let no_translate = EventViewOptions(rawValue: 1 << 6)
|
||||
static let small_pfp = EventViewOptions(rawValue: 1 << 7)
|
||||
static let nested = EventViewOptions(rawValue: 1 << 8)
|
||||
|
||||
static let embedded: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested]
|
||||
}
|
||||
|
||||
struct TextEvent: View {
|
||||
@@ -36,14 +42,13 @@ struct TextEvent: View {
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.background(event_validity_color(event.validity))
|
||||
.id(event.id)
|
||||
.frame(maxWidth: .infinity, minHeight: PFP_SIZE)
|
||||
.padding([.bottom], 2)
|
||||
}
|
||||
|
||||
func Pfp(is_anon: Bool) -> some View {
|
||||
MaybeAnonPfpView(state: damus, is_anon: is_anon, pubkey: pubkey)
|
||||
MaybeAnonPfpView(state: damus, is_anon: is_anon, pubkey: pubkey, size: options.contains(.small_pfp) ? eventview_pfp_size(.small) : PFP_SIZE )
|
||||
}
|
||||
|
||||
func TopPart(is_anon: Bool) -> some View {
|
||||
@@ -80,9 +85,9 @@ struct TextEvent: View {
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
EvBody(options: [.truncate_content, .pad_content])
|
||||
EvBody(options: self.options.union(.pad_content))
|
||||
|
||||
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
|
||||
if let mention = get_mention() {
|
||||
Mention(mention)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
@@ -108,7 +113,7 @@ struct TextEvent: View {
|
||||
}
|
||||
|
||||
var ContextButton: some View {
|
||||
EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks)
|
||||
EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks, muted_threads: damus.muted_threads)
|
||||
.padding([.bottom], 4)
|
||||
}
|
||||
|
||||
@@ -135,6 +140,14 @@ struct TextEvent: View {
|
||||
return Rectangle().frame(height: 2).opacity(0)
|
||||
}
|
||||
|
||||
func get_mention() -> Mention? {
|
||||
if self.options.contains(.nested) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return first_eref_mention(ev: event, privkey: damus.keypair.privkey)
|
||||
}
|
||||
|
||||
var ThreadedStyle: some View {
|
||||
HStack(alignment: .top) {
|
||||
|
||||
@@ -149,9 +162,9 @@ struct TextEvent: View {
|
||||
TopPart(is_anon: is_anon)
|
||||
|
||||
ReplyPart
|
||||
EvBody(options: [])
|
||||
EvBody(options: self.options)
|
||||
|
||||
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
|
||||
if let mention = get_mention() {
|
||||
Mention(mention)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,13 +12,20 @@ struct FollowUserView: View {
|
||||
let damus_state: DamusState
|
||||
|
||||
static let markdown = Markdown()
|
||||
@State var navigating: Bool = false
|
||||
|
||||
var body: some View {
|
||||
let dest = ProfileView(damus_state: damus_state, pubkey: target.pubkey)
|
||||
NavigationLink(destination: dest, isActive: $navigating) {
|
||||
EmptyView()
|
||||
}
|
||||
|
||||
HStack {
|
||||
UserView(damus_state: damus_state, pubkey: target.pubkey)
|
||||
UserViewRow(damus_state: damus_state, pubkey: target.pubkey)
|
||||
|
||||
FollowButtonView(target: target, follows_you: false, follow_state: damus_state.contacts.follow_state(target.pubkey))
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,10 +40,9 @@ struct FollowersView: View {
|
||||
LazyVStack(alignment: .leading) {
|
||||
ForEach(followers.contacts ?? [], id: \.self) { pk in
|
||||
FollowUserView(target: .pubkey(pk), damus_state: damus_state)
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.navigationBarTitle(NSLocalizedString("Followers", comment: "Navigation bar title for view that shows who is following a user."))
|
||||
.onAppear {
|
||||
@@ -57,7 +63,7 @@ struct FollowingView: View {
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading) {
|
||||
ForEach(following.contacts, id: \.self) { pk in
|
||||
ForEach(following.contacts.reversed(), id: \.self) { pk in
|
||||
FollowUserView(target: .pubkey(pk), damus_state: damus_state)
|
||||
}
|
||||
}
|
||||
|
||||
124
damus/Views/ImagePicker.swift
Normal file
124
damus/Views/ImagePicker.swift
Normal file
@@ -0,0 +1,124 @@
|
||||
//
|
||||
// ImagePicker.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Swift on 3/31/23.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
struct ImagePicker: UIViewControllerRepresentable {
|
||||
|
||||
@Environment(\.presentationMode)
|
||||
private var presentationMode
|
||||
|
||||
let sourceType: UIImagePickerController.SourceType
|
||||
let pubkey: String
|
||||
@Binding var image_upload_confirm: Bool
|
||||
var imagesOnly: Bool = false
|
||||
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
|
||||
@Binding var image_upload_confirm: Bool
|
||||
|
||||
init(presentationMode: Binding<PresentationMode>,
|
||||
sourceType: UIImagePickerController.SourceType,
|
||||
onImagePicked: @escaping (URL) -> Void,
|
||||
onVideoPicked: @escaping (URL) -> Void,
|
||||
image_upload_confirm: Binding<Bool>) {
|
||||
_presentationMode = presentationMode
|
||||
self.sourceType = sourceType
|
||||
self.onImagePicked = onImagePicked
|
||||
self.onVideoPicked = onVideoPicked
|
||||
self._image_upload_confirm = image_upload_confirm
|
||||
}
|
||||
|
||||
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
|
||||
onImagePicked(imageURL)
|
||||
} else if let cameraImage = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
|
||||
if let imageURL = saveImageToTemporaryFolder(image: cameraImage, imageType: "jpeg") {
|
||||
onImagePicked(imageURL)
|
||||
}
|
||||
} else if let editedImage = info[UIImagePickerController.InfoKey.editedImage] as? UIImage {
|
||||
if let editedImageURL = saveImageToTemporaryFolder(image: editedImage) {
|
||||
onImagePicked(editedImageURL)
|
||||
}
|
||||
}
|
||||
image_upload_confirm = true
|
||||
}
|
||||
|
||||
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||
presentationMode.dismiss()
|
||||
}
|
||||
|
||||
func saveImageToTemporaryFolder(image: UIImage, imageType: String = "png") -> URL? {
|
||||
// Convert UIImage to Data
|
||||
let imageData: Data?
|
||||
if imageType.lowercased() == "jpeg" {
|
||||
imageData = image.jpegData(compressionQuality: 1.0)
|
||||
} else {
|
||||
imageData = image.pngData()
|
||||
}
|
||||
|
||||
guard let data = imageData else {
|
||||
print("Failed to convert UIImage to Data.")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate a temporary URL with a unique filename
|
||||
let temporaryDirectoryURL = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
||||
let uniqueImageName = "\(UUID().uuidString).\(imageType)"
|
||||
let temporaryImageURL = temporaryDirectoryURL.appendingPathComponent(uniqueImageName)
|
||||
|
||||
// Save the image data to the temporary URL
|
||||
do {
|
||||
try data.write(to: temporaryImageURL)
|
||||
return temporaryImageURL
|
||||
} catch {
|
||||
print("Error saving image data to temporary URL: \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}, image_upload_confirm: $image_upload_confirm)
|
||||
}
|
||||
|
||||
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
|
||||
let picker = UIImagePickerController()
|
||||
picker.sourceType = sourceType
|
||||
let mediaUploader = get_media_uploader(pubkey)
|
||||
picker.mediaTypes = ["public.image", "com.compuserve.gif"]
|
||||
if mediaUploader.supportsVideo && !imagesOnly {
|
||||
picker.mediaTypes.append("public.movie")
|
||||
}
|
||||
picker.delegate = context.coordinator
|
||||
return picker
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIImagePickerController,
|
||||
context: UIViewControllerRepresentableContext<ImagePicker>) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import SwiftUI
|
||||
|
||||
struct ImageView: View {
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
Text(verbatim: /*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user