Compare commits
274 Commits
tyiu/parti
...
tyiu/filte
| Author | SHA1 | Date | |
|---|---|---|---|
|
866e93d338
|
|||
|
f75fc7eebe
|
|||
|
|
0b40cd127c | ||
|
|
754ee254e9 | ||
|
|
963cb37762 | ||
|
|
159d0fa2b5 | ||
|
|
61fddf800e | ||
|
|
b6d5b6f45e | ||
|
|
f5ed9cd5d4 | ||
|
|
57006b928b | ||
|
|
98f0b2f2d2 | ||
|
|
9a4d93824a | ||
|
|
f76563b354 | ||
|
|
f84d4516db | ||
|
|
2e34230119 | ||
|
|
cad89525b7 | ||
|
|
d2cf18aeee | ||
|
|
a8ce39fc96 | ||
|
|
ed90139b0c | ||
|
|
022045d916 | ||
|
|
4bda490010 | ||
|
|
97382adb63 | ||
|
|
c582755246 | ||
|
|
44a59e8d57 | ||
|
|
98685645d3 | ||
|
|
14f71f1a1d | ||
|
|
91cb6a6763 | ||
|
|
a65351154b | ||
|
|
2e2b33e21d | ||
|
|
c24b0afb8f | ||
|
|
a357bbe4a6 | ||
|
|
b687006b64 | ||
|
|
1f095b0896 | ||
|
|
4f7ed36a7c | ||
|
|
393809c7d7 | ||
|
|
9091cb1aae | ||
|
|
e78a82e5b7 | ||
|
|
7b0ef5f4a7 | ||
|
|
66a5df68b3 | ||
|
|
fa2344b9ba | ||
|
|
68c018cf44 | ||
|
f367df2225
|
|||
|
|
e0984aab34 | ||
|
|
eabbb12195 | ||
|
|
7b1f4b7701 | ||
|
|
7b6d3ef9df | ||
|
|
bc58686016 | ||
|
|
a574dcb27c | ||
|
|
761982e359 | ||
|
|
57d48a0395 | ||
|
|
4f96c88b9b | ||
|
|
da11bc575a | ||
|
|
cc9532d958 | ||
|
|
35f4e7c78d | ||
|
|
d8c822858a | ||
|
|
ca0c837231 | ||
|
|
38fc5afa44 | ||
| 9b76afae4f | |||
|
|
f911f1646d | ||
|
|
20fd061293 | ||
|
|
3f5262cd5d | ||
|
|
982d15ab4a | ||
|
|
074b6efc0f | ||
|
|
ad0ca6ca1a | ||
|
|
e140cacfdf | ||
|
|
b825aa80d8 | ||
|
|
9ee91553c1 | ||
|
|
7ce862f552 | ||
|
|
231f9d1853 | ||
|
|
63acf11065 | ||
|
|
0502f06ef8 | ||
|
|
d921a40f24 | ||
| 2e82b349b7 | |||
|
|
b0007af030 | ||
|
|
dd5c2d7301 | ||
|
|
27c0fbf453 | ||
|
|
d1ad4dc9ff | ||
|
|
4c58c4ffef | ||
|
|
cb3603fb35 | ||
|
|
6df5288294 | ||
|
|
9e02dac5d0 | ||
|
|
b7d9db5cec | ||
|
|
e46792e596 | ||
|
|
fc65da3473 | ||
|
|
4f15469320 | ||
|
|
3ea3595902 | ||
|
|
3caebd9c63 | ||
|
|
4d4f340ab0 | ||
|
|
6a549e5019 | ||
|
|
52bf47a494 | ||
|
|
aee243d3e0 | ||
|
|
18745403ce | ||
|
|
07a20040a4 | ||
|
|
ef3ef03b7f | ||
|
|
71e3ee4867 | ||
|
|
252a77fd97 | ||
|
|
a611a5d252 | ||
|
|
1533be77d8 | ||
|
|
c05223ca2b | ||
|
|
5d441d3192 | ||
|
|
04bce34297 | ||
|
|
af8ce3d32d | ||
|
|
cabe584938 | ||
|
|
dd511c3061 | ||
|
|
18449c8c0d | ||
|
|
044631b324 | ||
|
|
318b254b5d | ||
|
|
487419d098 | ||
|
|
ba82f19a11 | ||
|
|
cba6b3aef7 | ||
|
|
6872382bb7 | ||
|
|
42ea150d45 | ||
|
|
85f86ee31f | ||
|
|
96decd2392 | ||
|
|
73f7b69654 | ||
|
|
d982bb886e | ||
|
|
9766653969 | ||
|
|
5d91e7e595 | ||
|
|
ae00c103ad | ||
|
|
88aa713729 | ||
|
|
be1c03ad0e | ||
|
|
b2b62828e3 | ||
|
|
d1a77891c7 | ||
|
|
20505236ae | ||
|
|
094ac34135 | ||
|
|
6b6743fcbb | ||
|
|
8059408d5f | ||
|
|
04fa4edad8 | ||
|
|
6fffe250c2 | ||
|
|
1e7d9a6373 | ||
|
|
21989719fc | ||
|
|
d5e4866c55 | ||
|
|
f305df3471 | ||
|
|
21320367b1 | ||
|
|
82723faf33 | ||
|
|
48434f83ae | ||
|
|
083d0fa0e5 | ||
| d5a646f9ce | |||
|
38a1ad7611
|
|||
|
|
9bc3860f00 | ||
|
|
35f5ac04b4 | ||
|
|
75b73718d1 | ||
|
|
29cacebe58 | ||
|
|
84ae914bcc | ||
|
|
ef5f3ae649 | ||
|
|
f8068a42e5 | ||
|
|
bdde33bb51 | ||
|
|
e3b602df13 | ||
|
|
38b17f1acd | ||
|
|
575b91554c | ||
|
|
f36bc84618 | ||
|
|
d54c9b7d12 | ||
|
2c6647c95a
|
|||
|
|
3c2f281c6d | ||
|
|
4ba63b0dbd | ||
|
|
e2df7d5df6 | ||
|
|
0dfea0680f | ||
|
|
6cc34632fd | ||
|
|
dffb60a601 | ||
|
|
df076b03fd | ||
|
|
fc83cd4db7 | ||
|
|
e01761ce72 | ||
|
|
efc50f5b18 | ||
|
|
10c9e8ddbc | ||
|
|
f88718d56e | ||
|
|
b6a7f52596 | ||
|
|
cff98161ee | ||
|
|
8a70240968 | ||
|
|
a4855775ef | ||
|
|
06c2741bf4 | ||
|
|
721bb9abf5 | ||
|
|
89bb293acd | ||
|
|
f9c330aebf | ||
|
|
ffbfcd36f5 | ||
|
|
52f568f9b3 | ||
|
|
1c2a7db328 | ||
|
|
3110abc65b | ||
|
|
a9f62960ec | ||
|
|
150bbb1eb2 | ||
|
|
0aff41d384 | ||
|
|
3fec9dd209 | ||
|
|
a560d50366 | ||
|
|
174f7f6cc5 | ||
| a325a3c064 | |||
|
|
d0a6c2e2e4 | ||
|
|
b58baca227 | ||
|
|
5423704980 | ||
|
|
241ed1041d | ||
|
|
5134004ff7 | ||
|
|
071a4209ea | ||
|
|
7f385b2e7e | ||
|
|
502c4daf6f | ||
|
|
ffe2c7284a | ||
|
|
6b1f57d6d0 | ||
|
|
77f5268336 | ||
| c72c0079cc | |||
|
|
5ab1d6294c | ||
|
|
2f90f2d4b7 | ||
|
|
7c2e8a6cc5 | ||
|
|
1288732e5d | ||
|
|
4a6c6a65ab | ||
|
|
0f29d67e1f | ||
|
|
9fd2f51971 | ||
|
|
386bae64ca | ||
|
|
4b5c217213 | ||
|
240fda2429
|
|||
|
bacd9b3c38
|
|||
|
0152286859
|
|||
|
|
06e9a1b392
|
||
|
|
483730af18
|
||
|
|
23229015a6
|
||
|
|
7ab95583df
|
||
|
|
b7a48a24e9
|
||
|
04028d9cff
|
|||
|
6918fb46cf
|
|||
|
|
2b854ef9b7
|
||
|
|
5eb61f1ac1
|
||
|
|
c3bbf7aa8f
|
||
|
|
1e52d958ac
|
||
|
|
5252e5f5bb
|
||
|
|
71d5625f04
|
||
|
|
990e783c30
|
||
|
|
3602189133
|
||
|
|
3ca9acdf34
|
||
|
|
2036d5843b
|
||
|
|
2d3bd11d56
|
||
|
|
a715987e71
|
||
|
|
0303031445
|
||
|
|
d6ae9a5d79
|
||
|
|
356bd06e6a
|
||
|
|
e757bdca90
|
||
|
|
ff1b4d724d
|
||
|
|
b8614f055c
|
||
|
|
63ab151a5e
|
||
|
|
fd9d4deb44
|
||
|
|
a5c719673e
|
||
|
|
2bf3c6718d
|
||
|
|
3f3e59488a
|
||
|
|
b1b4b5b6c9
|
||
|
|
7455665672
|
||
|
|
9f22234926
|
||
|
|
21a8a4e96f
|
||
|
|
c635f3d77a
|
||
|
|
2e9a4388b9
|
||
|
|
4a1949eeb8
|
||
|
|
b4fcb58bcb
|
||
|
|
7d852eb33b
|
||
|
|
a448f610c0
|
||
|
|
8cc561b8c6
|
||
|
|
01630d0a4c
|
||
|
16156f4d9a
|
|||
|
f840fe9c80
|
|||
|
77bcd1b715
|
|||
|
|
75fd8de456 | ||
|
|
71f7ea47df | ||
|
|
64b1a57918 | ||
|
|
6c63f8f22a | ||
|
|
673358408a | ||
|
|
e4dd585754 | ||
|
|
436d20dfbd | ||
|
|
810b3e1fa5 | ||
|
|
75fb0d19e2 | ||
|
|
a2749eaaaa | ||
|
|
83c9289345 | ||
|
|
4c3a83772e | ||
| 5cd4c2d75e | |||
|
|
85e797a054 | ||
|
|
9f52e2c246 | ||
|
|
0210ae5d61 | ||
|
|
e5749c8748 | ||
|
|
8b9958a4ad | ||
|
|
87a0bdac94 | ||
|
|
37b964c296 | ||
|
|
b1a2b47116 |
197
CHANGELOG.md
197
CHANGELOG.md
@@ -1,3 +1,199 @@
|
||||
## [1.3.0-7] - 2023-03-24
|
||||
|
||||
- New experimental timeline view
|
||||
|
||||
[1.3.0-7]: https://github.com/damus-io/damus/releases/tag/v1.3.0-7
|
||||
|
||||
## [1.3.0-6] - 2023-03-21
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix bug where nostr: links and QRs stopped working (William Casarin)
|
||||
|
||||
|
||||
[1.3.0-6]: https://github.com/damus-io/damus/releases/tag/v1.3.0-6
|
||||
|
||||
## [1.3.0-5] - 2023-03-20
|
||||
|
||||
### Added
|
||||
|
||||
- Add Time Ago to DM View (Joel Klabo)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed internal links opening in other nostr clients (William Casarin)
|
||||
- Remove authentication for copying npub (Swift)
|
||||
|
||||
|
||||
[1.3.0-5]: https://github.com/damus-io/damus/releases/tag/v1.3.0-5
|
||||
|
||||
## [1.3.0-4] - 2023-03-17
|
||||
|
||||
### Changed
|
||||
|
||||
- It's much easier to tag users in replies and posts (William Casarin)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix bug where small black text appears during image upload (William Casarin)
|
||||
|
||||
|
||||
[1.3.0-4]: https://github.com/damus-io/damus/releases/tag/v1.3.0-4
|
||||
|
||||
## [1.3.0-3] - 2023-03-17
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix image upload url delay after progress bar disappears (William Casarin)
|
||||
- Fix issue where damus stops trying to reconnect (William Casarin)
|
||||
|
||||
[1.3.0-3]: https://github.com/damus-io/damus/releases/tag/v1.3.0-3
|
||||
|
||||
## [1.3.0-2] - 2023-03-16
|
||||
|
||||
### Added
|
||||
|
||||
- Add image uploader (Swift)
|
||||
- Add option to always show images (never blur) (William Casarin)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Fixed embedded note popping (William Casarin)
|
||||
- Bump notification limit from 100 to 500 (William Casarin)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix zap button preventing scrolling (William Casarin)
|
||||
|
||||
|
||||
[1.3.0-2]: https://github.com/damus-io/damus/releases/tag/v1.3.0-2
|
||||
|
||||
## [1.3.0] - 2023-03-15
|
||||
|
||||
### Added
|
||||
|
||||
- Extend user tagging search to all local profiles (William Casarin)
|
||||
- Vibrate when a zap is received (Swift)
|
||||
- New and Improved Share sheet (ericholguin)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Reduce battery usage by using exp backoff on connections (Bryan Montz)
|
||||
- Don't show both realname and username if they are the same (William Casarin)
|
||||
- Show error on invalid lightning tip address (Swift)
|
||||
- Make DM Content More Visible (Joel Klabo)
|
||||
- Remove spaces from hashtag searches (gladiusKatana)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Show @ mentions for users with display_names and no username (William Casarin)
|
||||
- Make user search case insensitive (William Casarin)
|
||||
- Fix repost button sometimes not working (OlegAba)
|
||||
- Don't show follows you for your own profile (benthecarman)
|
||||
- Fix json appearing in profile searches (gladiusKatana)
|
||||
- Fix unexpected font size when posting (Bryan Montz)
|
||||
- Fix keyboard sticking issues (OlegAba)
|
||||
- Fixed tab bar background color on macOS (Joel Klabo)
|
||||
- Fix some links getting interpreted as images (gladiusKatana)
|
||||
|
||||
|
||||
[1.3.0]: https://github.com/damus-io/damus/releases/tag/v1.3.0
|
||||
|
||||
## [1.2.0-4] - 2023-03-05
|
||||
|
||||
### Added
|
||||
|
||||
- Add ellipsis button to notes (ericholguin)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Immediately search for events and profiles (William Casarin)
|
||||
- Use long-press for custom zaps (William Casarin)
|
||||
- Make shaka animation smoother (Swift)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed hit detection bugs on profile page (OlegAba)
|
||||
- Fix disappearing text on Thread view (Bryan Montz)
|
||||
- Render links in notification summaries (Joel Klabo)
|
||||
- Don't show notifications from ourselves (William Casarin)
|
||||
- Fix issue where navbar back button would show the wrong text (Jack Chakany)
|
||||
- Fix case sensitivity when searching hashtags (randymcmillan)
|
||||
- Fix issue where opening reposts shows json (William Casarin)
|
||||
|
||||
|
||||
[1.2.0-4]: https://github.com/damus-io/damus/releases/tag/v1.2.0-4
|
||||
|
||||
## [1.2.0-3] - 2023-03-04
|
||||
|
||||
### Added
|
||||
|
||||
- Add additional info to recommended relay view (ericholguin)
|
||||
- Add shaka animation (Swift)
|
||||
- Add option to disable image animation (OlegAba)
|
||||
- Add additional warning when deleting account (ericholguin)
|
||||
- Threads now load instantly and are cached (William Casarin)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Wrap long profile display names (OlegAba)
|
||||
- Fixed weird scaling on profile pictures (OlegAba)
|
||||
- Fixed width of copy pubkey on profile page (Joel Klabo)
|
||||
- Make damus purple use more consistent in mentions (Joel Klabo)
|
||||
|
||||
|
||||
|
||||
[1.2.0-3]: https://github.com/damus-io/damus/releases/tag/v1.2.0-3
|
||||
|
||||
## [1.1.0-10] - 2023-03-01
|
||||
|
||||
### Added
|
||||
|
||||
- Truncate large posts and add a show more button (OlegAba)
|
||||
- Private Zaps (William Casarin)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix default zap amount setting not getting updated (William Casarin)
|
||||
- Fix issue where keyboard covers custom zap comment (William Casarin)
|
||||
|
||||
|
||||
[1.1.0-10]: https://github.com/damus-io/damus/releases/tag/v1.1.0-10
|
||||
|
||||
## [1.1.0-9] - 2023-02-26
|
||||
|
||||
### Added
|
||||
|
||||
- Customized zaps (William Casarin)
|
||||
- Add new Notifications View (William Casarin)
|
||||
- Bookmarking (Joel Klabo)
|
||||
|
||||
### Changed
|
||||
|
||||
- No more inline npubs when tagging users (Swift)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix alignment of side menu labels (Joel Klabo)
|
||||
- Fix duplicated participants in reply-to view (Joel Klabo)
|
||||
- Load missing profiles in Zaps view (William Casarin)
|
||||
- Fix memory leak with inline videos (William Casarin)
|
||||
- Eliminate popping when scrolling (William Casarin)
|
||||
|
||||
|
||||
[1.1.0-9]: https://github.com/damus-io/damus/releases/tag/v1.1.0-9
|
||||
|
||||
## [1.1.0-3] - 2023-02-20
|
||||
|
||||
### Added
|
||||
@@ -610,4 +806,3 @@
|
||||
|
||||
[0.1.2]: https://github.com/damus-io/damus/releases/tag/v0.1.2
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,11 @@
|
||||
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */; };
|
||||
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.swift */; };
|
||||
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; };
|
||||
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */; };
|
||||
3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */; };
|
||||
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */; };
|
||||
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; };
|
||||
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */; };
|
||||
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; };
|
||||
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
|
||||
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
|
||||
@@ -43,6 +48,11 @@
|
||||
4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8B28398BC6008A31F1 /* Keys.swift */; };
|
||||
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */; };
|
||||
4C2CDDF7299D4A5E00879FD5 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */; };
|
||||
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */; };
|
||||
4C30AC7429A5680900E2BD5A /* EventGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7329A5680900E2BD5A /* EventGroupView.swift */; };
|
||||
4C30AC7629A5770900E2BD5A /* NotificationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */; };
|
||||
4C30AC7829A577AB00E2BD5A /* EventCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7729A577AB00E2BD5A /* EventCache.swift */; };
|
||||
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C30AC7F29A6A53F00E2BD5A /* ProfilePicturesView.swift */; };
|
||||
4C363A8428233689006E126D /* Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8328233689006E126D /* Parser.swift */; };
|
||||
4C363A8828236948006E126D /* BlocksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8728236948006E126D /* BlocksView.swift */; };
|
||||
4C363A8A28236B57006E126D /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363A8928236B57006E126D /* MentionView.swift */; };
|
||||
@@ -93,6 +103,9 @@
|
||||
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3EA67E28FFC01D00C48A62 /* InvoiceView.swift */; };
|
||||
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C42812B298C848200DBF26F /* TranslateView.swift */; };
|
||||
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C477C9D282C3A4800033AA3 /* TipCounter.swift */; };
|
||||
4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0629A540BA003E4487 /* NotificationsModel.swift */; };
|
||||
4C54AA0A29A55429003E4487 /* EventGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0929A55429003E4487 /* EventGroup.swift */; };
|
||||
4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C54AA0B29A5543C003E4487 /* ZapGroup.swift */; };
|
||||
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */; };
|
||||
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */; };
|
||||
4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5F9113283D694D0052CD1C /* FollowTarget.swift */; };
|
||||
@@ -122,6 +135,10 @@
|
||||
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */; };
|
||||
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C987B56283FD07F0042CE38 /* FollowersModel.swift */; };
|
||||
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */; };
|
||||
4C9BB83129C0ED4F00FC4E37 /* DisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */; };
|
||||
4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */; };
|
||||
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */; };
|
||||
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */; };
|
||||
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; };
|
||||
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AC298851D000060CEA /* AccountDeletion.swift */; };
|
||||
4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */; };
|
||||
@@ -155,6 +172,10 @@
|
||||
4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF5297F1A6A00430951 /* EventBody.swift */; };
|
||||
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF7297F1CEE00430951 /* EventProfile.swift */; };
|
||||
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; };
|
||||
4CCEB7A929B29DD50078AA28 /* SearchResultsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7A829B29DD50078AA28 /* SearchResultsModel.swift */; };
|
||||
4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */; };
|
||||
4CCEB7B029B5415A0078AA28 /* SearchingProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */; };
|
||||
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; };
|
||||
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
|
||||
4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */; };
|
||||
4CE0E2B229A3DF6900DB4CA2 /* LoadMoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */; };
|
||||
@@ -201,25 +222,36 @@
|
||||
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABED29844B5500D66079 /* AnyEncodable.swift */; };
|
||||
4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */; };
|
||||
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABF52985CD5500D66079 /* UserSearch.swift */; };
|
||||
4CFF8F6329CC9AD7008DB934 /* ImageContextMenuModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */; };
|
||||
4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6629CC9E3A008DB934 /* ImageView.swift */; };
|
||||
4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */; };
|
||||
4CFF8F6B29CD0079008DB934 /* RepostedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */; };
|
||||
4CFF8F6D29CD022E008DB934 /* WideEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6C29CD022E008DB934 /* WideEventView.swift */; };
|
||||
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
|
||||
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; };
|
||||
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
|
||||
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
|
||||
5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF72FC129B9142F00124A13 /* ShareAction.swift */; };
|
||||
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfileZoomView.swift */; };
|
||||
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643EA5C7296B764E005081BB /* RelayFilterView.swift */; };
|
||||
647D9A8D2968520300A295DE /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; };
|
||||
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64FBD06E296255C400D9D3B2 /* Theme.swift */; };
|
||||
6C7DE41F2955169800E66263 /* Vault in Frameworks */ = {isa = PBXBuildFile; productRef = 6C7DE41E2955169800E66263 /* Vault */; };
|
||||
7C0F392F29B57CAF0039859C /* Binding+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C0F392E29B57CAF0039859C /* Binding+.swift */; };
|
||||
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C60CAEE298471A1009C80D6 /* CoreSVG.swift */; };
|
||||
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */; };
|
||||
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */; };
|
||||
7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CFF6316299FEFE5005D382A /* SelectableText.swift */; };
|
||||
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; };
|
||||
9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C83F89229A937B900136C08 /* TextViewWrapper.swift */; };
|
||||
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; };
|
||||
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
|
||||
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
|
||||
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; };
|
||||
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; };
|
||||
E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */; };
|
||||
E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadView.swift */; };
|
||||
F75BA12D29A1855400E10810 /* BookmarksManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75BA12C29A1855400E10810 /* BookmarksManager.swift */; };
|
||||
F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75BA12E29A18EF500E10810 /* BookmarksView.swift */; };
|
||||
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E91298B0F0700AB113A /* RelayDetailView.swift */; };
|
||||
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */; };
|
||||
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */; };
|
||||
@@ -255,6 +287,18 @@
|
||||
3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A2B8B0A296A8982009CC16D /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "en-US"; path = "en-US.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyDescriptionTests.swift; sourceTree = "<group>"; };
|
||||
3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventDetailBarTests.swift; sourceTree = "<group>"; };
|
||||
3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationUtil.swift; sourceTree = "<group>"; };
|
||||
3A3040F229A91366008A0F29 /* ProfileViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewTests.swift; sourceTree = "<group>"; };
|
||||
3A3040F929A91ED6008A0F29 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A3040FA29A91EFC008A0F29 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A3040FB29A91F03008A0F29 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-HK"; path = "zh-HK.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A3040FC29A91F31008A0F29 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-TW"; path = "zh-TW.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3A3040FD29A91F31008A0F29 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-TW"; path = "zh-TW.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3A3040FE29A91F31008A0F29 /* zh-TW */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-TW"; path = "zh-TW.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A3040FF29AB02D1008A0F29 /* en-US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "en-US"; path = "en-US.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGroupViewTests.swift; sourceTree = "<group>"; };
|
||||
3A41E559299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
@@ -287,6 +331,12 @@
|
||||
3AA247FE297E3D900090C62D /* RepostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostsView.swift; sourceTree = "<group>"; };
|
||||
3AA24801297E3DC20090C62D /* RepostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostView.swift; sourceTree = "<group>"; };
|
||||
3AA59D1C2999B0400061C48E /* DraftsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftsModel.swift; sourceTree = "<group>"; };
|
||||
3AA5E70229B682A5002701ED /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3AA5E70329B682AD002701ED /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3AA5E70429B682B3002701ED /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = uk; path = uk.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3AA5E70529B9E83E002701ED /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3AA5E70629B9E844002701ED /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3AA5E70729B9E84A002701ED /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = bg; path = bg.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3AAA95C9298DF87B00F3D526 /* TranslationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationService.swift; sourceTree = "<group>"; };
|
||||
3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLPlan.swift; sourceTree = "<group>"; };
|
||||
3AB5B86A2986D8A3006599D2 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
@@ -299,6 +349,21 @@
|
||||
3ACB685B297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3ACB685E297633BC00C46468 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3ACBCB77295FE5C70037388A /* TimeAgoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeAgoTests.swift; sourceTree = "<group>"; };
|
||||
3AD14EB529C40F38009D2D9C /* hu-HU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "hu-HU"; path = "hu-HU.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3AD14EB629C40F38009D2D9C /* hu-HU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hu-HU"; path = "hu-HU.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3AD14EB729C40F38009D2D9C /* hu-HU */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "hu-HU"; path = "hu-HU.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3AD14EB829C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "sv-SE"; path = "sv-SE.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3AD14EB929C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sv-SE"; path = "sv-SE.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3AD14EBA29C40F3F009D2D9C /* sv-SE */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "sv-SE"; path = "sv-SE.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3AD14EBB29C40F47009D2D9C /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3AD14EBC29C40F47009D2D9C /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "fr-CA"; path = "fr-CA.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
|
||||
3AD14EBD29C40F47009D2D9C /* fr-CA */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "fr-CA"; path = "fr-CA.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
3AD5662B29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3AD5662C29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = fa; path = fa.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3AD5662D29BD2F5300BF77C5 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3AD5663129C0DA4B00BF77C5 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3AD5663229C0DA4B00BF77C5 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ko; path = ko.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3AD5663329C0DA4B00BF77C5 /* ko */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ko; path = ko.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3AE45AF5297BB2E700C1D842 /* LibreTranslateServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreTranslateServer.swift; sourceTree = "<group>"; };
|
||||
3AEB8003297CCEA800713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "tr-TR"; path = "tr-TR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
|
||||
3AEB8004297CCEA800713A25 /* tr-TR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "tr-TR"; path = "tr-TR.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
@@ -328,6 +393,11 @@
|
||||
4C285C8B28398BC6008A31F1 /* Keys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keys.swift; sourceTree = "<group>"; };
|
||||
4C285C8D28399BFD008A31F1 /* SaveKeysView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveKeysView.swift; sourceTree = "<group>"; };
|
||||
4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debouncer.swift; sourceTree = "<group>"; };
|
||||
4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
|
||||
4C30AC7329A5680900E2BD5A /* EventGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGroupView.swift; sourceTree = "<group>"; };
|
||||
4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationItemView.swift; sourceTree = "<group>"; };
|
||||
4C30AC7729A577AB00E2BD5A /* EventCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventCache.swift; sourceTree = "<group>"; };
|
||||
4C30AC7F29A6A53F00E2BD5A /* ProfilePicturesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePicturesView.swift; sourceTree = "<group>"; };
|
||||
4C363A8328233689006E126D /* Parser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parser.swift; sourceTree = "<group>"; };
|
||||
4C363A8728236948006E126D /* BlocksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlocksView.swift; sourceTree = "<group>"; };
|
||||
4C363A8928236B57006E126D /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = "<group>"; };
|
||||
@@ -408,6 +478,9 @@
|
||||
4C42812B298C848200DBF26F /* TranslateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslateView.swift; sourceTree = "<group>"; };
|
||||
4C477C9D282C3A4800033AA3 /* TipCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TipCounter.swift; sourceTree = "<group>"; };
|
||||
4C4A3A5A288A1B2200453788 /* damus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = damus.entitlements; sourceTree = "<group>"; };
|
||||
4C54AA0629A540BA003E4487 /* NotificationsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsModel.swift; sourceTree = "<group>"; };
|
||||
4C54AA0929A55429003E4487 /* EventGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventGroup.swift; sourceTree = "<group>"; };
|
||||
4C54AA0B29A5543C003E4487 /* ZapGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapGroup.swift; sourceTree = "<group>"; };
|
||||
4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHomeModel.swift; sourceTree = "<group>"; };
|
||||
4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsView.swift; sourceTree = "<group>"; };
|
||||
4C5F9113283D694D0052CD1C /* FollowTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowTarget.swift; sourceTree = "<group>"; };
|
||||
@@ -437,6 +510,10 @@
|
||||
4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Tests.swift; sourceTree = "<group>"; };
|
||||
4C987B56283FD07F0042CE38 /* FollowersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersModel.swift; sourceTree = "<group>"; };
|
||||
4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatroomMetadata.swift; sourceTree = "<group>"; };
|
||||
4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayName.swift; sourceTree = "<group>"; };
|
||||
4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfileName.swift; sourceTree = "<group>"; };
|
||||
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = "<group>"; };
|
||||
4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeAnonPfpView.swift; sourceTree = "<group>"; };
|
||||
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; };
|
||||
4CAAD8AC298851D000060CEA /* AccountDeletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletion.swift; sourceTree = "<group>"; };
|
||||
4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConfigView.swift; sourceTree = "<group>"; };
|
||||
@@ -470,6 +547,10 @@
|
||||
4CC7AAF5297F1A6A00430951 /* EventBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventBody.swift; sourceTree = "<group>"; };
|
||||
4CC7AAF7297F1CEE00430951 /* EventProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfile.swift; sourceTree = "<group>"; };
|
||||
4CC7AAF9297F64AC00430951 /* EventMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMenu.swift; sourceTree = "<group>"; };
|
||||
4CCEB7A829B29DD50078AA28 /* SearchResultsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsModel.swift; sourceTree = "<group>"; };
|
||||
4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingEventView.swift; sourceTree = "<group>"; };
|
||||
4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingProfileView.swift; sourceTree = "<group>"; };
|
||||
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploadModel.swift; sourceTree = "<group>"; };
|
||||
4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; };
|
||||
4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventHolder.swift; sourceTree = "<group>"; };
|
||||
4CE0E2B129A3DF6900DB4CA2 /* LoadMoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreButton.swift; sourceTree = "<group>"; };
|
||||
@@ -519,24 +600,35 @@
|
||||
4CF0ABED29844B5500D66079 /* AnyEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = "<group>"; };
|
||||
4CF0ABEF29857E9200D66079 /* Bech32Object.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Object.swift; sourceTree = "<group>"; };
|
||||
4CF0ABF52985CD5500D66079 /* UserSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSearch.swift; sourceTree = "<group>"; };
|
||||
4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContextMenuModifier.swift; sourceTree = "<group>"; };
|
||||
4CFF8F6629CC9E3A008DB934 /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = "<group>"; };
|
||||
4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContainerView.swift; sourceTree = "<group>"; };
|
||||
4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostedEvent.swift; sourceTree = "<group>"; };
|
||||
4CFF8F6C29CD022E008DB934 /* WideEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WideEventView.swift; sourceTree = "<group>"; };
|
||||
4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; };
|
||||
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; };
|
||||
5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; };
|
||||
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
|
||||
5CF72FC129B9142F00124A13 /* ShareAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAction.swift; sourceTree = "<group>"; };
|
||||
6439E013296790CF0020672B /* ProfileZoomView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZoomView.swift; sourceTree = "<group>"; };
|
||||
643EA5C7296B764E005081BB /* RelayFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterView.swift; sourceTree = "<group>"; };
|
||||
647D9A8C2968520300A295DE /* SideMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = "<group>"; };
|
||||
64FBD06E296255C400D9D3B2 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
|
||||
7C0F392E29B57CAF0039859C /* Binding+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+.swift"; sourceTree = "<group>"; };
|
||||
7C60CAEE298471A1009C80D6 /* CoreSVG.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreSVG.swift; sourceTree = "<group>"; };
|
||||
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomableScrollView.swift; sourceTree = "<group>"; };
|
||||
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KFOptionSetter+.swift"; sourceTree = "<group>"; };
|
||||
7CFF6316299FEFE5005D382A /* SelectableText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableText.swift; sourceTree = "<group>"; };
|
||||
9609F057296E220800069BF3 /* BannerImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerImageView.swift; sourceTree = "<group>"; };
|
||||
9C83F89229A937B900136C08 /* TextViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewWrapper.swift; sourceTree = "<group>"; };
|
||||
9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachMediaUtility.swift; sourceTree = "<group>"; };
|
||||
BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; };
|
||||
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; };
|
||||
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTests.swift; sourceTree = "<group>"; };
|
||||
E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; };
|
||||
E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadV2View.swift; sourceTree = "<group>"; };
|
||||
E9E4ED0A295867B900DD7078 /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; };
|
||||
F75BA12C29A1855400E10810 /* BookmarksManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksManager.swift; sourceTree = "<group>"; };
|
||||
F75BA12E29A18EF500E10810 /* BookmarksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksView.swift; sourceTree = "<group>"; };
|
||||
F7908E91298B0F0700AB113A /* RelayDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayDetailView.swift; sourceTree = "<group>"; };
|
||||
F7908E96298B1FDF00AB113A /* NIPURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIPURLBuilder.swift; sourceTree = "<group>"; };
|
||||
F7F0BA24297892BD009531F3 /* SwipeToDismiss.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeToDismiss.swift; sourceTree = "<group>"; };
|
||||
@@ -584,6 +676,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3AA24801297E3DC20090C62D /* RepostView.swift */,
|
||||
4CFF8F6A29CD0079008DB934 /* RepostedEvent.swift */,
|
||||
);
|
||||
path = Reposts;
|
||||
sourceTree = "<group>";
|
||||
@@ -645,6 +738,8 @@
|
||||
4C0A3F8D280F63FF000448DE /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CCEB7A729B29DC90078AA28 /* Search */,
|
||||
4C54AA0829A55416003E4487 /* Notifications */,
|
||||
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */,
|
||||
4C0A3F8E280F640A000448DE /* ThreadModel.swift */,
|
||||
4C0A3F92280F66F5000448DE /* ReplyMap.swift */,
|
||||
@@ -665,6 +760,7 @@
|
||||
4C63334F283D40E500B1C9C3 /* HomeModel.swift */,
|
||||
4C633351283D419F00B1C9C3 /* SignalModel.swift */,
|
||||
4C5F9113283D694D0052CD1C /* FollowTarget.swift */,
|
||||
F75BA12C29A1855400E10810 /* BookmarksManager.swift */,
|
||||
4C5F9115283D855D0052CD1C /* EventsModel.swift */,
|
||||
4C5F9117283D88E40052CD1C /* FollowingModel.swift */,
|
||||
4C987B56283FD07F0042CE38 /* FollowersModel.swift */,
|
||||
@@ -683,13 +779,38 @@
|
||||
3AAA95CB298E07E900F3D526 /* DeepLPlan.swift */,
|
||||
4CE8795A2996C47A00F758CC /* ZapsModel.swift */,
|
||||
3AA59D1C2999B0400061C48E /* DraftsModel.swift */,
|
||||
4C54AA0629A540BA003E4487 /* NotificationsModel.swift */,
|
||||
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C30AC7029A5676F00E2BD5A /* Notifications */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C30AC7129A5677A00E2BD5A /* NotificationsView.swift */,
|
||||
4C30AC7329A5680900E2BD5A /* EventGroupView.swift */,
|
||||
4C30AC7529A5770900E2BD5A /* NotificationItemView.swift */,
|
||||
4C30AC7F29A6A53F00E2BD5A /* ProfilePicturesView.swift */,
|
||||
);
|
||||
path = Notifications;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C54AA0829A55416003E4487 /* Notifications */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C54AA0929A55429003E4487 /* EventGroup.swift */,
|
||||
4C54AA0B29A5543C003E4487 /* ZapGroup.swift */,
|
||||
);
|
||||
path = Notifications;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4C75EFA227FA576C0006080F /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CFF8F6129CC9A80008DB934 /* Images */,
|
||||
4CCEB7AC29B53D180078AA28 /* Search */,
|
||||
4C30AC7029A5676F00E2BD5A /* Notifications */,
|
||||
4CE0E2B029A3DF4700DB4CA2 /* Timeline */,
|
||||
4CE879562996C44A00F758CC /* Zaps */,
|
||||
4CB9D4A52992D01900A9A7E4 /* Profile */,
|
||||
@@ -702,6 +823,7 @@
|
||||
4CB88387296AF97C00DC99E7 /* ActionBar */,
|
||||
4CE4F9E228528C5200C00DD9 /* AddRelayView.swift */,
|
||||
4C363A8728236948006E126D /* BlocksView.swift */,
|
||||
F75BA12E29A18EF500E10810 /* BookmarksView.swift */,
|
||||
4C285C8128385570008A31F1 /* CarouselView.swift */,
|
||||
4C0A3F8B280F5FCA000448DE /* ChatroomView.swift */,
|
||||
4C0A3F90280F6528000448DE /* ChatView.swift */,
|
||||
@@ -721,11 +843,9 @@
|
||||
4C363A8D28236FE4006E126D /* NoteContentView.swift */,
|
||||
4C75EFAC28049CFB0006080F /* PostButton.swift */,
|
||||
4C75EFA327FA577B0006080F /* PostView.swift */,
|
||||
9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */,
|
||||
9C83F89229A937B900136C08 /* TextViewWrapper.swift */,
|
||||
4CEE2AF8280B2EAC00AB5EEF /* PowView.swift */,
|
||||
4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */,
|
||||
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */,
|
||||
4CEE2AF2280B25C500AB5EEF /* ProfilePicView.swift */,
|
||||
4C8682862814DE470026224F /* ProfileView.swift */,
|
||||
4C3AC7A42836987600E1F516 /* MainTabView.swift */,
|
||||
4C363A8B28236B92006E126D /* PubkeyView.swift */,
|
||||
4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */,
|
||||
@@ -736,7 +856,7 @@
|
||||
4C363AA128296A7E006E126D /* SearchView.swift */,
|
||||
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */,
|
||||
4C3AC7A02835A81400E1F516 /* SetupView.swift */,
|
||||
E9E4ED0A295867B900DD7078 /* ThreadV2View.swift */,
|
||||
E9E4ED0A295867B900DD7078 /* ThreadView.swift */,
|
||||
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */,
|
||||
4CB55EF4295E679D007FD187 /* UserRelaysView.swift */,
|
||||
647D9A8C2968520300A295DE /* SideMenuView.swift */,
|
||||
@@ -775,6 +895,7 @@
|
||||
4C7FF7D628233637009601DB /* Util */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7C0F392D29B57C8F0039859C /* Extensions */,
|
||||
4CE879492995B58700F758CC /* Relays */,
|
||||
4CF0ABEA29844B2F00D66079 /* AnyCodable */,
|
||||
4CC7AAE6297EFA7B00430951 /* Zap.swift */,
|
||||
@@ -802,8 +923,10 @@
|
||||
4CB883B5297730E400DC99E7 /* LNUrls.swift */,
|
||||
3AB72AB8298ECF30004BB58C /* Translator.swift */,
|
||||
4C2CDDF6299D4A5E00879FD5 /* Debouncer.swift */,
|
||||
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */,
|
||||
4CE0E2AE29A2E82100DB4CA2 /* EventHolder.swift */,
|
||||
3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */,
|
||||
4C30AC7729A577AB00E2BD5A /* EventCache.swift */,
|
||||
4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */,
|
||||
);
|
||||
path = Util;
|
||||
sourceTree = "<group>";
|
||||
@@ -828,6 +951,7 @@
|
||||
children = (
|
||||
4CEE2B01280B39E800AB5EEF /* EventActionBar.swift */,
|
||||
4CB88388296AF99A00DC99E7 /* EventDetailBar.swift */,
|
||||
5CF72FC129B9142F00124A13 /* ShareAction.swift */,
|
||||
);
|
||||
path = ActionBar;
|
||||
sourceTree = "<group>";
|
||||
@@ -843,8 +967,14 @@
|
||||
4CB9D4A52992D01900A9A7E4 /* Profile */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */,
|
||||
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */,
|
||||
4CEE2AF2280B25C500AB5EEF /* ProfilePicView.swift */,
|
||||
4C8682862814DE470026224F /* ProfileView.swift */,
|
||||
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */,
|
||||
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */,
|
||||
4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */,
|
||||
4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */,
|
||||
);
|
||||
path = Profile;
|
||||
sourceTree = "<group>";
|
||||
@@ -862,10 +992,28 @@
|
||||
4CF0ABE6298444FC00D66079 /* MutedEventView.swift */,
|
||||
4C3D52B5298DB4E6001C5831 /* ZapEvent.swift */,
|
||||
4C3D52B7298DB5C6001C5831 /* TextEvent.swift */,
|
||||
4CFF8F6C29CD022E008DB934 /* WideEventView.swift */,
|
||||
);
|
||||
path = Events;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4CCEB7A729B29DC90078AA28 /* Search */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CCEB7A829B29DD50078AA28 /* SearchResultsModel.swift */,
|
||||
);
|
||||
path = Search;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4CCEB7AC29B53D180078AA28 /* Search */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */,
|
||||
4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */,
|
||||
);
|
||||
path = Search;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4CE0E2B029A3DF4700DB4CA2 /* Timeline */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -965,6 +1113,10 @@
|
||||
4CF0ABDB2981A19E00D66079 /* ListTests.swift */,
|
||||
4CB883A9297612FF00DC99E7 /* ZapTests.swift */,
|
||||
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */,
|
||||
3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */,
|
||||
3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */,
|
||||
3A3040F229A91366008A0F29 /* ProfileViewTests.swift */,
|
||||
3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */,
|
||||
);
|
||||
path = damusTests;
|
||||
sourceTree = "<group>";
|
||||
@@ -999,6 +1151,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CE879572996C45300F758CC /* ZapsView.swift */,
|
||||
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */,
|
||||
);
|
||||
path = Zaps;
|
||||
sourceTree = "<group>";
|
||||
@@ -1037,6 +1190,25 @@
|
||||
path = Posting;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4CFF8F6129CC9A80008DB934 /* Images */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */,
|
||||
4CFF8F6629CC9E3A008DB934 /* ImageView.swift */,
|
||||
4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */,
|
||||
);
|
||||
path = Images;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7C0F392D29B57C8F0039859C /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7C95CAED299DCEF1009DCB67 /* KFOptionSetter+.swift */,
|
||||
7C0F392E29B57CAF0039859C /* Binding+.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F7F0BA23297892AE009531F3 /* Modifiers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1154,6 +1326,15 @@
|
||||
id,
|
||||
cs,
|
||||
ru,
|
||||
"zh-HK",
|
||||
"zh-TW",
|
||||
uk,
|
||||
bg,
|
||||
fa,
|
||||
ko,
|
||||
"hu-HU",
|
||||
"sv-SE",
|
||||
"fr-CA",
|
||||
);
|
||||
mainGroup = 4CE6DEDA27F7A08100C66700;
|
||||
packageReferences = (
|
||||
@@ -1208,8 +1389,10 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */,
|
||||
5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */,
|
||||
4C363A8A28236B57006E126D /* MentionView.swift in Sources */,
|
||||
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */,
|
||||
4C30AC7829A577AB00E2BD5A /* EventCache.swift in Sources */,
|
||||
4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */,
|
||||
4C216F34286F5ACD00040376 /* DMView.swift in Sources */,
|
||||
4C3EA64428FF558100C48A62 /* sha256.c in Sources */,
|
||||
@@ -1222,9 +1405,11 @@
|
||||
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */,
|
||||
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */,
|
||||
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
|
||||
4C54AA0C29A5543C003E4487 /* ZapGroup.swift in Sources */,
|
||||
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
|
||||
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */,
|
||||
4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */,
|
||||
4CCEB7A929B29DD50078AA28 /* SearchResultsModel.swift in Sources */,
|
||||
4C3EA67728FF7A9800C48A62 /* talstr.c in Sources */,
|
||||
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
|
||||
4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */,
|
||||
@@ -1233,8 +1418,10 @@
|
||||
4C363AA228296A7E006E126D /* SearchView.swift in Sources */,
|
||||
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */,
|
||||
4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */,
|
||||
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */,
|
||||
4C75EFB92804A2740006080F /* EventView.swift in Sources */,
|
||||
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */,
|
||||
F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */,
|
||||
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */,
|
||||
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */,
|
||||
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */,
|
||||
@@ -1259,21 +1446,26 @@
|
||||
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */,
|
||||
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */,
|
||||
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */,
|
||||
4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */,
|
||||
4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */,
|
||||
4C649844285A952100EAE2B3 /* LocalUserConfig.swift in Sources */,
|
||||
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
|
||||
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
|
||||
4C30AC7629A5770900E2BD5A /* NotificationItemView.swift in Sources */,
|
||||
4C363A8428233689006E126D /* Parser.swift in Sources */,
|
||||
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */,
|
||||
4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */,
|
||||
4C363A9A28283854006E126D /* Reply.swift in Sources */,
|
||||
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */,
|
||||
4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */,
|
||||
4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */,
|
||||
4CB8838B296F6E1E00DC99E7 /* NIP05Badge.swift in Sources */,
|
||||
4C3EA66828FF5F9900C48A62 /* hex.c in Sources */,
|
||||
E9E4ED0B295867B900DD7078 /* ThreadV2View.swift in Sources */,
|
||||
E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */,
|
||||
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */,
|
||||
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
|
||||
4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */,
|
||||
9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */,
|
||||
4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */,
|
||||
4CB883A82975FC1800DC99E7 /* Zaps.swift in Sources */,
|
||||
4C75EFB128049D510006080F /* NostrResponse.swift in Sources */,
|
||||
@@ -1282,10 +1474,13 @@
|
||||
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */,
|
||||
F7908E97298B1FDF00AB113A /* NIPURLBuilder.swift in Sources */,
|
||||
4C285C8228385570008A31F1 /* CarouselView.swift in Sources */,
|
||||
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */,
|
||||
F75BA12D29A1855400E10810 /* BookmarksManager.swift in Sources */,
|
||||
4C3EA67F28FFC01D00C48A62 /* InvoiceView.swift in Sources */,
|
||||
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */,
|
||||
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
|
||||
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
|
||||
4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */,
|
||||
4CF0ABE929844AF100D66079 /* AnyCodable.swift in Sources */,
|
||||
4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */,
|
||||
4CC7AAF2297F129C00430951 /* EmbeddedEventView.swift in Sources */,
|
||||
@@ -1294,6 +1489,7 @@
|
||||
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
|
||||
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
|
||||
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */,
|
||||
4CCEB7B029B5415A0078AA28 /* SearchingProfileView.swift in Sources */,
|
||||
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */,
|
||||
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */,
|
||||
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */,
|
||||
@@ -1310,16 +1506,22 @@
|
||||
4C285C8C28398BC7008A31F1 /* Keys.swift in Sources */,
|
||||
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */,
|
||||
4CE879582996C45300F758CC /* ZapsView.swift in Sources */,
|
||||
4C30AC7429A5680900E2BD5A /* EventGroupView.swift in Sources */,
|
||||
4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */,
|
||||
4CFF8F6D29CD022E008DB934 /* WideEventView.swift in Sources */,
|
||||
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */,
|
||||
4C363A94282704FA006E126D /* Post.swift in Sources */,
|
||||
4C216F32286E388800040376 /* DMChatView.swift in Sources */,
|
||||
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */,
|
||||
4CFF8F6329CC9AD7008DB934 /* ImageContextMenuModifier.swift in Sources */,
|
||||
4C54AA0A29A55429003E4487 /* EventGroup.swift in Sources */,
|
||||
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
|
||||
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
|
||||
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */,
|
||||
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */,
|
||||
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
|
||||
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
|
||||
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
|
||||
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
||||
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
|
||||
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */,
|
||||
@@ -1335,11 +1537,13 @@
|
||||
4C3EA64F28FF59F200C48A62 /* tal.c in Sources */,
|
||||
4CB88393296F798300DC99E7 /* ReactionsModel.swift in Sources */,
|
||||
4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */,
|
||||
4CFF8F6B29CD0079008DB934 /* RepostedEvent.swift in Sources */,
|
||||
4C8682872814DE470026224F /* ProfileView.swift in Sources */,
|
||||
4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */,
|
||||
4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */,
|
||||
4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */,
|
||||
4CF0ABD82981980C00D66079 /* Lists.swift in Sources */,
|
||||
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */,
|
||||
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
|
||||
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
|
||||
6439E014296790CF0020672B /* ProfileZoomView.swift in Sources */,
|
||||
@@ -1368,7 +1572,10 @@
|
||||
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */,
|
||||
4C3AC79B28306D7B00E1F516 /* Contacts.swift in Sources */,
|
||||
4C3EA63D28FF52D600C48A62 /* bolt11.c in Sources */,
|
||||
4C9BB83129C0ED4F00FC4E37 /* DisplayName.swift in Sources */,
|
||||
7CFF6317299FEFE5005D382A /* SelectableText.swift in Sources */,
|
||||
4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */,
|
||||
4C54AA0729A540BA003E4487 /* NotificationsModel.swift in Sources */,
|
||||
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */,
|
||||
4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */,
|
||||
4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */,
|
||||
@@ -1393,11 +1600,13 @@
|
||||
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */,
|
||||
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
|
||||
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
|
||||
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */,
|
||||
4C75EFB528049D790006080F /* Relay.swift in Sources */,
|
||||
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */,
|
||||
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
|
||||
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
|
||||
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
|
||||
7C0F392F29B57CAF0039859C /* Binding+.swift in Sources */,
|
||||
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */,
|
||||
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
|
||||
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
|
||||
@@ -1408,8 +1617,11 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
|
||||
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */,
|
||||
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */,
|
||||
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */,
|
||||
3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */,
|
||||
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
|
||||
4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */,
|
||||
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */,
|
||||
@@ -1419,6 +1631,7 @@
|
||||
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */,
|
||||
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */,
|
||||
4CE6DEF827F7A08200C66700 /* damusTests.swift in Sources */,
|
||||
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */,
|
||||
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -1468,6 +1681,15 @@
|
||||
3A41E55B299D52BE001FA465 /* id */,
|
||||
3A8624DB299E82BE00BD8BE9 /* cs */,
|
||||
3A827A1A299FC69D00C4D171 /* ru */,
|
||||
3A3040FB29A91F03008A0F29 /* zh-HK */,
|
||||
3A3040FD29A91F31008A0F29 /* zh-TW */,
|
||||
3AA5E70429B682B3002701ED /* uk */,
|
||||
3AA5E70729B9E84A002701ED /* bg */,
|
||||
3AD5662C29BD2F5300BF77C5 /* fa */,
|
||||
3AD5663229C0DA4B00BF77C5 /* ko */,
|
||||
3AD14EB529C40F38009D2D9C /* hu-HU */,
|
||||
3AD14EB829C40F3F009D2D9C /* sv-SE */,
|
||||
3AD14EBC29C40F47009D2D9C /* fr-CA */,
|
||||
);
|
||||
name = Localizable.stringsdict;
|
||||
sourceTree = "<group>";
|
||||
@@ -1491,6 +1713,15 @@
|
||||
3A41E559299D52BE001FA465 /* id */,
|
||||
3A8624D9299E82BE00BD8BE9 /* cs */,
|
||||
3A827A18299FC69D00C4D171 /* ru */,
|
||||
3A3040F929A91ED6008A0F29 /* zh-HK */,
|
||||
3A3040FC29A91F31008A0F29 /* zh-TW */,
|
||||
3AA5E70329B682AD002701ED /* uk */,
|
||||
3AA5E70529B9E83E002701ED /* bg */,
|
||||
3AD5662B29BD2F5300BF77C5 /* fa */,
|
||||
3AD5663329C0DA4B00BF77C5 /* ko */,
|
||||
3AD14EB629C40F38009D2D9C /* hu-HU */,
|
||||
3AD14EB929C40F3F009D2D9C /* sv-SE */,
|
||||
3AD14EBB29C40F47009D2D9C /* fr-CA */,
|
||||
);
|
||||
name = InfoPlist.strings;
|
||||
sourceTree = "<group>";
|
||||
@@ -1514,6 +1745,16 @@
|
||||
3A41E55A299D52BE001FA465 /* id */,
|
||||
3A8624DA299E82BE00BD8BE9 /* cs */,
|
||||
3A827A19299FC69D00C4D171 /* ru */,
|
||||
3A3040FA29A91EFC008A0F29 /* zh-HK */,
|
||||
3A3040FE29A91F31008A0F29 /* zh-TW */,
|
||||
3A3040FF29AB02D1008A0F29 /* en-US */,
|
||||
3AA5E70229B682A5002701ED /* uk */,
|
||||
3AA5E70629B9E844002701ED /* bg */,
|
||||
3AD5662D29BD2F5300BF77C5 /* fa */,
|
||||
3AD5663129C0DA4B00BF77C5 /* ko */,
|
||||
3AD14EB729C40F38009D2D9C /* hu-HU */,
|
||||
3AD14EBA29C40F3F009D2D9C /* sv-SE */,
|
||||
3AD14EBD29C40F47009D2D9C /* fr-CA */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
@@ -1649,7 +1890,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
CURRENT_PROJECT_VERSION = 7;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1672,7 +1913,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
MARKETING_VERSION = 1.3.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
@@ -1691,7 +1932,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 3;
|
||||
CURRENT_PROJECT_VERSION = 7;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -1714,7 +1955,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 1.1.0;
|
||||
MARKETING_VERSION = 1.3.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
|
||||
@@ -52,6 +52,7 @@ struct CustomPicker<SelectionValue: Hashable, Content: View>: View {
|
||||
.accentColor(tag == selection ? textColor() : .gray)
|
||||
}
|
||||
}
|
||||
.background(Color(UIColor.systemBackground))
|
||||
}
|
||||
|
||||
func textColor() -> Color {
|
||||
|
||||
@@ -31,160 +31,7 @@ struct ShareSheet: UIViewControllerRepresentable {
|
||||
}
|
||||
}
|
||||
|
||||
struct ImageContextMenuModifier: ViewModifier {
|
||||
let url: URL?
|
||||
let image: UIImage?
|
||||
@Binding var showShareSheet: Bool
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
return content.contextMenu {
|
||||
Button {
|
||||
UIPasteboard.general.url = url
|
||||
} label: {
|
||||
Label(NSLocalizedString("Copy Image URL", comment: "Context menu option to copy the URL of an image into clipboard."), systemImage: "doc.on.doc")
|
||||
}
|
||||
if let someImage = image {
|
||||
Button {
|
||||
UIPasteboard.general.image = someImage
|
||||
} label: {
|
||||
Label(NSLocalizedString("Copy Image", comment: "Context menu option to copy an image into clipboard."), systemImage: "photo.on.rectangle")
|
||||
}
|
||||
Button {
|
||||
UIImageWriteToSavedPhotosAlbum(someImage, nil, nil, nil)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Save Image", comment: "Context menu option to save an image."), systemImage: "square.and.arrow.down")
|
||||
}
|
||||
}
|
||||
Button {
|
||||
showShareSheet = true
|
||||
} label: {
|
||||
Label(NSLocalizedString("Share", comment: "Button to share an image."), systemImage: "square.and.arrow.up")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct ImageContainerView: View {
|
||||
|
||||
let url: URL?
|
||||
|
||||
@State private var image: UIImage?
|
||||
@State private var showShareSheet = false
|
||||
|
||||
private struct ImageHandler: ImageModifier {
|
||||
@Binding var handler: UIImage?
|
||||
|
||||
func modify(_ image: UIImage) -> UIImage {
|
||||
handler = image
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
KFAnimatedImage(url)
|
||||
.imageContext(.note)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.imageModifier(ImageHandler(handler: $image))
|
||||
.clipped()
|
||||
.modifier(ImageContextMenuModifier(url: url, image: image, showShareSheet: $showShareSheet))
|
||||
.sheet(isPresented: $showShareSheet) {
|
||||
ShareSheet(activityItems: [url])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ImageView: View {
|
||||
|
||||
let urls: [URL?]
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
@State private var selectedIndex = 0
|
||||
@State var showMenu = true
|
||||
|
||||
var navBarView: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
Text(urls[selectedIndex]?.lastPathComponent ?? "")
|
||||
.bold()
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}, label: {
|
||||
Image(systemName: "xmark")
|
||||
})
|
||||
}
|
||||
.padding()
|
||||
|
||||
Divider()
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
.background(.regularMaterial)
|
||||
}
|
||||
|
||||
var tabViewIndicator: some View {
|
||||
HStack(spacing: 10) {
|
||||
ForEach(urls.indices, id: \.self) { index in
|
||||
Capsule()
|
||||
.fill(index == selectedIndex ? Color(UIColor.label) : Color.secondary)
|
||||
.frame(width: 7, height: 7)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(.regularMaterial)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color(.systemBackground)
|
||||
.ignoresSafeArea()
|
||||
|
||||
TabView(selection: $selectedIndex) {
|
||||
ForEach(urls.indices, id: \.self) { index in
|
||||
ZoomableScrollView {
|
||||
ImageContainerView(url: urls[index])
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.padding(.top, Theme.safeAreaInsets?.top)
|
||||
.padding(.bottom, Theme.safeAreaInsets?.bottom)
|
||||
}
|
||||
.modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}))
|
||||
.ignoresSafeArea()
|
||||
.tag(index)
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
||||
.gesture(TapGesture(count: 2).onEnded {
|
||||
// Prevents menu from hiding on double tap
|
||||
})
|
||||
.gesture(TapGesture(count: 1).onEnded {
|
||||
showMenu.toggle()
|
||||
})
|
||||
.overlay(
|
||||
VStack {
|
||||
if showMenu {
|
||||
navBarView
|
||||
Spacer()
|
||||
|
||||
if (urls.count > 1) {
|
||||
tabViewIndicator
|
||||
}
|
||||
}
|
||||
}
|
||||
.animation(.easeInOut, value: showMenu)
|
||||
.padding(.bottom, Theme.safeAreaInsets?.bottom)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ImageCarousel: View {
|
||||
var urls: [URL]
|
||||
@@ -204,24 +51,24 @@ struct ImageCarousel: View {
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.aspectRatio(contentMode: .fill)
|
||||
//.cornerRadius(10)
|
||||
.tabItem {
|
||||
Text(url.absoluteString)
|
||||
}
|
||||
.id(url.absoluteString)
|
||||
.contextMenu {
|
||||
Button(NSLocalizedString("Copy Image", comment: "Context menu option to copy an image to clipboard.")) {
|
||||
UIPasteboard.general.string = url.absoluteString
|
||||
}
|
||||
}
|
||||
// .contextMenu {
|
||||
// Button(NSLocalizedString("Copy Image", comment: "Context menu option to copy an image to clipboard.")) {
|
||||
// UIPasteboard.general.string = url.absoluteString
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
.cornerRadius(10)
|
||||
.fullScreenCover(isPresented: $open_sheet) {
|
||||
ImageView(urls: urls)
|
||||
}
|
||||
.frame(height: 200)
|
||||
.frame(height: 350)
|
||||
.onTapGesture {
|
||||
open_sheet = true
|
||||
}
|
||||
|
||||
@@ -15,12 +15,10 @@ struct Reposted: View {
|
||||
var body: some View {
|
||||
HStack(alignment: .center) {
|
||||
Image(systemName: "arrow.2.squarepath")
|
||||
.font(.footnote)
|
||||
.foregroundColor(Color.gray)
|
||||
ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true, show_nip5_domain: false)
|
||||
.foregroundColor(Color.gray)
|
||||
Text("Reposted", comment: "Text indicating that the post was reposted (i.e. re-shared).")
|
||||
.font(.footnote)
|
||||
.foregroundColor(Color.gray)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ struct SelectableText: View {
|
||||
fixedWidth: selectedTextWidth,
|
||||
height: $selectedTextHeight
|
||||
)
|
||||
.padding([.leading, .trailing], -1.0)
|
||||
.onAppear {
|
||||
self.selectedTextWidth = geo.size.width
|
||||
}
|
||||
@@ -49,8 +50,11 @@ struct SelectableText: View {
|
||||
view.isEditable = false
|
||||
view.dataDetectorTypes = .all
|
||||
view.isSelectable = true
|
||||
view.backgroundColor = .clear
|
||||
view.textContainer.lineFragmentPadding = 0
|
||||
view.textContainerInset = .zero
|
||||
view.textContainerInset.left = 1.0
|
||||
view.textContainerInset.right = 1.0
|
||||
return view
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ struct TranslateView: View {
|
||||
@State var translated_note: String? = nil
|
||||
@State var show_translated_note: Bool = false
|
||||
@State var translated_artifacts: NoteArtifacts? = nil
|
||||
|
||||
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
|
||||
|
||||
var TranslateButton: some View {
|
||||
Button(NSLocalizedString("Translate Note", comment: "Button to translate note from different language.")) {
|
||||
@@ -80,24 +82,7 @@ struct TranslateView: View {
|
||||
currentLanguage = Locale.current.languageCode ?? "en"
|
||||
}
|
||||
|
||||
// Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in
|
||||
// and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer.
|
||||
let originalBlocks = event.blocks(damus_state.keypair.privkey)
|
||||
let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ")
|
||||
|
||||
// Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate.
|
||||
let languageRecognizer = NLLanguageRecognizer()
|
||||
languageRecognizer.processString(originalOnlyText)
|
||||
noteLanguage = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= 0.5 })?.key.rawValue ?? currentLanguage
|
||||
|
||||
if let lang = noteLanguage, noteLanguage != currentLanguage {
|
||||
// If the detected dominant language is a variant, remove the variant component and just take the language part as translation services typically only supports the variant-less language.
|
||||
if #available(iOS 16, *) {
|
||||
noteLanguage = Locale.LanguageCode(stringLiteral: lang).identifier(.alpha2)
|
||||
} else {
|
||||
noteLanguage = NSLocale(localeIdentifier: lang).languageCode
|
||||
}
|
||||
}
|
||||
noteLanguage = event.note_language(damus_state.keypair.privkey) ?? currentLanguage
|
||||
|
||||
guard let note_lang = noteLanguage else {
|
||||
noteLanguage = currentLanguage
|
||||
@@ -106,9 +91,9 @@ struct TranslateView: View {
|
||||
return
|
||||
}
|
||||
|
||||
if note_lang != currentLanguage {
|
||||
if !preferredLanguages.contains(note_lang) {
|
||||
do {
|
||||
// If the note language is different from our language, send a translation request.
|
||||
// If the note language is different from our preferred languages, send a translation request.
|
||||
let translator = Translator(damus_state.settings)
|
||||
let originalContent = event.get_content(damus_state.keypair.privkey)
|
||||
translated_note = try await translator.translate(originalContent, from: note_lang, to: currentLanguage)
|
||||
@@ -132,11 +117,21 @@ struct TranslateView: View {
|
||||
}
|
||||
|
||||
checkingTranslationStatus = false
|
||||
|
||||
|
||||
show_translated_note = damus_state.settings.auto_translate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func translate_button_style() -> some View {
|
||||
return self
|
||||
.font(.footnote)
|
||||
.contentShape(Rectangle())
|
||||
.padding([.top, .bottom], 10)
|
||||
}
|
||||
}
|
||||
|
||||
struct TranslateView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let ds = test_damus_state()
|
||||
|
||||
@@ -12,11 +12,7 @@ struct UserView: View {
|
||||
let pubkey: String
|
||||
|
||||
var body: some View {
|
||||
let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state)
|
||||
let followers = FollowersModel(damus_state: damus_state, target: pubkey)
|
||||
let pv = ProfileView(damus_state: damus_state, profile: pmodel, followers: followers)
|
||||
|
||||
NavigationLink(destination: pv) {
|
||||
NavigationLink(destination: ProfileView(damus_state: damus_state, pubkey: pubkey)) {
|
||||
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
|
||||
@@ -7,6 +7,22 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum ZappingEventType {
|
||||
case failed(ZappingError)
|
||||
case got_zap_invoice(String)
|
||||
}
|
||||
|
||||
enum ZappingError {
|
||||
case fetching_invoice
|
||||
case bad_lnurl
|
||||
}
|
||||
|
||||
struct ZappingEvent {
|
||||
let is_custom: Bool
|
||||
let type: ZappingEventType
|
||||
let event: NostrEvent
|
||||
}
|
||||
|
||||
struct ZapButton: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
@@ -19,61 +35,8 @@ struct ZapButton: View {
|
||||
@State var slider_value: Double = 0.0
|
||||
@State var slider_visible: Bool = false
|
||||
@State var showing_select_wallet: Bool = false
|
||||
|
||||
func send_zap() {
|
||||
guard let privkey = damus_state.keypair.privkey else {
|
||||
return
|
||||
}
|
||||
|
||||
// Only take the first 10 because reasons
|
||||
let relays = Array(damus_state.pool.descriptors.prefix(10))
|
||||
let target = ZapTarget.note(id: event.id, author: event.pubkey)
|
||||
// TODO: gather comment?
|
||||
let content = ""
|
||||
let zapreq = make_zap_request_event(pubkey: damus_state.pubkey, privkey: privkey, content: content, relays: relays, target: target)
|
||||
|
||||
zapping = true
|
||||
|
||||
Task {
|
||||
var mpayreq = damus_state.lnurls.lookup(target.pubkey)
|
||||
if mpayreq == nil {
|
||||
mpayreq = await fetch_static_payreq(lnurl)
|
||||
}
|
||||
|
||||
guard let payreq = mpayreq else {
|
||||
// TODO: show error
|
||||
DispatchQueue.main.async {
|
||||
zapping = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
damus_state.lnurls.endpoints[target.pubkey] = payreq
|
||||
}
|
||||
|
||||
let zap_amount = get_default_zap_amount(pubkey: damus_state.pubkey) ?? 1000
|
||||
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount) else {
|
||||
DispatchQueue.main.async {
|
||||
zapping = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
zapping = false
|
||||
|
||||
if should_show_wallet_selector(damus_state.pubkey) {
|
||||
self.invoice = inv
|
||||
self.showing_select_wallet = true
|
||||
} else {
|
||||
open_with_wallet(wallet: get_default_wallet(damus_state.pubkey).model, invoice: inv)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//damus_state.pool.send(.event(zapreq))
|
||||
}
|
||||
@State var showing_zap_customizer: Bool = false
|
||||
@State var is_charging: Bool = false
|
||||
|
||||
var zap_img: String {
|
||||
if bar.zapped {
|
||||
@@ -92,6 +55,10 @@ struct ZapButton: View {
|
||||
return Color.orange
|
||||
}
|
||||
|
||||
if is_charging {
|
||||
return Color.yellow
|
||||
}
|
||||
|
||||
if !zapping {
|
||||
return nil
|
||||
}
|
||||
@@ -101,22 +68,66 @@ struct ZapButton: View {
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 4) {
|
||||
EventActionButton(img: zap_img, col: zap_color) {
|
||||
if bar.zapped {
|
||||
//notify(.delete, bar.our_tip)
|
||||
} else if !zapping {
|
||||
send_zap()
|
||||
Button(action: {
|
||||
}, label: {
|
||||
Image(systemName: zap_img)
|
||||
.foregroundColor(zap_color == nil ? Color.gray : zap_color!)
|
||||
.font(.footnote.weight(.medium))
|
||||
})
|
||||
.simultaneousGesture(LongPressGesture().onEnded {_ in
|
||||
guard !zapping else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
self.showing_zap_customizer = true
|
||||
})
|
||||
.highPriorityGesture(TapGesture().onEnded {_ in
|
||||
guard !zapping else {
|
||||
return
|
||||
}
|
||||
|
||||
send_zap(damus_state: damus_state, event: event, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: ZapType.pub)
|
||||
self.zapping = true
|
||||
})
|
||||
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
|
||||
|
||||
Text(String("\(bar.zap_total > 0 ? "\(format_msats_abbrev(bar.zap_total))" : "")"))
|
||||
.font(.footnote)
|
||||
.foregroundColor(bar.zapped ? Color.orange : Color.gray)
|
||||
|
||||
if bar.zap_total > 0 {
|
||||
Text(verbatim: format_msats_abbrev(bar.zap_total))
|
||||
.font(.footnote)
|
||||
.foregroundColor(bar.zapped ? Color.orange : Color.gray)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showing_zap_customizer) {
|
||||
CustomizeZapView(state: damus_state, event: event, lnurl: lnurl)
|
||||
}
|
||||
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
|
||||
SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: invoice)
|
||||
}
|
||||
.onReceive(handle_notify(.zapping)) { notif in
|
||||
let zap_ev = notif.object as! ZappingEvent
|
||||
|
||||
guard zap_ev.event.id == self.event.id else {
|
||||
return
|
||||
}
|
||||
|
||||
guard !zap_ev.is_custom else {
|
||||
return
|
||||
}
|
||||
|
||||
switch zap_ev.type {
|
||||
case .failed:
|
||||
break
|
||||
case .got_zap_invoice(let inv):
|
||||
if should_show_wallet_selector(damus_state.pubkey) {
|
||||
self.invoice = inv
|
||||
self.showing_select_wallet = true
|
||||
} else {
|
||||
open_with_wallet(wallet: get_default_wallet(damus_state.pubkey).model, invoice: inv)
|
||||
}
|
||||
}
|
||||
|
||||
self.zapping = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,3 +139,56 @@ struct ZapButton_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) {
|
||||
guard let keypair = damus_state.keypair.to_full() else {
|
||||
return
|
||||
}
|
||||
|
||||
// Only take the first 10 because reasons
|
||||
let relays = Array(damus_state.pool.descriptors.prefix(10))
|
||||
let target = ZapTarget.note(id: event.id, author: event.pubkey)
|
||||
let content = comment ?? ""
|
||||
|
||||
let zapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type)
|
||||
|
||||
Task {
|
||||
var mpayreq = damus_state.lnurls.lookup(target.pubkey)
|
||||
if mpayreq == nil {
|
||||
mpayreq = await fetch_static_payreq(lnurl)
|
||||
}
|
||||
|
||||
guard let payreq = mpayreq else {
|
||||
// TODO: show error
|
||||
DispatchQueue.main.async {
|
||||
let typ = ZappingEventType.failed(.bad_lnurl)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
|
||||
notify(.zapping, ev)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
damus_state.lnurls.endpoints[target.pubkey] = payreq
|
||||
}
|
||||
|
||||
let zap_amount = amount_sats ?? get_default_zap_amount(pubkey: damus_state.pubkey) ?? 1000
|
||||
|
||||
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount, zap_type: zap_type, comment: comment) else {
|
||||
DispatchQueue.main.async {
|
||||
let typ = ZappingEventType.failed(.fetching_invoice)
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
|
||||
notify(.zapping, ev)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), event: event)
|
||||
notify(.zapping, ev)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -11,11 +11,8 @@ import Starscream
|
||||
var BOOTSTRAP_RELAYS = [
|
||||
"wss://relay.damus.io",
|
||||
"wss://eden.nostr.land",
|
||||
"wss://relay.snort.social",
|
||||
"wss://offchain.pub",
|
||||
"wss://nostr.wine",
|
||||
"wss://nos.lol",
|
||||
"wss://relay.current.fyi",
|
||||
"wss://brb.io",
|
||||
]
|
||||
|
||||
struct TimestampedProfile {
|
||||
@@ -81,7 +78,7 @@ struct ContentView: View {
|
||||
@State var event: NostrEvent? = nil
|
||||
@State var active_profile: String? = nil
|
||||
@State var active_search: NostrFilter? = nil
|
||||
@State var active_event_id: String? = nil
|
||||
@State var active_event: NostrEvent? = nil
|
||||
@State var profile_open: Bool = false
|
||||
@State var thread_open: Bool = false
|
||||
@State var search_open: Bool = false
|
||||
@@ -89,6 +86,7 @@ struct ContentView: View {
|
||||
@State var confirm_block: Bool = false
|
||||
@State var user_blocked_confirm: Bool = false
|
||||
@State var confirm_overwrite_mutelist: Bool = false
|
||||
@State var current_boost: NostrEvent? = nil
|
||||
@State var filter_state : FilterState = .posts_and_replies
|
||||
@State private var isSideBarOpened = false
|
||||
@StateObject var home: HomeModel = HomeModel()
|
||||
@@ -147,27 +145,23 @@ struct ContentView: View {
|
||||
search_open = false
|
||||
isSideBarOpened = false
|
||||
}
|
||||
|
||||
var timelineNavItem: some View {
|
||||
VStack {
|
||||
switch selected_timeline {
|
||||
case .home:
|
||||
Image("damus-home")
|
||||
.resizable()
|
||||
.frame(width:30,height:30)
|
||||
.shadow(color: Color("DamusPurple"), radius: 2)
|
||||
case .dms:
|
||||
Text("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
|
||||
.bold()
|
||||
case .notifications:
|
||||
Text("Notifications", comment: "Toolbar label for Notifications view.")
|
||||
.bold()
|
||||
case .search:
|
||||
Text("Universe 🛸", comment: "Toolbar label for the universal view where posts from all connected relay servers appear.")
|
||||
.bold()
|
||||
case .none:
|
||||
Text("", comment: "Toolbar label for unknown views. This label would be displayed only if a new timeline view is added but a toolbar label was not explicitly assigned to it yet.")
|
||||
}
|
||||
|
||||
var timelineNavItem: Text {
|
||||
switch selected_timeline {
|
||||
case .home:
|
||||
return Text("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.")
|
||||
.bold()
|
||||
case .dms:
|
||||
return Text("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
|
||||
.bold()
|
||||
case .notifications:
|
||||
return Text("Notifications", comment: "Toolbar label for Notifications view.")
|
||||
.bold()
|
||||
case .search:
|
||||
return Text("Universe 🛸", comment: "Toolbar label for the universal view where posts from all connected relay servers appear.")
|
||||
.bold()
|
||||
case .none:
|
||||
return Text(verbatim: "")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,24 +170,31 @@ struct ContentView: View {
|
||||
NavigationLink(destination: MaybeProfileView, isActive: $profile_open) {
|
||||
EmptyView()
|
||||
}
|
||||
NavigationLink(destination: MaybeThreadView, isActive: $thread_open) {
|
||||
EmptyView()
|
||||
if let active_event {
|
||||
let thread = ThreadModel(event: active_event, damus_state: damus_state!)
|
||||
NavigationLink(destination: ThreadView(state: damus_state!, thread: thread), isActive: $thread_open) {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
NavigationLink(destination: MaybeSearchView, isActive: $search_open) {
|
||||
EmptyView()
|
||||
}
|
||||
switch selected_timeline {
|
||||
case .search:
|
||||
SearchHomeView(damus_state: damus_state!, model: SearchHomeModel(damus_state: damus_state!))
|
||||
if #available(iOS 16.0, *) {
|
||||
SearchHomeView(damus_state: damus_state!, model: SearchHomeModel(damus_state: damus_state!))
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
SearchHomeView(damus_state: damus_state!, model: SearchHomeModel(damus_state: damus_state!))
|
||||
}
|
||||
|
||||
case .home:
|
||||
PostingTimelineView
|
||||
|
||||
case .notifications:
|
||||
VStack(spacing: 0) {
|
||||
Divider()
|
||||
TimelineView(events: home.notifications, loading: $home.loading, damus: damus, show_friend_icon: true, filter: { _ in true })
|
||||
}
|
||||
NotificationsView(state: damus, notifications: home.notifications)
|
||||
|
||||
case .dms:
|
||||
DirectMessagesView(damus_state: damus_state!)
|
||||
.environmentObject(home.dms)
|
||||
@@ -202,15 +203,25 @@ struct ContentView: View {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
.navigationBarTitle(selected_timeline == .home ? NSLocalizedString("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.") : NSLocalizedString("Universe 🛸", comment: "Navigation bar title for universal view where posts from all connected relay servers appear."), displayMode: .inline)
|
||||
.navigationBarTitle(timeline_name(selected_timeline), displayMode: .inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
timelineNavItem
|
||||
.opacity(isSideBarOpened ? 0 : 1)
|
||||
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
||||
VStack {
|
||||
if selected_timeline == .home {
|
||||
Image("damus-home")
|
||||
.resizable()
|
||||
.frame(width:30,height:30)
|
||||
.shadow(color: Color("DamusPurple"), radius: 2)
|
||||
.opacity(isSideBarOpened ? 0 : 1)
|
||||
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
||||
} else {
|
||||
timelineNavItem
|
||||
.opacity(isSideBarOpened ? 0 : 1)
|
||||
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea(.keyboard)
|
||||
}
|
||||
|
||||
var MaybeSearchView: some View {
|
||||
@@ -223,16 +234,6 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
|
||||
var MaybeThreadView: some View {
|
||||
Group {
|
||||
if let evid = self.active_event_id {
|
||||
BuildThreadV2View(damus: damus_state!, event_id: evid)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var MaybeProfileView: some View {
|
||||
Group {
|
||||
if let pk = self.active_profile {
|
||||
@@ -263,49 +264,47 @@ struct ContentView: View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
if let damus = self.damus_state {
|
||||
NavigationView {
|
||||
ZStack {
|
||||
TabView { // Prevents navbar appearance change on scroll
|
||||
MainContent(damus: damus)
|
||||
.toolbar() {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
isSideBarOpened.toggle()
|
||||
} label: {
|
||||
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles)
|
||||
.opacity(isSideBarOpened ? 0 : 1)
|
||||
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
||||
}
|
||||
.disabled(isSideBarOpened)
|
||||
TabView { // Prevents navbar appearance change on scroll
|
||||
MainContent(damus: damus)
|
||||
.toolbar() {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button {
|
||||
isSideBarOpened.toggle()
|
||||
} label: {
|
||||
ProfilePicView(pubkey: damus_state!.pubkey, size: 32, highlight: .none, profiles: damus_state!.profiles)
|
||||
.opacity(isSideBarOpened ? 0 : 1)
|
||||
.animation(isSideBarOpened ? .none : .default, value: isSideBarOpened)
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
HStack(alignment: .center) {
|
||||
if home.signal.signal != home.signal.max_signal {
|
||||
NavigationLink(destination: RelayConfigView(state: damus_state!)) {
|
||||
Text("\(home.signal.signal)/\(home.signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.disabled(isSideBarOpened)
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
HStack(alignment: .center) {
|
||||
if home.signal.signal != home.signal.max_signal {
|
||||
NavigationLink(destination: RelayConfigView(state: damus_state!)) {
|
||||
Text("\(home.signal.signal)/\(home.signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
|
||||
.font(.callout)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
|
||||
// maybe expand this to other timelines in the future
|
||||
if selected_timeline == .search {
|
||||
Button(action: {
|
||||
//isFilterVisible.toggle()
|
||||
self.active_sheet = .filter
|
||||
}) {
|
||||
// checklist, checklist.checked, lisdt.bullet, list.bullet.circle, line.3.horizontal.decrease..., line.3.horizontail.decrease
|
||||
Label(NSLocalizedString("Filter", comment: "Button label text for filtering relay servers."), systemImage: "line.3.horizontal.decrease")
|
||||
.foregroundColor(.gray)
|
||||
//.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
|
||||
// maybe expand this to other timelines in the future
|
||||
if selected_timeline == .search {
|
||||
Button(action: {
|
||||
//isFilterVisible.toggle()
|
||||
self.active_sheet = .filter
|
||||
}) {
|
||||
// checklist, checklist.checked, lisdt.bullet, list.bullet.circle, line.3.horizontal.decrease..., line.3.horizontail.decrease
|
||||
Label(NSLocalizedString("Filter", comment: "Button label text for filtering relay servers."), systemImage: "line.3.horizontal.decrease")
|
||||
.foregroundColor(.gray)
|
||||
//.contentShape(Rectangle())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||
}
|
||||
}
|
||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||
.overlay(
|
||||
SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened.animation())
|
||||
)
|
||||
@@ -314,8 +313,10 @@ struct ContentView: View {
|
||||
|
||||
TabBar(new_events: $home.new_events, selected: $selected_timeline, isSidebarVisible: $isSideBarOpened, action: switch_timeline)
|
||||
.padding([.bottom], 8)
|
||||
.background(Color(uiColor: .systemBackground).ignoresSafeArea())
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea(.keyboard)
|
||||
.onAppear() {
|
||||
self.connect()
|
||||
setup_notifications()
|
||||
@@ -352,7 +353,11 @@ struct ContentView: View {
|
||||
active_profile = ref.ref_id
|
||||
profile_open = true
|
||||
} else if ref.key == "e" {
|
||||
active_event_id = ref.ref_id
|
||||
find_event(state: damus_state!, evid: ref.ref_id, search_type: .event, find_from: nil) { ev in
|
||||
if let ev {
|
||||
active_event = ev
|
||||
}
|
||||
}
|
||||
thread_open = true
|
||||
}
|
||||
case .filter(let filt):
|
||||
@@ -364,12 +369,7 @@ struct ContentView: View {
|
||||
|
||||
}
|
||||
.onReceive(handle_notify(.boost)) { notif in
|
||||
guard let privkey = self.privkey else {
|
||||
return
|
||||
}
|
||||
let ev = notif.object as! NostrEvent
|
||||
let boost = make_boost_event(pubkey: pubkey, privkey: privkey, boosted: ev)
|
||||
self.damus_state?.pool.send(.event(boost))
|
||||
current_boost = (notif.object as? NostrEvent)
|
||||
}
|
||||
.onReceive(handle_notify(.open_thread)) { obj in
|
||||
//let ev = obj.object as! NostrEvent
|
||||
@@ -486,7 +486,7 @@ struct ContentView: View {
|
||||
}, message: {
|
||||
if let pubkey = self.blocking {
|
||||
let profile = damus_state!.profiles.lookup(id: pubkey)
|
||||
let name = Profile.displayName(profile: profile, pubkey: pubkey)
|
||||
let name = Profile.displayName(profile: profile, pubkey: pubkey).username
|
||||
Text("\(name) has been blocked", comment: "Alert message that informs a user was blocked.")
|
||||
} else {
|
||||
Text("User has been blocked", comment: "Alert message that informs a user was blocked.")
|
||||
@@ -554,12 +554,22 @@ struct ContentView: View {
|
||||
}, message: {
|
||||
if let pubkey = blocking {
|
||||
let profile = damus_state?.profiles.lookup(id: pubkey)
|
||||
let name = Profile.displayName(profile: profile, pubkey: pubkey)
|
||||
let name = Profile.displayName(profile: profile, pubkey: pubkey).username
|
||||
Text("Block \(name)?", comment: "Alert message prompt to ask if a user should be blocked.")
|
||||
} else {
|
||||
Text("Could not find user to block...", comment: "Alert message to indicate that the blocked user could not be found.")
|
||||
}
|
||||
})
|
||||
.alert(NSLocalizedString("Repost", comment: "Title of alert for confirming to repost a post."), isPresented: $current_boost.mappedToBool()) {
|
||||
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of reposting a post.")) {
|
||||
current_boost = nil
|
||||
}
|
||||
Button(NSLocalizedString("Repost", comment: "Button to confirm reposting a post.")) {
|
||||
self.damus_state?.pool.send(.event(current_boost!))
|
||||
}
|
||||
} message: {
|
||||
Text("Are you sure you want to repost this?", comment: "Alert message to ask if user wants to repost a post.")
|
||||
}
|
||||
}
|
||||
|
||||
func switch_timeline(_ timeline: Timeline) {
|
||||
@@ -615,7 +625,9 @@ struct ContentView: View {
|
||||
settings: UserSettingsStore(),
|
||||
relay_filters: relay_filters,
|
||||
relay_metadata: metadatas,
|
||||
drafts: Drafts()
|
||||
drafts: Drafts(),
|
||||
events: EventCache(),
|
||||
bookmarks: BookmarksManager(pubkey: pubkey)
|
||||
)
|
||||
home.damus_state = self.damus_state!
|
||||
|
||||
@@ -764,3 +776,68 @@ func setup_notifications() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func find_event(state: DamusState, evid: String, search_type: SearchType, find_from: [String]?, callback: @escaping (NostrEvent?) -> ()) {
|
||||
if let ev = state.events.lookup(evid) {
|
||||
callback(ev)
|
||||
return
|
||||
}
|
||||
|
||||
let subid = UUID().description
|
||||
|
||||
var has_event = false
|
||||
|
||||
var filter = search_type == .event ? NostrFilter.filter_ids([ evid ]) : NostrFilter.filter_authors([ evid ])
|
||||
|
||||
if search_type == .profile {
|
||||
filter.kinds = [0]
|
||||
}
|
||||
|
||||
filter.limit = 1
|
||||
var attempts = 0
|
||||
|
||||
state.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in
|
||||
guard case .nostr_event(let ev) = res else {
|
||||
return
|
||||
}
|
||||
|
||||
guard ev.subid == subid else {
|
||||
return
|
||||
}
|
||||
|
||||
switch ev {
|
||||
case .event(_, let ev):
|
||||
has_event = true
|
||||
callback(ev)
|
||||
state.pool.unsubscribe(sub_id: subid)
|
||||
case .eose:
|
||||
if !has_event {
|
||||
attempts += 1
|
||||
if attempts == state.pool.descriptors.count / 2 {
|
||||
callback(nil)
|
||||
}
|
||||
state.pool.unsubscribe(sub_id: subid, to: [relay_id])
|
||||
}
|
||||
case .notice(_):
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func timeline_name(_ timeline: Timeline?) -> String {
|
||||
guard let timeline else {
|
||||
return ""
|
||||
}
|
||||
switch timeline {
|
||||
case .home:
|
||||
return "Home"
|
||||
case .notifications:
|
||||
return "Notifications"
|
||||
case .search:
|
||||
return "Universe 🛸"
|
||||
case .dms:
|
||||
return "DMs"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,16 @@
|
||||
<string>nostr</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>io.damus</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>damus</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
|
||||
71
damus/Models/BookmarksManager.swift
Normal file
71
damus/Models/BookmarksManager.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// BookmarksManager.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Joel Klabo on 2/18/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
fileprivate func get_bookmarks_key(pubkey: String) -> String {
|
||||
pk_setting_key(pubkey, key: "bookmarks")
|
||||
}
|
||||
|
||||
func load_bookmarks(pubkey: String) -> [NostrEvent] {
|
||||
let key = get_bookmarks_key(pubkey: pubkey)
|
||||
return (UserDefaults.standard.stringArray(forKey: key) ?? []).compactMap {
|
||||
event_from_json(dat: $0)
|
||||
}
|
||||
}
|
||||
|
||||
func save_bookmarks(pubkey: String, current_value: [NostrEvent], value: [NostrEvent]) -> Bool {
|
||||
let uniq_bookmarks = Array(Set(value))
|
||||
|
||||
if uniq_bookmarks != current_value {
|
||||
let encoded = uniq_bookmarks.map(event_to_json)
|
||||
UserDefaults.standard.set(encoded, forKey: get_bookmarks_key(pubkey: pubkey))
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
class BookmarksManager: ObservableObject {
|
||||
|
||||
private let userDefaults = UserDefaults.standard
|
||||
private let pubkey: String
|
||||
|
||||
private var _bookmarks: [NostrEvent]
|
||||
var bookmarks: [NostrEvent] {
|
||||
get {
|
||||
return _bookmarks
|
||||
}
|
||||
set {
|
||||
if save_bookmarks(pubkey: pubkey, current_value: _bookmarks, value: newValue) {
|
||||
self._bookmarks = newValue
|
||||
self.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(pubkey: String) {
|
||||
self._bookmarks = load_bookmarks(pubkey: pubkey)
|
||||
self.pubkey = pubkey
|
||||
}
|
||||
|
||||
func isBookmarked(_ ev: NostrEvent) -> Bool {
|
||||
return bookmarks.contains(ev)
|
||||
}
|
||||
|
||||
func updateBookmark(_ ev: NostrEvent) {
|
||||
if isBookmarked(ev) {
|
||||
bookmarks = bookmarks.filter { $0 != ev }
|
||||
} else {
|
||||
bookmarks.append(ev)
|
||||
}
|
||||
}
|
||||
|
||||
func clearAll() {
|
||||
bookmarks = []
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,8 @@ struct DamusState {
|
||||
let relay_filters: RelayFilters
|
||||
let relay_metadata: RelayMetadatas
|
||||
let drafts: Drafts
|
||||
let events: EventCache
|
||||
let bookmarks: BookmarksManager
|
||||
|
||||
var pubkey: String {
|
||||
return keypair.pubkey
|
||||
@@ -32,9 +34,8 @@ struct DamusState {
|
||||
var is_privkey_user: Bool {
|
||||
keypair.privkey != nil
|
||||
}
|
||||
|
||||
|
||||
static var empty: DamusState {
|
||||
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts())
|
||||
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
import Foundation
|
||||
|
||||
class Drafts: ObservableObject {
|
||||
@Published var post: String = ""
|
||||
@Published var replies: [NostrEvent: String] = [:]
|
||||
@Published var post: NSMutableAttributedString = NSMutableAttributedString(string: "")
|
||||
@Published var replies: [NostrEvent: NSMutableAttributedString] = [:]
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ class EventsModel: ObservableObject {
|
||||
case .notice(_):
|
||||
break
|
||||
case .eose(_):
|
||||
load_profiles(profiles_subid: profiles_id, relay_id: relay_id, events: events, damus_state: state)
|
||||
load_profiles(profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,22 +50,18 @@ class HomeModel: ObservableObject {
|
||||
let profiles_subid = UUID().description
|
||||
|
||||
@Published var new_events: NewEventsBits = NewEventsBits()
|
||||
@Published var notifications: EventHolder
|
||||
@Published var notifications = NotificationsModel()
|
||||
@Published var dms: DirectMessagesModel
|
||||
@Published var events: EventHolder
|
||||
@Published var events = EventHolder()
|
||||
@Published var loading: Bool = false
|
||||
@Published var signal: SignalModel = SignalModel()
|
||||
|
||||
init() {
|
||||
self.events = EventHolder()
|
||||
self.notifications = EventHolder()
|
||||
self.damus_state = DamusState.empty
|
||||
self.dms = DirectMessagesModel(our_pubkey: "")
|
||||
}
|
||||
|
||||
init(damus_state: DamusState) {
|
||||
self.events = EventHolder()
|
||||
self.notifications = EventHolder()
|
||||
self.damus_state = damus_state
|
||||
self.dms = DirectMessagesModel(our_pubkey: damus_state.pubkey)
|
||||
self.setup_debouncer()
|
||||
@@ -129,36 +125,43 @@ class HomeModel: ObservableObject {
|
||||
handle_channel_meta(ev)
|
||||
case .zap:
|
||||
handle_zap_event(ev)
|
||||
case .zap_request:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func handle_zap_event_with_zapper(_ ev: NostrEvent, our_pubkey: String, zapper: String) {
|
||||
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper) else {
|
||||
func handle_zap_event_with_zapper(_ ev: NostrEvent, our_keypair: Keypair, zapper: String) {
|
||||
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.zaps.add_zap(zap: zap)
|
||||
|
||||
guard zap.target.pubkey == our_pubkey else {
|
||||
guard zap.target.pubkey == our_keypair.pubkey else {
|
||||
return
|
||||
}
|
||||
|
||||
if !notifications.insert(ev) {
|
||||
if !notifications.insert_zap(zap) {
|
||||
return
|
||||
}
|
||||
|
||||
handle_last_event(ev: ev, timeline: .notifications)
|
||||
|
||||
if handle_last_event(ev: ev, timeline: .notifications) && damus_state.settings.zap_vibration {
|
||||
// Generate zap vibration
|
||||
zap_vibrate(zap_amount: zap.invoice.amount)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
func handle_zap_event(_ ev: NostrEvent) {
|
||||
// These are zap notifications
|
||||
guard let ptag = event_tag(ev, name: "p") else {
|
||||
return
|
||||
}
|
||||
|
||||
let our_keypair = damus_state.keypair
|
||||
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) {
|
||||
handle_zap_event_with_zapper(ev, our_pubkey: damus_state.pubkey, zapper: local_zapper)
|
||||
handle_zap_event_with_zapper(ev, our_keypair: our_keypair, zapper: local_zapper)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -177,7 +180,7 @@ class HomeModel: ObservableObject {
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.damus_state.profiles.zappers[ptag] = zapper
|
||||
self.handle_zap_event_with_zapper(ev, our_pubkey: self.damus_state.pubkey, zapper: zapper)
|
||||
self.handle_zap_event_with_zapper(ev, our_keypair: our_keypair, zapper: zapper)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,7 +232,7 @@ class HomeModel: ObservableObject {
|
||||
guard inner_ev.is_valid else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if inner_ev.is_textlike {
|
||||
handle_text_event(sub_id: sub_id, ev)
|
||||
}
|
||||
@@ -255,12 +258,11 @@ class HomeModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
// CHECK SIGS ON THESE
|
||||
|
||||
switch damus_state.likes.add_event(ev, target: e.ref_id) {
|
||||
case .already_counted:
|
||||
break
|
||||
case .success(let n):
|
||||
handle_notification(ev: ev)
|
||||
let liked = Counted(event: ev, id: e.ref_id, total: n)
|
||||
notify(.liked, liked)
|
||||
notify(.update_stats, e.ref_id)
|
||||
@@ -320,9 +322,9 @@ class HomeModel: ObservableObject {
|
||||
if sub_id == dms_subid {
|
||||
var dms = dms.dms.flatMap { $0.1.events }
|
||||
dms.append(contentsOf: incoming_dms)
|
||||
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: dms, damus_state: damus_state)
|
||||
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(dms), damus_state: damus_state)
|
||||
} else if sub_id == notifications_subid {
|
||||
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: notifications.all_events, damus_state: damus_state)
|
||||
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_keys(notifications.uniq_pubkeys()), damus_state: damus_state)
|
||||
}
|
||||
|
||||
self.loading = false
|
||||
@@ -375,7 +377,6 @@ class HomeModel: ObservableObject {
|
||||
// TODO: separate likes?
|
||||
var home_filter = NostrFilter.filter_kinds([
|
||||
NostrKind.text.rawValue,
|
||||
NostrKind.chat.rawValue,
|
||||
NostrKind.like.rawValue,
|
||||
NostrKind.boost.rawValue,
|
||||
])
|
||||
@@ -385,13 +386,12 @@ class HomeModel: ObservableObject {
|
||||
|
||||
var notifications_filter = NostrFilter.filter_kinds([
|
||||
NostrKind.text.rawValue,
|
||||
NostrKind.chat.rawValue,
|
||||
NostrKind.like.rawValue,
|
||||
NostrKind.boost.rawValue,
|
||||
NostrKind.zap.rawValue,
|
||||
])
|
||||
notifications_filter.pubkeys = [damus_state.pubkey]
|
||||
notifications_filter.limit = 100
|
||||
notifications_filter.limit = 500
|
||||
|
||||
var home_filters = [home_filter]
|
||||
var notifications_filters = [notifications_filter]
|
||||
@@ -457,20 +457,38 @@ class HomeModel: ObservableObject {
|
||||
}
|
||||
|
||||
func handle_notification(ev: NostrEvent) {
|
||||
// don't show notifications from ourselves
|
||||
guard ev.pubkey != damus_state.pubkey else {
|
||||
return
|
||||
}
|
||||
|
||||
guard event_has_our_pubkey(ev, our_pubkey: self.damus_state.pubkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
if !notifications.insert(ev) {
|
||||
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.events.insert(ev)
|
||||
if let inner_ev = ev.inner_event {
|
||||
damus_state.events.insert(inner_ev)
|
||||
}
|
||||
|
||||
if !notifications.insert_event(ev) {
|
||||
return
|
||||
}
|
||||
|
||||
handle_last_event(ev: ev, timeline: .notifications)
|
||||
}
|
||||
|
||||
func handle_last_event(ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) {
|
||||
|
||||
@discardableResult
|
||||
func handle_last_event(ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) -> Bool {
|
||||
if let new_bits = handle_last_events(new_events: self.new_events, ev: ev, timeline: timeline, shouldNotify: shouldNotify) {
|
||||
new_events = new_bits
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,6 +502,8 @@ class HomeModel: ObservableObject {
|
||||
guard should_show_event(contacts: damus_state.contacts, ev: ev) else {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.events.insert(ev)
|
||||
|
||||
if sub_id == home_subid {
|
||||
insert_home_event(ev)
|
||||
@@ -895,3 +915,16 @@ func should_show_event(contacts: Contacts, ev: NostrEvent) -> Bool {
|
||||
return ev.should_show_event
|
||||
}
|
||||
|
||||
func zap_vibrate(zap_amount: Int64) {
|
||||
let sats = zap_amount / 1000
|
||||
var vibration_generator: UIImpactFeedbackGenerator
|
||||
if sats >= 10000 {
|
||||
vibration_generator = UIImpactFeedbackGenerator(style: .heavy)
|
||||
} else if sats >= 1000 {
|
||||
vibration_generator = UIImpactFeedbackGenerator(style: .medium)
|
||||
} else {
|
||||
vibration_generator = UIImpactFeedbackGenerator(style: .light)
|
||||
}
|
||||
vibration_generator.impactOccurred()
|
||||
}
|
||||
|
||||
|
||||
28
damus/Models/ImageUploadModel.swift
Normal file
28
damus/Models/ImageUploadModel.swift
Normal file
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// ImageUploadModel.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-03-16.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
|
||||
class ImageUploadModel: NSObject, URLSessionTaskDelegate, ObservableObject {
|
||||
@Published var progress: Double? = nil
|
||||
|
||||
func start(img: UIImage, uploader: ImageUploader) async -> ImageUploadResult {
|
||||
let res = await create_image_upload_request(imageToUpload: img, imageUploader: uploader, progress: self)
|
||||
DispatchQueue.main.async {
|
||||
self.progress = nil
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
|
||||
DispatchQueue.main.async {
|
||||
self.progress = Double(totalBytesSent) / Double(totalBytesExpectedToSend)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,6 +94,14 @@ enum Block {
|
||||
return nil
|
||||
}
|
||||
|
||||
var is_note_mention: Bool {
|
||||
guard case .mention(let mention) = self else {
|
||||
return false
|
||||
}
|
||||
|
||||
return mention.type == .event
|
||||
}
|
||||
|
||||
var is_mention: Bool {
|
||||
if case .mention = self {
|
||||
return true
|
||||
@@ -263,17 +271,19 @@ func format_msats_abbrev(_ msats: Int64) -> String {
|
||||
return formatter.string(from: sats) ?? sats.stringValue
|
||||
}
|
||||
|
||||
func format_msats(_ msat: Int64) -> String {
|
||||
func format_msats(_ msat: Int64, locale: Locale = Locale.current) -> String {
|
||||
let numberFormatter = NumberFormatter()
|
||||
numberFormatter.numberStyle = .decimal
|
||||
numberFormatter.minimumFractionDigits = 0
|
||||
numberFormatter.maximumFractionDigits = 3
|
||||
numberFormatter.roundingMode = .down
|
||||
numberFormatter.locale = locale
|
||||
|
||||
let sats = NSNumber(value: (Double(msat) / 1000.0))
|
||||
let formattedSats = numberFormatter.string(from: sats) ?? sats.stringValue
|
||||
|
||||
return String(format: Bundle.main.localizedString(forKey: "sats_count", value: nil, table: nil), sats.decimalValue as NSDecimalNumber, formattedSats)
|
||||
let format = localizedStringFormat(key: "sats_count", locale: locale)
|
||||
return String(format: format, locale: locale, sats.decimalValue as NSDecimalNumber, formattedSats)
|
||||
}
|
||||
|
||||
func convert_invoice_block(_ b: invoice_block) -> Block? {
|
||||
|
||||
32
damus/Models/Notifications/EventGroup.swift
Normal file
32
damus/Models/Notifications/EventGroup.swift
Normal file
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// ReactionGroup.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class EventGroup {
|
||||
var events: [NostrEvent]
|
||||
|
||||
var last_event_at: Int64 {
|
||||
guard let first = self.events.first else {
|
||||
return 0
|
||||
}
|
||||
|
||||
return first.created_at
|
||||
}
|
||||
|
||||
init() {
|
||||
self.events = []
|
||||
}
|
||||
|
||||
init(events: [NostrEvent]) {
|
||||
self.events = events
|
||||
}
|
||||
|
||||
func insert(_ ev: NostrEvent) -> Bool {
|
||||
return insert_uniq_sorted_event_created(events: &events, new_ev: ev)
|
||||
}
|
||||
}
|
||||
59
damus/Models/Notifications/ZapGroup.swift
Normal file
59
damus/Models/Notifications/ZapGroup.swift
Normal file
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// ZapGroup.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class ZapGroup {
|
||||
var zaps: [Zap]
|
||||
var msat_total: Int64
|
||||
var zappers: Set<String>
|
||||
|
||||
var last_event_at: Int64 {
|
||||
guard let first = zaps.first else {
|
||||
return 0
|
||||
}
|
||||
|
||||
return first.event.created_at
|
||||
}
|
||||
|
||||
func zap_requests() -> [NostrEvent] {
|
||||
zaps.map { z in
|
||||
if let priv = z.private_request {
|
||||
return priv
|
||||
} else {
|
||||
return z.request.ev
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(zaps: [Zap]) {
|
||||
self.zaps = zaps
|
||||
self.msat_total = 0
|
||||
self.zappers = Set()
|
||||
}
|
||||
|
||||
init() {
|
||||
self.zaps = []
|
||||
self.msat_total = 0
|
||||
self.zappers = Set()
|
||||
}
|
||||
|
||||
func insert(_ zap: Zap) -> Bool {
|
||||
if !insert_uniq_sorted_zap_by_created(zaps: &zaps, new_zap: zap) {
|
||||
return false
|
||||
}
|
||||
|
||||
msat_total += zap.invoice.amount
|
||||
|
||||
if !zappers.contains(zap.request.ev.pubkey) {
|
||||
zappers.insert(zap.request.ev.pubkey)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
320
damus/Models/NotificationsModel.swift
Normal file
320
damus/Models/NotificationsModel.swift
Normal file
@@ -0,0 +1,320 @@
|
||||
//
|
||||
// NotificationsModel.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-21.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum NotificationItem {
|
||||
case repost(String, EventGroup)
|
||||
case reaction(String, EventGroup)
|
||||
case profile_zap(ZapGroup)
|
||||
case event_zap(String, ZapGroup)
|
||||
case reply(NostrEvent)
|
||||
|
||||
var is_reply: NostrEvent? {
|
||||
if case .reply(let ev) = self {
|
||||
return ev
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var is_zap: ZapGroup? {
|
||||
switch self {
|
||||
case .profile_zap(let zapgrp):
|
||||
return zapgrp
|
||||
case .event_zap(_, let zapgrp):
|
||||
return zapgrp
|
||||
case .reaction:
|
||||
return nil
|
||||
case .reply:
|
||||
return nil
|
||||
case .repost:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .repost(let evid, _):
|
||||
return "repost_" + evid
|
||||
case .reaction(let evid, _):
|
||||
return "reaction_" + evid
|
||||
case .profile_zap:
|
||||
return "profile_zap"
|
||||
case .event_zap(let evid, _):
|
||||
return "event_zap_" + evid
|
||||
case .reply(let ev):
|
||||
return "reply_" + ev.id
|
||||
}
|
||||
}
|
||||
|
||||
var last_event_at: Int64 {
|
||||
switch self {
|
||||
case .reaction(_, let evgrp):
|
||||
return evgrp.last_event_at
|
||||
case .repost(_, let evgrp):
|
||||
return evgrp.last_event_at
|
||||
case .profile_zap(let zapgrp):
|
||||
return zapgrp.last_event_at
|
||||
case .event_zap(_, let zapgrp):
|
||||
return zapgrp.last_event_at
|
||||
case .reply(let reply):
|
||||
return reply.created_at
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NotificationsModel: ObservableObject, ScrollQueue {
|
||||
var incoming_zaps: [Zap]
|
||||
var incoming_events: [NostrEvent]
|
||||
var should_queue: Bool
|
||||
|
||||
// mappings from events to
|
||||
var zaps: [String: ZapGroup]
|
||||
var profile_zaps: ZapGroup
|
||||
var reactions: [String: EventGroup]
|
||||
var reposts: [String: EventGroup]
|
||||
var replies: [NostrEvent]
|
||||
var has_reply: Set<String>
|
||||
|
||||
@Published var notifications: [NotificationItem]
|
||||
|
||||
init() {
|
||||
self.zaps = [:]
|
||||
self.reactions = [:]
|
||||
self.reposts = [:]
|
||||
self.replies = []
|
||||
self.has_reply = Set()
|
||||
self.should_queue = true
|
||||
self.incoming_zaps = []
|
||||
self.incoming_events = []
|
||||
self.profile_zaps = ZapGroup()
|
||||
self.notifications = []
|
||||
}
|
||||
|
||||
func set_should_queue(_ val: Bool) {
|
||||
self.should_queue = val
|
||||
}
|
||||
|
||||
func uniq_pubkeys() -> [String] {
|
||||
var pks = Set<String>()
|
||||
|
||||
for ev in incoming_events {
|
||||
pks.insert(ev.pubkey)
|
||||
}
|
||||
|
||||
for grp in reposts {
|
||||
for ev in grp.value.events {
|
||||
pks.insert(ev.pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
for ev in replies {
|
||||
pks.insert(ev.pubkey)
|
||||
}
|
||||
|
||||
for zap in incoming_zaps {
|
||||
pks.insert(zap.request.ev.pubkey)
|
||||
}
|
||||
|
||||
return Array(pks)
|
||||
}
|
||||
|
||||
func build_notifications() -> [NotificationItem] {
|
||||
var notifs: [NotificationItem] = []
|
||||
|
||||
for el in zaps {
|
||||
let evid = el.key
|
||||
let zapgrp = el.value
|
||||
|
||||
let notif: NotificationItem = .event_zap(evid, zapgrp)
|
||||
notifs.append(notif)
|
||||
}
|
||||
|
||||
if !profile_zaps.zaps.isEmpty {
|
||||
notifs.append(.profile_zap(profile_zaps))
|
||||
}
|
||||
|
||||
for el in reposts {
|
||||
let evid = el.key
|
||||
let evgrp = el.value
|
||||
|
||||
notifs.append(.repost(evid, evgrp))
|
||||
}
|
||||
|
||||
for el in reactions {
|
||||
let evid = el.key
|
||||
let evgrp = el.value
|
||||
|
||||
notifs.append(.reaction(evid, evgrp))
|
||||
}
|
||||
|
||||
for reply in replies {
|
||||
notifs.append(.reply(reply))
|
||||
}
|
||||
|
||||
notifs.sort { $0.last_event_at > $1.last_event_at }
|
||||
return notifs
|
||||
}
|
||||
|
||||
|
||||
private func insert_repost(_ ev: NostrEvent) -> Bool {
|
||||
guard let reposted_ev = ev.inner_event else {
|
||||
return false
|
||||
}
|
||||
|
||||
let id = reposted_ev.id
|
||||
|
||||
if let evgrp = self.reposts[id] {
|
||||
return evgrp.insert(ev)
|
||||
} else {
|
||||
let evgrp = EventGroup()
|
||||
self.reposts[id] = evgrp
|
||||
return evgrp.insert(ev)
|
||||
}
|
||||
}
|
||||
|
||||
private func insert_text(_ ev: NostrEvent) -> Bool {
|
||||
guard !has_reply.contains(ev.id) else {
|
||||
return false
|
||||
}
|
||||
|
||||
has_reply.insert(ev.id)
|
||||
replies.append(ev)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private func insert_reaction(_ ev: NostrEvent) -> Bool {
|
||||
guard let ref_id = ev.referenced_ids.last else {
|
||||
return false
|
||||
}
|
||||
|
||||
let id = ref_id.id
|
||||
|
||||
if let evgrp = self.reactions[id] {
|
||||
return evgrp.insert(ev)
|
||||
} else {
|
||||
let evgrp = EventGroup()
|
||||
self.reactions[id] = evgrp
|
||||
return evgrp.insert(ev)
|
||||
}
|
||||
}
|
||||
|
||||
private func insert_event_immediate(_ ev: NostrEvent) -> Bool {
|
||||
if ev.known_kind == .boost {
|
||||
return insert_repost(ev)
|
||||
} else if ev.known_kind == .like {
|
||||
return insert_reaction(ev)
|
||||
} else if ev.known_kind == .text {
|
||||
return insert_text(ev)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private func insert_zap_immediate(_ zap: Zap) -> Bool {
|
||||
switch zap.target {
|
||||
case .note(let notezt):
|
||||
let id = notezt.note_id
|
||||
if let zapgrp = self.zaps[notezt.note_id] {
|
||||
return zapgrp.insert(zap)
|
||||
} else {
|
||||
let zapgrp = ZapGroup()
|
||||
self.zaps[id] = zapgrp
|
||||
return zapgrp.insert(zap)
|
||||
}
|
||||
|
||||
case .profile:
|
||||
return profile_zaps.insert(zap)
|
||||
}
|
||||
}
|
||||
|
||||
func insert_event(_ ev: NostrEvent) -> Bool {
|
||||
if should_queue {
|
||||
return insert_uniq_sorted_event_created(events: &incoming_events, new_ev: ev)
|
||||
}
|
||||
|
||||
if insert_event_immediate(ev) {
|
||||
self.notifications = build_notifications()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func insert_zap(_ zap: Zap) -> Bool {
|
||||
if should_queue {
|
||||
return insert_uniq_sorted_zap_by_created(zaps: &incoming_zaps, new_zap: zap)
|
||||
}
|
||||
|
||||
if insert_zap_immediate(zap) {
|
||||
self.notifications = build_notifications()
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func filter(_ isIncluded: (NostrEvent) -> Bool) {
|
||||
var changed = false
|
||||
var count = 0
|
||||
|
||||
count = incoming_events.count
|
||||
incoming_events = incoming_events.filter(isIncluded)
|
||||
changed = changed || incoming_events.count != count
|
||||
|
||||
count = profile_zaps.zaps.count
|
||||
profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request.ev) }
|
||||
changed = changed || profile_zaps.zaps.count != count
|
||||
|
||||
for el in reactions {
|
||||
count = el.value.events.count
|
||||
el.value.events = el.value.events.filter(isIncluded)
|
||||
changed = changed || el.value.events.count != count
|
||||
}
|
||||
|
||||
for el in reposts {
|
||||
count = el.value.events.count
|
||||
el.value.events = el.value.events.filter(isIncluded)
|
||||
changed = changed || el.value.events.count != count
|
||||
}
|
||||
|
||||
for el in zaps {
|
||||
count = el.value.zaps.count
|
||||
el.value.zaps = el.value.zaps.filter {
|
||||
isIncluded($0.request.ev)
|
||||
}
|
||||
changed = changed || el.value.zaps.count != count
|
||||
}
|
||||
|
||||
count = replies.count
|
||||
replies = replies.filter(isIncluded)
|
||||
changed = changed || replies.count != count
|
||||
|
||||
if changed {
|
||||
self.notifications = build_notifications()
|
||||
}
|
||||
}
|
||||
|
||||
func flush() -> Bool {
|
||||
var inserted = false
|
||||
|
||||
for zap in incoming_zaps {
|
||||
inserted = insert_zap_immediate(zap) || inserted
|
||||
}
|
||||
|
||||
for event in incoming_events {
|
||||
inserted = insert_event_immediate(event) || inserted
|
||||
}
|
||||
|
||||
if inserted {
|
||||
self.notifications = build_notifications()
|
||||
}
|
||||
|
||||
return inserted
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,12 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
@Published var contacts: NostrEvent? = nil
|
||||
@Published var following: Int = 0
|
||||
@Published var relays: [String: RelayInfo]? = nil
|
||||
@Published var progress: Int = 0
|
||||
|
||||
let pubkey: String
|
||||
let damus: DamusState
|
||||
|
||||
|
||||
var seen_event: Set<String> = Set()
|
||||
var sub_id = UUID().description
|
||||
var prof_subid = UUID().description
|
||||
@@ -127,15 +129,16 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
case .ws_event:
|
||||
return
|
||||
case .nostr_event(let resp):
|
||||
guard resp.subid == self.sub_id || resp.subid == self.prof_subid else {
|
||||
return
|
||||
}
|
||||
switch resp {
|
||||
case .event(let sid, let ev):
|
||||
if sid != self.sub_id && sid != self.prof_subid {
|
||||
return
|
||||
}
|
||||
case .event(_, let ev):
|
||||
add_event(ev)
|
||||
case .notice(let notice):
|
||||
notify(.notice, notice)
|
||||
case .eose:
|
||||
progress += 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,25 @@
|
||||
import Foundation
|
||||
|
||||
class ReplyMap {
|
||||
var replies: [String: String] = [:]
|
||||
var replies: [String: Set<String>] = [:]
|
||||
|
||||
func lookup(_ id: String) -> String? {
|
||||
func lookup(_ id: String) -> Set<String>? {
|
||||
return replies[id]
|
||||
}
|
||||
func add(id: String, reply_id: String) {
|
||||
replies[id] = reply_id
|
||||
|
||||
private func ensure_set(id: String) {
|
||||
if replies[id] == nil {
|
||||
replies[id] = Set()
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func add(id: String, reply_id: String) -> Bool {
|
||||
ensure_set(id: id)
|
||||
if (replies[id]!).contains(reply_id) {
|
||||
return false
|
||||
}
|
||||
replies[id]!.insert(reply_id)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
12
damus/Models/Search/SearchResultsModel.swift
Normal file
12
damus/Models/Search/SearchResultsModel.swift
Normal file
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// SearchResultsModel.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-03-03.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
class SearchResultsModel: ObservableObject {
|
||||
}
|
||||
@@ -76,7 +76,7 @@ class SearchHomeModel: ObservableObject {
|
||||
// global events are not realtime
|
||||
unsubscribe(to: relay_id)
|
||||
|
||||
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, events: events.all_events, damus_state: damus_state)
|
||||
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.all_events), damus_state: damus_state)
|
||||
}
|
||||
|
||||
|
||||
@@ -98,8 +98,31 @@ func find_profiles_to_fetch_pk(profiles: Profiles, event_pubkeys: [String]) -> [
|
||||
|
||||
return Array(pubkeys)
|
||||
}
|
||||
|
||||
func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad) -> [String] {
|
||||
switch load {
|
||||
case .from_events(let events):
|
||||
return find_profiles_to_fetch_from_events(profiles: profiles, events: events)
|
||||
case .from_keys(let pks):
|
||||
return find_profiles_to_fetch_from_keys(profiles: profiles, pks: pks)
|
||||
}
|
||||
}
|
||||
|
||||
func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [String]) -> [String] {
|
||||
var pubkeys = Set<String>()
|
||||
|
||||
func find_profiles_to_fetch(profiles: Profiles, events: [NostrEvent]) -> [String] {
|
||||
for pk in pks {
|
||||
if profiles.lookup(id: pk) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
pubkeys.insert(pk)
|
||||
}
|
||||
|
||||
return Array(pubkeys)
|
||||
}
|
||||
|
||||
func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent]) -> [String] {
|
||||
var pubkeys = Set<String>()
|
||||
|
||||
for ev in events {
|
||||
@@ -113,9 +136,14 @@ func find_profiles_to_fetch(profiles: Profiles, events: [NostrEvent]) -> [String
|
||||
return Array(pubkeys)
|
||||
}
|
||||
|
||||
func load_profiles(profiles_subid: String, relay_id: String, events: [NostrEvent], damus_state: DamusState) {
|
||||
enum PubkeysToLoad {
|
||||
case from_events([NostrEvent])
|
||||
case from_keys([String])
|
||||
}
|
||||
|
||||
func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad, damus_state: DamusState) {
|
||||
var filter = NostrFilter.filter_profiles
|
||||
let authors = find_profiles_to_fetch(profiles: damus_state.profiles, events: events)
|
||||
let authors = find_profiles_to_fetch(profiles: damus_state.profiles, load: load)
|
||||
filter.authors = authors
|
||||
|
||||
guard !authors.isEmpty else {
|
||||
|
||||
@@ -30,162 +30,100 @@ enum InitialEvent {
|
||||
|
||||
/// manages the lifetime of a thread
|
||||
class ThreadModel: ObservableObject {
|
||||
@Published var initial_event: InitialEvent
|
||||
@Published var events: [NostrEvent] = []
|
||||
@Published var event_map: [String: Int] = [:]
|
||||
@Published var event: NostrEvent
|
||||
var event_map: Set<NostrEvent>
|
||||
|
||||
@Published var loading: Bool = false
|
||||
|
||||
var replies: ReplyMap = ReplyMap()
|
||||
|
||||
var event: NostrEvent? {
|
||||
switch initial_event {
|
||||
case .event(let ev):
|
||||
return ev
|
||||
case .event_id(let evid):
|
||||
for event in events {
|
||||
if event.id == evid {
|
||||
return event
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
init(event: NostrEvent, damus_state: DamusState) {
|
||||
self.damus_state = damus_state
|
||||
self.event_map = Set()
|
||||
self.event = event
|
||||
add_event(event, privkey: nil)
|
||||
}
|
||||
|
||||
let damus_state: DamusState
|
||||
|
||||
let profiles_subid = UUID().description
|
||||
var base_subid = UUID().description
|
||||
|
||||
init(evid: String, damus_state: DamusState) {
|
||||
self.damus_state = damus_state
|
||||
self.initial_event = .event_id(evid)
|
||||
}
|
||||
let base_subid = UUID().description
|
||||
let meta_subid = UUID().description
|
||||
|
||||
init(event: NostrEvent, damus_state: DamusState) {
|
||||
self.damus_state = damus_state
|
||||
self.initial_event = .event(event)
|
||||
var subids: [String] {
|
||||
return [profiles_subid, base_subid, meta_subid]
|
||||
}
|
||||
|
||||
func unsubscribe() {
|
||||
self.damus_state.pool.remove_handler(sub_id: base_subid)
|
||||
self.damus_state.pool.remove_handler(sub_id: meta_subid)
|
||||
self.damus_state.pool.remove_handler(sub_id: profiles_subid)
|
||||
self.damus_state.pool.unsubscribe(sub_id: base_subid)
|
||||
print("unsubscribing from thread \(initial_event.id) with sub_id \(base_subid)")
|
||||
self.damus_state.pool.unsubscribe(sub_id: meta_subid)
|
||||
self.damus_state.pool.unsubscribe(sub_id: profiles_subid)
|
||||
print("unsubscribing from thread \(event.id) with sub_id \(base_subid)")
|
||||
}
|
||||
|
||||
func reset_events() {
|
||||
self.events.removeAll()
|
||||
self.event_map.removeAll()
|
||||
self.replies.replies.removeAll()
|
||||
}
|
||||
|
||||
func should_resubscribe(_ ev_b: NostrEvent) -> Bool {
|
||||
if self.events.count == 0 {
|
||||
return true
|
||||
}
|
||||
@discardableResult
|
||||
func set_active_event(_ ev: NostrEvent, privkey: String?) -> Bool {
|
||||
self.event = ev
|
||||
add_event(ev, privkey: privkey)
|
||||
|
||||
if ev_b.is_root_event() {
|
||||
return false
|
||||
}
|
||||
|
||||
// rough heuristic to save us from resubscribing all the time
|
||||
//return ev_b.count_ids() != self.event.count_ids()
|
||||
return true
|
||||
}
|
||||
|
||||
func set_active_event(_ ev: NostrEvent, privkey: String?) {
|
||||
if should_resubscribe(ev) {
|
||||
unsubscribe()
|
||||
self.initial_event = .event(ev)
|
||||
subscribe()
|
||||
} else {
|
||||
self.initial_event = .event(ev)
|
||||
if events.count == 0 {
|
||||
add_event(ev, privkey: privkey)
|
||||
}
|
||||
}
|
||||
//self.objectWillChange.send()
|
||||
return false
|
||||
}
|
||||
|
||||
func subscribe() {
|
||||
var meta_events = NostrFilter()
|
||||
var event_filter = NostrFilter()
|
||||
var ref_events = NostrFilter()
|
||||
var events_filter = NostrFilter()
|
||||
//var likes_filter = NostrFilter.filter_kinds(7])
|
||||
|
||||
// TODO: add referenced relays
|
||||
switch self.initial_event {
|
||||
case .event(let ev):
|
||||
ref_events.referenced_ids = ev.referenced_ids.map { $0.ref_id }
|
||||
ref_events.referenced_ids?.append(ev.id)
|
||||
ref_events.limit = 50
|
||||
events_filter.ids = ref_events.referenced_ids ?? []
|
||||
events_filter.limit = 100
|
||||
events_filter.ids?.append(ev.id)
|
||||
case .event_id(let evid):
|
||||
ref_events.referenced_ids = [evid]
|
||||
ref_events.limit = 50
|
||||
events_filter.ids = [evid]
|
||||
events_filter.limit = 100
|
||||
let thread_id = event.thread_id(privkey: nil)
|
||||
|
||||
ref_events.referenced_ids = [thread_id, event.id]
|
||||
ref_events.kinds = [1]
|
||||
ref_events.limit = 1000
|
||||
|
||||
event_filter.ids = [thread_id, event.id]
|
||||
|
||||
meta_events.referenced_ids = [event.id]
|
||||
meta_events.kinds = [9735, 1, 6, 7]
|
||||
meta_events.limit = 1000
|
||||
|
||||
/*
|
||||
if let last_ev = self.events.last {
|
||||
if last_ev.created_at <= Int64(Date().timeIntervalSince1970) {
|
||||
ref_events.since = last_ev.created_at
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
let base_filters = [event_filter, ref_events]
|
||||
let meta_filters = [meta_events]
|
||||
|
||||
//likes_filter.ids = ref_events.referenced_ids!
|
||||
|
||||
print("subscribing to thread \(initial_event.id) with sub_id \(base_subid)")
|
||||
damus_state.pool.register_handler(sub_id: base_subid, handler: handle_event)
|
||||
print("subscribing to thread \(event.id) with sub_id \(base_subid)")
|
||||
loading = true
|
||||
damus_state.pool.send(.subscribe(.init(filters: [ref_events, events_filter], sub_id: base_subid)))
|
||||
}
|
||||
|
||||
func lookup(_ event_id: String) -> NostrEvent? {
|
||||
if let i = event_map[event_id] {
|
||||
return events[i]
|
||||
}
|
||||
return nil
|
||||
damus_state.pool.subscribe(sub_id: base_subid, filters: base_filters, handler: handle_event)
|
||||
damus_state.pool.subscribe(sub_id: meta_subid, filters: meta_filters, handler: handle_event)
|
||||
}
|
||||
|
||||
func add_event(_ ev: NostrEvent, privkey: String?) {
|
||||
guard ev.should_show_event else {
|
||||
if event_map.contains(ev) {
|
||||
return
|
||||
}
|
||||
|
||||
if event_map[ev.id] != nil {
|
||||
return
|
||||
}
|
||||
let the_ev = damus_state.events.upsert(ev)
|
||||
damus_state.events.add_replies(ev: the_ev)
|
||||
|
||||
for reply in ev.direct_replies(privkey) {
|
||||
self.replies.add(id: ev.id, reply_id: reply.ref_id)
|
||||
}
|
||||
|
||||
if insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at < $1.created_at }) {
|
||||
objectWillChange.send()
|
||||
}
|
||||
//self.events.append(ev)
|
||||
//self.events = self.events.sorted { $0.created_at < $1.created_at }
|
||||
|
||||
var i: Int = 0
|
||||
for ev in events {
|
||||
self.event_map[ev.id] = i
|
||||
i += 1
|
||||
}
|
||||
|
||||
if let evid = self.initial_event.is_event_id {
|
||||
if ev.id == evid {
|
||||
// this should trigger a resubscribe...
|
||||
set_active_event(ev, privkey: privkey)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func handle_channel_meta(_ ev: NostrEvent) {
|
||||
guard let meta: ChatroomMetadata = decode_json(ev.content) else {
|
||||
return
|
||||
}
|
||||
|
||||
notify(.chatroom_meta, meta)
|
||||
event_map.insert(ev)
|
||||
objectWillChange.send()
|
||||
}
|
||||
|
||||
func handle_event(relay_id: String, ev: NostrConnectionEvent) {
|
||||
|
||||
let (sub_id, done) = handle_subid_event(pool: damus_state.pool, relay_id: relay_id, ev: ev) { sid, ev in
|
||||
guard sid == base_subid || sid == profiles_subid else {
|
||||
guard subids.contains(sid) else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -193,21 +131,19 @@ class ThreadModel: ObservableObject {
|
||||
process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
|
||||
} else if ev.is_textlike {
|
||||
self.add_event(ev, privkey: self.damus_state.keypair.privkey)
|
||||
} else if ev.known_kind == .channel_meta || ev.known_kind == .channel_create {
|
||||
handle_channel_meta(ev)
|
||||
}
|
||||
}
|
||||
|
||||
guard done && (sub_id == base_subid || sub_id == profiles_subid) else {
|
||||
guard done, let sub_id, subids.contains(sub_id) else {
|
||||
return
|
||||
}
|
||||
|
||||
if (events.contains { ev in ev.id == initial_event.id }) {
|
||||
if event_map.contains(event) {
|
||||
loading = false
|
||||
}
|
||||
|
||||
if sub_id == self.base_subid {
|
||||
load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, events: events, damus_state: damus_state)
|
||||
load_profiles(profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(Array(event_map)), damus_state: damus_state)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ enum TranslationService: String, CaseIterable, Identifiable {
|
||||
var model: Model {
|
||||
switch self {
|
||||
case .none:
|
||||
return .init(tag: self.rawValue, displayName: NSLocalizedString("None", comment: "Dropdown option for selecting no translation service."))
|
||||
return .init(tag: self.rawValue, displayName: NSLocalizedString("none_translation_service", value: "None", comment: "Dropdown option for selecting no translation service."))
|
||||
case .libretranslate:
|
||||
return .init(tag: self.rawValue, displayName: NSLocalizedString("LibreTranslate (Open Source)", comment: "Dropdown option for selecting LibreTranslate as the translation service."))
|
||||
case .deepl:
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import Foundation
|
||||
import Vault
|
||||
import UIKit
|
||||
|
||||
func should_show_wallet_selector(_ pubkey: String) -> Bool {
|
||||
return UserDefaults.standard.object(forKey: "show_wallet_selector") as? Bool ?? true
|
||||
@@ -34,6 +35,10 @@ func get_default_zap_amount(pubkey: String) -> Int? {
|
||||
return amt
|
||||
}
|
||||
|
||||
func should_disable_image_animation() -> Bool {
|
||||
return (UserDefaults.standard.object(forKey: "disable_animation") as? Bool)
|
||||
?? UIAccessibility.isReduceMotionEnabled
|
||||
}
|
||||
|
||||
func get_default_wallet(_ pubkey: String) -> Wallet {
|
||||
if let defaultWalletName = UserDefaults.standard.string(forKey: "default_wallet"),
|
||||
@@ -45,6 +50,15 @@ func get_default_wallet(_ pubkey: String) -> Wallet {
|
||||
}
|
||||
}
|
||||
|
||||
func get_image_uploader(_ pubkey: String) -> ImageUploader {
|
||||
if let defaultImageUploader = UserDefaults.standard.string(forKey: "default_image_uploader"),
|
||||
let defaultImageUploader = ImageUploader(rawValue: defaultImageUploader) {
|
||||
return defaultImageUploader
|
||||
} else {
|
||||
return .nostrBuild
|
||||
}
|
||||
}
|
||||
|
||||
private func get_translation_service(_ pubkey: String) -> TranslationService? {
|
||||
guard let translation_service = UserDefaults.standard.string(forKey: "translation_service") else {
|
||||
return nil
|
||||
@@ -83,6 +97,12 @@ class UserSettingsStore: ObservableObject {
|
||||
UserDefaults.standard.set(default_wallet.rawValue, forKey: "default_wallet")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var default_image_uploader: ImageUploader {
|
||||
didSet {
|
||||
UserDefaults.standard.set(default_image_uploader.rawValue, forKey: "default_image_uploader")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var show_wallet_selector: Bool {
|
||||
didSet {
|
||||
@@ -95,6 +115,30 @@ class UserSettingsStore: ObservableObject {
|
||||
UserDefaults.standard.set(left_handed, forKey: "left_handed")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var always_show_images: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(always_show_images, forKey: "always_show_images")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var zap_vibration: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(zap_vibration, forKey: "zap_vibration")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var auto_translate: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(auto_translate, forKey: "auto_translate")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var show_only_preferred_languages: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(show_only_preferred_languages, forKey: "show_only_preferred_languages")
|
||||
}
|
||||
}
|
||||
|
||||
@Published var translation_service: TranslationService {
|
||||
didSet {
|
||||
@@ -159,14 +203,27 @@ class UserSettingsStore: ObservableObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Published var disable_animation: Bool {
|
||||
didSet {
|
||||
UserDefaults.standard.set(disable_animation, forKey: "disable_animation")
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
// TODO: pubkey-scoped settings
|
||||
let pubkey = ""
|
||||
self.default_wallet = get_default_wallet(pubkey)
|
||||
show_wallet_selector = should_show_wallet_selector(pubkey)
|
||||
always_show_images = UserDefaults.standard.object(forKey: "always_show_images") as? Bool ?? false
|
||||
|
||||
default_image_uploader = get_image_uploader(pubkey)
|
||||
|
||||
left_handed = UserDefaults.standard.object(forKey: "left_handed") as? Bool ?? false
|
||||
zap_vibration = UserDefaults.standard.object(forKey: "zap_vibration") as? Bool ?? false
|
||||
disable_animation = should_disable_image_animation()
|
||||
auto_translate = UserDefaults.standard.object(forKey: "auto_translate") as? Bool ?? false
|
||||
show_only_preferred_languages = UserDefaults.standard.object(forKey: "show_only_preferred_languages") as? Bool ?? false
|
||||
|
||||
// Note from @tyiu:
|
||||
// Default translation service is disabled by default for now until we gain some confidence that it is working well in production.
|
||||
|
||||
@@ -13,6 +13,7 @@ class ZapsModel: ObservableObject {
|
||||
var zaps: [Zap]
|
||||
|
||||
let zaps_subid = UUID().description
|
||||
let profiles_subid = UUID().description
|
||||
|
||||
init(state: DamusState, target: ZapTarget) {
|
||||
self.state = state
|
||||
@@ -44,34 +45,39 @@ class ZapsModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
guard case .event(_, let ev) = resp else {
|
||||
return
|
||||
}
|
||||
|
||||
guard ev.kind == 9735 else {
|
||||
return
|
||||
}
|
||||
|
||||
if let zap = state.zaps.zaps[ev.id] {
|
||||
if insert_uniq_sorted_zap(zaps: &zaps, new_zap: zap) {
|
||||
objectWillChange.send()
|
||||
}
|
||||
} else {
|
||||
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
|
||||
switch resp {
|
||||
case .notice:
|
||||
break
|
||||
case .eose:
|
||||
let events = self.zaps.map { $0.request.ev }
|
||||
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state)
|
||||
case .event(_, let ev):
|
||||
guard ev.kind == 9735 else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper) else {
|
||||
return
|
||||
}
|
||||
|
||||
state.zaps.add_zap(zap: zap)
|
||||
|
||||
if insert_uniq_sorted_zap(zaps: &zaps, new_zap: zap) {
|
||||
objectWillChange.send()
|
||||
if let zap = state.zaps.zaps[ev.id] {
|
||||
if insert_uniq_sorted_zap_by_amount(zaps: &zaps, new_zap: zap) {
|
||||
objectWillChange.send()
|
||||
}
|
||||
} else {
|
||||
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: state.keypair.privkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
state.zaps.add_zap(zap: zap)
|
||||
|
||||
if insert_uniq_sorted_zap_by_amount(zaps: &zaps, new_zap: zap) {
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,9 +140,8 @@ struct Profile: Codable {
|
||||
try container.encode(value)
|
||||
}
|
||||
|
||||
static func displayName(profile: Profile?, pubkey: String) -> String {
|
||||
let pk = bech32_nopre_pubkey(pubkey) ?? pubkey
|
||||
return profile?.name ?? abbrev_pubkey(pk)
|
||||
static func displayName(profile: Profile?, pubkey: String) -> DisplayName {
|
||||
return parse_display_name(profile: profile, pubkey: pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import CommonCrypto
|
||||
import secp256k1
|
||||
import secp256k1_implementation
|
||||
import CryptoKit
|
||||
import NaturalLanguage
|
||||
|
||||
|
||||
|
||||
@@ -157,7 +158,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
||||
pubkey = refkey.ref_id
|
||||
}
|
||||
|
||||
let dec = decrypt_dm(key, pubkey: pubkey, content: self.content)
|
||||
let dec = decrypt_dm(key, pubkey: pubkey, content: self.content, encoding: .base64)
|
||||
self.decrypted_content = dec
|
||||
|
||||
return dec
|
||||
@@ -168,6 +169,9 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
||||
return decrypted(privkey: privkey) ?? "*failed to decrypt content*"
|
||||
}
|
||||
|
||||
return content
|
||||
|
||||
/*
|
||||
switch validity {
|
||||
case .ok:
|
||||
return content
|
||||
@@ -176,6 +180,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
||||
case .bad_sig:
|
||||
return content + "\n\n*WARNING: invalid signature, could be forged!*"
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
var description: String {
|
||||
@@ -211,6 +216,16 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func thread_id(privkey: String?) -> String {
|
||||
for ref in event_refs(privkey) {
|
||||
if let thread_id = ref.is_thread_id {
|
||||
return thread_id.ref_id
|
||||
}
|
||||
}
|
||||
|
||||
return self.id
|
||||
}
|
||||
|
||||
public func last_refid() -> ReferencedId? {
|
||||
var mlast: Int? = nil
|
||||
@@ -245,6 +260,25 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible, Equatable, Has
|
||||
return event_is_reply(self, privkey: privkey)
|
||||
}
|
||||
|
||||
func note_language(_ privkey: String?) -> String? {
|
||||
// Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the note is in
|
||||
// and filter on only the text portions of the content as URLs and hashtags confuse the language recognizer.
|
||||
let originalBlocks = blocks(privkey)
|
||||
let originalOnlyText = originalBlocks.compactMap { $0.is_text }.joined(separator: " ")
|
||||
|
||||
// Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate.
|
||||
let languageRecognizer = NLLanguageRecognizer()
|
||||
languageRecognizer.processString(originalOnlyText)
|
||||
|
||||
guard let locale = languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= 0.5 })?.key.rawValue else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Remove the variant component and just take the language part as translation services typically only supports the variant-less language.
|
||||
// Moreover, speakers of one variant can generally understand other variants.
|
||||
return localeToLanguage(locale)
|
||||
}
|
||||
|
||||
public var referenced_ids: [ReferencedId] {
|
||||
return get_referenced_ids(key: "e")
|
||||
}
|
||||
@@ -573,14 +607,115 @@ func zap_target_to_tags(_ target: ZapTarget) -> [[String]] {
|
||||
}
|
||||
}
|
||||
|
||||
func make_zap_request_event(pubkey: String, privkey: String, content: String, relays: [RelayDescriptor], target: ZapTarget) -> NostrEvent {
|
||||
func make_private_zap_request_event(identity: FullKeypair, enc_key: FullKeypair, target: ZapTarget, message: String) -> String? {
|
||||
// target tags must be the same as zap request target tags
|
||||
let tags = zap_target_to_tags(target)
|
||||
|
||||
let note = NostrEvent(content: message, pubkey: identity.pubkey, kind: 9733, tags: tags)
|
||||
note.id = calculate_event_id(ev: note)
|
||||
note.sig = sign_event(privkey: identity.privkey, ev: note)
|
||||
|
||||
guard let note_json = encode_json(note) else {
|
||||
return nil
|
||||
}
|
||||
return encrypt_message(message: note_json, privkey: enc_key.privkey, to_pk: target.pubkey, encoding: .bech32)
|
||||
}
|
||||
|
||||
func decrypt_private_zap(our_privkey: String, zapreq: NostrEvent, target: ZapTarget) -> NostrEvent? {
|
||||
guard let anon_tag = zapreq.tags.first(where: { t in t.count >= 2 && t[0] == "anon" }) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let enc_note = anon_tag[1]
|
||||
|
||||
var note = decrypt_note(our_privkey: our_privkey, their_pubkey: zapreq.pubkey, enc_note: enc_note, encoding: .bech32)
|
||||
|
||||
// check to see if the private note was from us
|
||||
if note == nil {
|
||||
guard let our_private_keypair = generate_private_keypair(our_privkey: our_privkey, id: target.id, created_at: zapreq.created_at) else{
|
||||
return nil
|
||||
}
|
||||
// use our private keypair and their pubkey to get the shared secret
|
||||
note = decrypt_note(our_privkey: our_private_keypair.privkey, their_pubkey: target.pubkey, enc_note: enc_note, encoding: .bech32)
|
||||
}
|
||||
|
||||
guard let note else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard note.kind == 9733 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let zr_etag = zapreq.referenced_ids.first
|
||||
let note_etag = note.referenced_ids.first
|
||||
|
||||
guard zr_etag == note_etag else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let zr_ptag = zapreq.referenced_pubkeys.first
|
||||
let note_ptag = note.referenced_pubkeys.first
|
||||
|
||||
guard let zr_ptag, let note_ptag, zr_ptag == note_ptag else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard validate_event(ev: note) == .ok else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return note
|
||||
}
|
||||
|
||||
func generate_private_keypair(our_privkey: String, id: String, created_at: Int64) -> FullKeypair? {
|
||||
let to_hash = our_privkey + id + String(created_at)
|
||||
guard let dat = to_hash.data(using: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
let privkey_bytes = sha256(dat)
|
||||
let privkey = hex_encode(privkey_bytes)
|
||||
guard let pubkey = privkey_to_pubkey(privkey: privkey) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return FullKeypair(pubkey: pubkey, privkey: privkey)
|
||||
}
|
||||
|
||||
func make_zap_request_event(keypair: FullKeypair, content: String, relays: [RelayDescriptor], target: ZapTarget, zap_type: ZapType) -> NostrEvent? {
|
||||
var tags = zap_target_to_tags(target)
|
||||
var relay_tag = ["relays"]
|
||||
relay_tag.append(contentsOf: relays.map { $0.url.absoluteString })
|
||||
tags.append(relay_tag)
|
||||
let ev = NostrEvent(content: content, pubkey: pubkey, kind: 9734, tags: tags)
|
||||
|
||||
var kp = keypair
|
||||
|
||||
let now = Int64(Date().timeIntervalSince1970)
|
||||
|
||||
var message = content
|
||||
switch zap_type {
|
||||
case .pub:
|
||||
break
|
||||
case .non_zap:
|
||||
break
|
||||
case .anon:
|
||||
tags.append(["anon"])
|
||||
kp = generate_new_keypair().to_full()!
|
||||
case .priv:
|
||||
guard let priv_kp = generate_private_keypair(our_privkey: keypair.privkey, id: target.id, created_at: now) else {
|
||||
return nil
|
||||
}
|
||||
kp = priv_kp
|
||||
guard let privreq = make_private_zap_request_event(identity: keypair, enc_key: kp, target: target, message: message) else {
|
||||
return nil
|
||||
}
|
||||
tags.append(["anon", privreq])
|
||||
message = ""
|
||||
}
|
||||
|
||||
let ev = NostrEvent(content: message, pubkey: kp.pubkey, kind: 9734, tags: tags, createdAt: now)
|
||||
ev.id = calculate_event_id(ev: ev)
|
||||
ev.sig = sign_event(privkey: privkey, ev: ev)
|
||||
ev.sig = sign_event(privkey: kp.privkey, ev: ev)
|
||||
return ev
|
||||
}
|
||||
|
||||
@@ -610,14 +745,14 @@ func event_to_json(ev: NostrEvent) -> String {
|
||||
return str
|
||||
}
|
||||
|
||||
func decrypt_dm(_ privkey: String?, pubkey: String, content: String) -> String? {
|
||||
func decrypt_dm(_ privkey: String?, pubkey: String, content: String, encoding: EncEncoding) -> String? {
|
||||
guard let privkey = privkey else {
|
||||
return nil
|
||||
}
|
||||
guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: pubkey) else {
|
||||
return nil
|
||||
}
|
||||
guard let dat = decode_dm_base64(content) else {
|
||||
guard let dat = (encoding == .base64 ? decode_dm_base64(content) : decode_dm_bech32(content)) else {
|
||||
return nil
|
||||
}
|
||||
guard let dat = aes_decrypt(data: dat.content, iv: dat.iv, shared_sec: shared_sec) else {
|
||||
@@ -626,6 +761,13 @@ func decrypt_dm(_ privkey: String?, pubkey: String, content: String) -> String?
|
||||
return String(data: dat, encoding: .utf8)
|
||||
}
|
||||
|
||||
func decrypt_note(our_privkey: String, their_pubkey: String, enc_note: String, encoding: EncEncoding) -> NostrEvent? {
|
||||
guard let dec = decrypt_dm(our_privkey, pubkey: their_pubkey, content: enc_note, encoding: encoding) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return decode_nostr_event_json(json: dec)
|
||||
}
|
||||
|
||||
func get_shared_secret(privkey: String, pubkey: String) -> [UInt8]? {
|
||||
guard let privkey_bytes = try? privkey.bytes else {
|
||||
@@ -671,6 +813,39 @@ struct DirectMessageBase64 {
|
||||
let iv: [UInt8]
|
||||
}
|
||||
|
||||
|
||||
|
||||
func encode_dm_bech32(content: [UInt8], iv: [UInt8]) -> String {
|
||||
let content_bech32 = bech32_encode(hrp: "pzap", content)
|
||||
let iv_bech32 = bech32_encode(hrp: "iv", iv)
|
||||
return content_bech32 + "_" + iv_bech32
|
||||
}
|
||||
|
||||
func decode_dm_bech32(_ all: String) -> DirectMessageBase64? {
|
||||
let parts = all.split(separator: "_")
|
||||
guard parts.count == 2 else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let content_bech32 = String(parts[0])
|
||||
let iv_bech32 = String(parts[1])
|
||||
|
||||
guard let content_tup = try? bech32_decode(content_bech32) else {
|
||||
return nil
|
||||
}
|
||||
guard let iv_tup = try? bech32_decode(iv_bech32) else {
|
||||
return nil
|
||||
}
|
||||
guard content_tup.hrp == "pzap" else {
|
||||
return nil
|
||||
}
|
||||
guard iv_tup.hrp == "iv" else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return DirectMessageBase64(content: content_tup.data.bytes, iv: iv_tup.data.bytes)
|
||||
}
|
||||
|
||||
func encode_dm_base64(content: [UInt8], iv: [UInt8]) -> String {
|
||||
let content_b64 = base64_encode(content)
|
||||
let iv_b64 = base64_encode(iv)
|
||||
@@ -835,7 +1010,7 @@ func first_eref_mention(ev: NostrEvent, privkey: String?) -> Mention? {
|
||||
extension [ReferencedId] {
|
||||
var pRefs: [ReferencedId] {
|
||||
get {
|
||||
self.filter { ref in
|
||||
Set(self).filter { ref in
|
||||
ref.key == "p"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,13 +37,17 @@ struct NostrFilter: Codable, Equatable {
|
||||
}
|
||||
|
||||
public static func filter_hashtag(_ htags: [String]) -> NostrFilter {
|
||||
return NostrFilter(ids: nil, kinds: nil, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil, hashtag: htags)
|
||||
return NostrFilter(ids: nil, kinds: nil, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil, hashtag: htags.map { $0.lowercased() })
|
||||
}
|
||||
|
||||
public static var filter_text: NostrFilter {
|
||||
return filter_kinds([1])
|
||||
}
|
||||
|
||||
|
||||
public static func filter_ids(_ ids: [String]) -> NostrFilter {
|
||||
return NostrFilter(ids: ids, kinds: nil, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil, hashtag: nil)
|
||||
}
|
||||
|
||||
public static var filter_profiles: NostrFilter {
|
||||
return filter_kinds([0])
|
||||
}
|
||||
|
||||
@@ -21,4 +21,5 @@ enum NostrKind: Int {
|
||||
case chat = 42
|
||||
case list = 30000
|
||||
case zap = 9735
|
||||
case zap_request = 9734
|
||||
}
|
||||
|
||||
@@ -127,6 +127,9 @@ func decode_nostr_uri(_ s: String) -> NostrLink? {
|
||||
var uri = s.replacingOccurrences(of: "nostr://", with: "")
|
||||
uri = uri.replacingOccurrences(of: "nostr:", with: "")
|
||||
|
||||
uri = uri.replacingOccurrences(of: "damus://", with: "")
|
||||
uri = uri.replacingOccurrences(of: "damus:", with: "")
|
||||
|
||||
let parts = uri.split(separator: ":")
|
||||
.reduce(into: Array<String>()) { acc, str in
|
||||
guard let decoded = str.removingPercentEncoding else {
|
||||
@@ -137,7 +140,7 @@ func decode_nostr_uri(_ s: String) -> NostrLink? {
|
||||
}
|
||||
|
||||
if tag_is_hashtag(parts) {
|
||||
return .filter(NostrFilter.filter_hashtag([parts[1].lowercased()]))
|
||||
return .filter(NostrFilter.filter_hashtag([parts[1]]))
|
||||
}
|
||||
|
||||
if let rid = tag_to_refid(parts) {
|
||||
|
||||
66
damus/Util/DisplayName.swift
Normal file
66
damus/Util/DisplayName.swift
Normal file
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// DisplayName.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-03-14.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
|
||||
struct BothNames {
|
||||
let username: String
|
||||
let display_name: String
|
||||
}
|
||||
|
||||
enum DisplayName {
|
||||
case both(BothNames)
|
||||
case one(String)
|
||||
|
||||
var display_name: String {
|
||||
switch self {
|
||||
case .one(let one):
|
||||
return one
|
||||
case .both(let b):
|
||||
return b.display_name
|
||||
}
|
||||
}
|
||||
|
||||
var username: String {
|
||||
switch self {
|
||||
case .one(let one):
|
||||
return one
|
||||
case .both(let b):
|
||||
return b.username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func parse_display_name(profile: Profile?, pubkey: String) -> DisplayName {
|
||||
if pubkey == "anon" {
|
||||
return .one("Anonymous")
|
||||
}
|
||||
|
||||
guard let profile else {
|
||||
return .one(abbrev_bech32_pubkey(pubkey: pubkey))
|
||||
}
|
||||
|
||||
let name = profile.name?.isEmpty == false ? profile.name : nil
|
||||
let disp_name = profile.display_name?.isEmpty == false ? profile.display_name : nil
|
||||
|
||||
if let name, let disp_name, name != disp_name {
|
||||
return .both(BothNames(username: name, display_name: disp_name))
|
||||
}
|
||||
|
||||
if let one = name ?? disp_name {
|
||||
return .one(one)
|
||||
}
|
||||
|
||||
return .one(abbrev_bech32_pubkey(pubkey: pubkey))
|
||||
}
|
||||
|
||||
func abbrev_bech32_pubkey(pubkey: String) -> String {
|
||||
let pk = bech32_nopre_pubkey(pubkey) ?? pubkey
|
||||
return abbrev_pubkey(pk)
|
||||
}
|
||||
92
damus/Util/EventCache.swift
Normal file
92
damus/Util/EventCache.swift
Normal file
@@ -0,0 +1,92 @@
|
||||
//
|
||||
// EventCache.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-21.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
class EventCache {
|
||||
private var events: [String: NostrEvent] = [:]
|
||||
private var replies = ReplyMap()
|
||||
private var cancellable: AnyCancellable?
|
||||
|
||||
//private var thread_latest: [String: Int64]
|
||||
|
||||
init() {
|
||||
cancellable = NotificationCenter.default.publisher(
|
||||
for: UIApplication.didReceiveMemoryWarningNotification
|
||||
).sink { [weak self] _ in
|
||||
self?.prune()
|
||||
}
|
||||
}
|
||||
|
||||
func parent_events(event: NostrEvent) -> [NostrEvent] {
|
||||
var parents: [NostrEvent] = []
|
||||
|
||||
var ev = event
|
||||
|
||||
while true {
|
||||
guard let direct_reply = ev.direct_replies(nil).first else {
|
||||
break
|
||||
}
|
||||
|
||||
guard let next_ev = lookup(direct_reply.ref_id), next_ev != ev else {
|
||||
break
|
||||
}
|
||||
|
||||
parents.append(next_ev)
|
||||
ev = next_ev
|
||||
}
|
||||
|
||||
return parents.reversed()
|
||||
}
|
||||
|
||||
func add_replies(ev: NostrEvent) {
|
||||
for reply in ev.direct_replies(nil) {
|
||||
replies.add(id: reply.ref_id, reply_id: ev.id)
|
||||
}
|
||||
}
|
||||
|
||||
func child_events(event: NostrEvent) -> [NostrEvent] {
|
||||
guard let xs = replies.lookup(event.id) else {
|
||||
return []
|
||||
}
|
||||
let evs: [NostrEvent] = xs.reduce(into: [], { evs, evid in
|
||||
guard let ev = self.lookup(evid) else {
|
||||
return
|
||||
}
|
||||
|
||||
evs.append(ev)
|
||||
}).sorted(by: { $0.created_at < $1.created_at })
|
||||
return evs
|
||||
}
|
||||
|
||||
func upsert(_ ev: NostrEvent) -> NostrEvent {
|
||||
if let found = lookup(ev.id) {
|
||||
return found
|
||||
}
|
||||
|
||||
insert(ev)
|
||||
return ev
|
||||
}
|
||||
|
||||
func lookup(_ evid: String) -> NostrEvent? {
|
||||
return events[evid]
|
||||
}
|
||||
|
||||
func insert(_ ev: NostrEvent) {
|
||||
guard events[ev.id] == nil else {
|
||||
return
|
||||
}
|
||||
events[ev.id] = ev
|
||||
}
|
||||
|
||||
private func prune() {
|
||||
events = [:]
|
||||
replies.replies = [:]
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,15 @@
|
||||
import Foundation
|
||||
|
||||
/// Used for holding back events until they're ready to be displayed
|
||||
class EventHolder: ObservableObject {
|
||||
class EventHolder: ObservableObject, ScrollQueue {
|
||||
private var has_event: Set<String>
|
||||
@Published var events: [NostrEvent]
|
||||
@Published var incoming: [NostrEvent]
|
||||
@Published var should_queue: Bool
|
||||
var should_queue: Bool
|
||||
|
||||
func set_should_queue(_ val: Bool) {
|
||||
self.should_queue = val
|
||||
}
|
||||
|
||||
var queued: Int {
|
||||
return incoming.count
|
||||
|
||||
49
damus/Util/Extensions/Binding+.swift
Normal file
49
damus/Util/Extensions/Binding+.swift
Normal file
@@ -0,0 +1,49 @@
|
||||
//
|
||||
// Binding+.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Oleg Abalonski on 3/5/23.
|
||||
// Ref: https://josephduffy.co.uk/posts/mapping-optional-binding-to-bool
|
||||
|
||||
import os.log
|
||||
import SwiftUI
|
||||
|
||||
extension Binding where Value == Bool {
|
||||
/// Creates a binding by mapping an optional value to a `Bool` that is
|
||||
/// `true` when the value is non-`nil` and `false` when the value is `nil`.
|
||||
///
|
||||
/// When the value of the produced binding is set to `false` the value
|
||||
/// of `bindingToOptional`'s `wrappedValue` is set to `nil`.
|
||||
///
|
||||
/// Setting the value of the produce binding to `true` does nothing and
|
||||
/// will log an error.
|
||||
///
|
||||
/// - parameter bindingToOptional: A `Binding` to an optional value, used to calculate the `wrappedValue`.
|
||||
public init<Wrapped>(mappedTo bindingToOptional: Binding<Wrapped?>) {
|
||||
self.init(
|
||||
get: { bindingToOptional.wrappedValue != nil },
|
||||
set: { newValue in
|
||||
if !newValue {
|
||||
bindingToOptional.wrappedValue = nil
|
||||
} else {
|
||||
os_log(
|
||||
.error,
|
||||
"Optional binding mapped to optional has been set to `true`, which will have no effect. Current value: %@",
|
||||
String(describing: bindingToOptional.wrappedValue)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Binding {
|
||||
/// Returns a binding by mapping this binding's value to a `Bool` that is
|
||||
/// `true` when the value is non-`nil` and `false` when the value is `nil`.
|
||||
///
|
||||
/// When the value of the produced binding is set to `false` this binding's value
|
||||
/// is set to `nil`.
|
||||
public func mappedToBool<Wrapped>() -> Binding<Bool> where Value == Wrapped? {
|
||||
return Binding<Bool>(mappedTo: self)
|
||||
}
|
||||
}
|
||||
@@ -14,19 +14,18 @@ extension KFOptionSetter {
|
||||
options.callbackQueue = .dispatch(.global(qos: .background))
|
||||
options.processingQueue = .dispatch(.global(qos: .background))
|
||||
options.downloader = CustomImageDownloader.shared
|
||||
options.backgroundDecode = true
|
||||
options.cacheOriginalImage = true
|
||||
options.scaleFactor = UIScreen.main.scale
|
||||
|
||||
options.processor = CustomImageProcessor(
|
||||
maxSize: imageContext.maxMebibyteSize(),
|
||||
downsampleSize: imageContext.downsampleSize()
|
||||
)
|
||||
|
||||
options.cacheSerializer = CustomCacheSerializer(
|
||||
maxSize: imageContext.maxMebibyteSize(),
|
||||
downsampleSize: imageContext.downsampleSize()
|
||||
)
|
||||
options.backgroundDecode = true
|
||||
options.cacheOriginalImage = true
|
||||
options.scaleFactor = UIScreen.main.scale
|
||||
options.onlyLoadFirstFrame = should_disable_image_animation()
|
||||
|
||||
return self
|
||||
}
|
||||
@@ -38,8 +38,7 @@ func insert_uniq_by_pubkey(events: inout [NostrEvent], new_ev: NostrEvent, cmp:
|
||||
return true
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool {
|
||||
func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap, cmp: (Zap, Zap) -> Bool) -> Bool {
|
||||
var i: Int = 0
|
||||
|
||||
for zap in zaps {
|
||||
@@ -48,7 +47,7 @@ func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if new_zap.invoice.amount > zap.invoice.amount {
|
||||
if cmp(new_zap, zap) {
|
||||
zaps.insert(new_zap, at: i)
|
||||
return true
|
||||
}
|
||||
@@ -59,6 +58,19 @@ func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func insert_uniq_sorted_zap_by_created(zaps: inout [Zap], new_zap: Zap) -> Bool {
|
||||
return insert_uniq_sorted_zap(zaps: &zaps, new_zap: new_zap) { (a, b) in
|
||||
a.event.created_at > b.event.created_at
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func insert_uniq_sorted_zap_by_amount(zaps: inout [Zap], new_zap: Zap) -> Bool {
|
||||
return insert_uniq_sorted_zap(zaps: &zaps, new_zap: new_zap) { (a, b) in
|
||||
a.invoice.amount > b.invoice.amount
|
||||
}
|
||||
}
|
||||
|
||||
func insert_uniq_sorted_event_created(events: inout [NostrEvent], new_ev: NostrEvent) -> Bool {
|
||||
return insert_uniq_sorted_event(events: &events, new_ev: new_ev) {
|
||||
|
||||
@@ -9,8 +9,10 @@ import Foundation
|
||||
|
||||
struct LNUrlPayRequest: Decodable {
|
||||
let allowsNostr: Bool?
|
||||
let commentAllowed: Int?
|
||||
let nostrPubkey: String?
|
||||
|
||||
let metadata: String?
|
||||
let minSendable: Int64?
|
||||
let maxSendable: Int64?
|
||||
let status: String?
|
||||
|
||||
@@ -10,10 +10,11 @@ import LinkPresentation
|
||||
|
||||
class CustomLinkView: LPLinkView {
|
||||
override var intrinsicContentSize: CGSize { CGSize(width: 0, height: super.intrinsicContentSize.height) }
|
||||
|
||||
}
|
||||
|
||||
enum Metadata {
|
||||
case linkmeta(LPLinkMetadata)
|
||||
case linkmeta(CachedMetadata)
|
||||
case url(URL)
|
||||
}
|
||||
|
||||
@@ -26,12 +27,19 @@ struct LinkViewRepresentable: UIViewRepresentable {
|
||||
func makeUIView(context: Context) -> CustomLinkView {
|
||||
switch meta {
|
||||
case .linkmeta(let linkmeta):
|
||||
return CustomLinkView(metadata: linkmeta)
|
||||
return CustomLinkView(metadata: linkmeta.meta)
|
||||
case .url(let url):
|
||||
return CustomLinkView(url: url)
|
||||
}
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: CustomLinkView, context: Context) {
|
||||
switch meta {
|
||||
case .linkmeta(let cached):
|
||||
cached.intrinsic_height = uiView.intrinsicContentSize.height
|
||||
case .url:
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
34
damus/Util/LocalizationUtil.swift
Normal file
34
damus/Util/LocalizationUtil.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// LocalizationUtil.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Terry Yiu on 2/24/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
func bundleForLocale(locale: Locale?) -> Bundle {
|
||||
if locale == nil {
|
||||
return Bundle.main
|
||||
}
|
||||
|
||||
let path = Bundle.main.path(forResource: locale!.identifier, ofType: "lproj")
|
||||
return path != nil ? (Bundle(path: path!) ?? Bundle.main) : Bundle.main
|
||||
}
|
||||
|
||||
func localizedStringFormat(key: String, locale: Locale?) -> String {
|
||||
let bundle = bundleForLocale(locale: locale)
|
||||
let fallback = bundleForLocale(locale: Locale(identifier: "en-US")).localizedString(forKey: key, value: nil, table: nil)
|
||||
return bundle.localizedString(forKey: key, value: fallback, table: nil)
|
||||
}
|
||||
|
||||
/**
|
||||
Removes the variant part of a locale code so that it contains only the language code.
|
||||
*/
|
||||
func localeToLanguage(_ locale: String) -> String? {
|
||||
if #available(iOS 16, *) {
|
||||
return Locale.LanguageCode(stringLiteral: locale).identifier(.alpha2)
|
||||
} else {
|
||||
return NSLocale(localeIdentifier: locale).languageCode
|
||||
}
|
||||
}
|
||||
@@ -101,6 +101,9 @@ extension Notification.Name {
|
||||
static var update_stats: Notification.Name {
|
||||
return Notification.Name("update_stats")
|
||||
}
|
||||
static var zapping: Notification.Name {
|
||||
return Notification.Name("zapping")
|
||||
}
|
||||
}
|
||||
|
||||
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {
|
||||
|
||||
8
damus/Util/PostBox.swift
Normal file
8
damus/Util/PostBox.swift
Normal file
@@ -0,0 +1,8 @@
|
||||
//
|
||||
// PostBox.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-03-20.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@@ -8,8 +8,18 @@
|
||||
import Foundation
|
||||
import LinkPresentation
|
||||
|
||||
class CachedMetadata {
|
||||
let meta: LPLinkMetadata
|
||||
var intrinsic_height: CGFloat?
|
||||
|
||||
init(meta: LPLinkMetadata) {
|
||||
self.meta = meta
|
||||
self.intrinsic_height = nil
|
||||
}
|
||||
}
|
||||
|
||||
enum Preview {
|
||||
case value(LinkViewRepresentable)
|
||||
case value(CachedMetadata)
|
||||
case failed
|
||||
}
|
||||
|
||||
@@ -20,12 +30,12 @@ class PreviewCache {
|
||||
return previews[evid]
|
||||
}
|
||||
|
||||
func store(evid: String, preview: LinkViewRepresentable?) {
|
||||
func store(evid: String, preview: LPLinkMetadata?) {
|
||||
switch preview {
|
||||
case .none:
|
||||
previews[evid] = .failed
|
||||
case .some(let meta):
|
||||
previews[evid] = .value(meta)
|
||||
previews[evid] = .value(CachedMetadata(meta: meta))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,5 +50,6 @@ public func time_ago_since(_ date: Date, _ calendar: Calendar = Calendar.current
|
||||
return formatter.string(from: DateComponents(calendar: calendar, second: second))!
|
||||
}
|
||||
|
||||
return NSLocalizedString("now", comment: "String indicating that a given timestamp just occurred")
|
||||
let bundle = bundleForLocale(locale: calendar.locale ?? Locale.current)
|
||||
return NSLocalizedString("now", bundle: bundle, comment: "String indicating that a given timestamp just occurred")
|
||||
}
|
||||
|
||||
@@ -7,12 +7,6 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ZapSource {
|
||||
case author(String)
|
||||
// TODO: anonymous
|
||||
//case anonymous
|
||||
}
|
||||
|
||||
public struct NoteZapTarget: Equatable {
|
||||
public let note_id: String
|
||||
public let author: String
|
||||
@@ -55,8 +49,10 @@ struct Zap {
|
||||
public let zapper: String /// zap authorizer
|
||||
public let target: ZapTarget
|
||||
public let request: ZapRequest
|
||||
public let is_anon: Bool
|
||||
public let private_request: NostrEvent?
|
||||
|
||||
public static func from_zap_event(zap_ev: NostrEvent, zapper: String) -> Zap? {
|
||||
public static func from_zap_event(zap_ev: NostrEvent, zapper: String, our_privkey: String?) -> Zap? {
|
||||
/// Make sure that we only create a zap event if it is authorized by the profile or event
|
||||
guard zapper == zap_ev.pubkey else {
|
||||
return nil
|
||||
@@ -83,14 +79,26 @@ struct Zap {
|
||||
guard let desc = get_zap_description(zap_ev, inv_desc: zap_invoice.description) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let zap_req = decode_nostr_event_json(desc) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard validate_event(ev: zap_req) == .ok else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let target = determine_zap_target(zap_req) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Zap(event: zap_ev, invoice: zap_invoice, zapper: zapper, target: target, request: ZapRequest(ev: zap_req))
|
||||
let private_request = our_privkey.flatMap {
|
||||
decrypt_private_zap(our_privkey: $0, zapreq: zap_req, target: target)
|
||||
}
|
||||
|
||||
let is_anon = private_request == nil && event_is_anonymous(ev: zap_req)
|
||||
|
||||
return Zap(event: zap_ev, invoice: zap_invoice, zapper: zapper, target: target, request: ZapRequest(ev: zap_req), is_anon: is_anon, private_request: private_request)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,7 +293,7 @@ func fetch_static_payreq(_ lnurl: String) async -> LNUrlPayRequest? {
|
||||
return endpoint
|
||||
}
|
||||
|
||||
func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int) async -> String? {
|
||||
func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent?, sats: Int, zap_type: ZapType, comment: String?) async -> String? {
|
||||
guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else {
|
||||
return nil
|
||||
}
|
||||
@@ -295,11 +303,15 @@ func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int)
|
||||
|
||||
var query = [URLQueryItem(name: "amount", value: "\(amount)")]
|
||||
|
||||
if zappable {
|
||||
if let json = encode_json(zapreq) {
|
||||
print("zapreq json: \(json)")
|
||||
query.append(URLQueryItem(name: "nostr", value: json))
|
||||
}
|
||||
if let zapreq, zappable && zap_type != .non_zap, let json = encode_json(zapreq) {
|
||||
print("zapreq json: \(json)")
|
||||
query.append(URLQueryItem(name: "nostr", value: json))
|
||||
}
|
||||
|
||||
// add a lud12 comment as well if we have it
|
||||
if zap_type != .priv, let comment, let limit = payreq.commentAllowed, limit != 0 {
|
||||
let limited_comment = String(comment.prefix(limit))
|
||||
query.append(URLQueryItem(name: "comment", value: limited_comment))
|
||||
}
|
||||
|
||||
base_url.queryItems = query
|
||||
@@ -310,7 +322,15 @@ func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int)
|
||||
|
||||
print("url \(url)")
|
||||
|
||||
guard let ret = try? await URLSession.shared.data(from: url) else {
|
||||
var ret: (Data, URLResponse)? = nil
|
||||
do {
|
||||
ret = try await URLSession.shared.data(from: url)
|
||||
} catch {
|
||||
print(error.localizedDescription)
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let ret else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class Zaps {
|
||||
if our_zaps[note_target.note_id] == nil {
|
||||
our_zaps[note_target.note_id] = [zap]
|
||||
} else {
|
||||
insert_uniq_sorted_zap(zaps: &(our_zaps[note_target.note_id]!), new_zap: zap)
|
||||
insert_uniq_sorted_zap_by_amount(zaps: &(our_zaps[note_target.note_id]!), new_zap: zap)
|
||||
}
|
||||
case .profile(_):
|
||||
break
|
||||
|
||||
@@ -26,11 +26,13 @@ struct EventActionBar: View {
|
||||
|
||||
// just used for previews
|
||||
@State var sheet: ActionBarSheet? = nil
|
||||
@State var confirm_boost: Bool = false
|
||||
@State var show_share_sheet: Bool = false
|
||||
@State var show_share_action: Bool = false
|
||||
|
||||
@ObservedObject var bar: ActionBarModel
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent, bar: ActionBarModel? = nil, test_lnurl: String? = nil) {
|
||||
self.damus_state = damus_state
|
||||
self.event = event
|
||||
@@ -56,8 +58,8 @@ struct EventActionBar: View {
|
||||
EventActionButton(img: "arrow.2.squarepath", col: bar.boosted ? Color.green : nil) {
|
||||
if bar.boosted {
|
||||
notify(.delete, bar.our_boost)
|
||||
} else if damus_state.is_privkey_user {
|
||||
self.confirm_boost = true
|
||||
} else {
|
||||
send_boost()
|
||||
}
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Boosts", comment: "Accessibility label for boosts button"))
|
||||
@@ -88,10 +90,23 @@ struct EventActionBar: View {
|
||||
|
||||
Spacer()
|
||||
EventActionButton(img: "square.and.arrow.up", col: Color.gray) {
|
||||
show_share_sheet = true
|
||||
show_share_action = true
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Share", comment: "Button to share a post"))
|
||||
}
|
||||
.sheet(isPresented: $show_share_action) {
|
||||
if #available(iOS 16.0, *) {
|
||||
ShareAction(event: event, bookmarks: damus_state.bookmarks, show_share_sheet: $show_share_sheet, show_share_action: $show_share_action)
|
||||
.presentationDetents([.height(300)])
|
||||
.presentationDragIndicator(.visible)
|
||||
} else {
|
||||
if let note_id = bech32_note_id(event.id) {
|
||||
if let url = URL(string: "https://damus.io/" + note_id) {
|
||||
ShareSheet(activityItems: [url])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $show_share_sheet) {
|
||||
if let note_id = bech32_note_id(event.id) {
|
||||
if let url = URL(string: "https://damus.io/" + note_id) {
|
||||
@@ -99,16 +114,6 @@ struct EventActionBar: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert(NSLocalizedString("Repost", comment: "Title of alert for confirming to repost a post."), isPresented: $confirm_boost) {
|
||||
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of reposting a post.")) {
|
||||
confirm_boost = false
|
||||
}
|
||||
Button(NSLocalizedString("Repost", comment: "Button to confirm reposting a post.")) {
|
||||
send_boost()
|
||||
}
|
||||
} message: {
|
||||
Text("Are you sure you want to repost this?", comment: "Alert message to ask if user wants to repost a post.")
|
||||
}
|
||||
.onReceive(handle_notify(.update_stats)) { n in
|
||||
let target = n.object as! String
|
||||
guard target == self.event.id else { return }
|
||||
@@ -135,7 +140,7 @@ struct EventActionBar: View {
|
||||
|
||||
self.bar.our_boost = boost
|
||||
|
||||
damus_state.pool.send(.event(boost))
|
||||
notify(.boost, boost)
|
||||
}
|
||||
|
||||
func send_like() {
|
||||
@@ -167,13 +172,41 @@ struct LikeButton: View {
|
||||
let action: () -> ()
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
// Following four are Shaka animation properties
|
||||
let timer = Timer.publish(every: 0.10, on: .main, in: .common).autoconnect()
|
||||
@State private var shouldAnimate = false
|
||||
@State private var rotationAngle = 0.0
|
||||
@State private var amountOfAngleIncrease: Double = 0.0
|
||||
|
||||
var body: some View {
|
||||
Button(action: action) {
|
||||
|
||||
Button(action: {
|
||||
withAnimation(Animation.easeOut(duration: 0.15)) {
|
||||
self.action()
|
||||
shouldAnimate = true
|
||||
amountOfAngleIncrease = 20.0
|
||||
}
|
||||
}) {
|
||||
Image(liked ? "shaka-full" : "shaka-line")
|
||||
.foregroundColor(liked ? .accentColor : .gray)
|
||||
}
|
||||
.accessibilityLabel(NSLocalizedString("Like", comment: "Accessibility Label for Like button"))
|
||||
.rotationEffect(Angle(degrees: shouldAnimate ? rotationAngle : 0))
|
||||
.onReceive(self.timer) { _ in
|
||||
// Shaka animation logic
|
||||
rotationAngle = amountOfAngleIncrease
|
||||
if amountOfAngleIncrease == 0 {
|
||||
timer.upstream.connect().cancel()
|
||||
return
|
||||
}
|
||||
amountOfAngleIncrease = -amountOfAngleIncrease
|
||||
if amountOfAngleIncrease < 0 {
|
||||
amountOfAngleIncrease += 2.5
|
||||
} else {
|
||||
amountOfAngleIncrease -= 2.5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,14 +26,16 @@ struct EventDetailBar: View {
|
||||
HStack {
|
||||
if bar.boosts > 0 {
|
||||
NavigationLink(destination: RepostsView(damus_state: state, model: RepostsModel(state: state, target: target))) {
|
||||
Text("\(Text(verbatim: "\(bar.boosts)").font(.body.bold())) \(Text(String(format: Bundle.main.localizedString(forKey: "reposts_count", value: nil, table: nil), bar.boosts)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.")
|
||||
let noun = Text(verbatim: "\(repostsCountString(bar.boosts))").foregroundColor(.gray)
|
||||
Text("\(Text("\(bar.boosts)").font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.")
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
|
||||
if bar.likes > 0 {
|
||||
NavigationLink(destination: ReactionsView(damus_state: state, model: ReactionsModel(state: state, target: target))) {
|
||||
Text("\(Text(verbatim: "\(bar.likes)").font(.body.bold())) \(Text(String(format: Bundle.main.localizedString(forKey: "reactions_count", value: nil, table: nil), bar.likes)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.")
|
||||
let noun = Text(verbatim: "\(reactionsCountString(bar.likes))").foregroundColor(.gray)
|
||||
Text("\(Text("\(bar.likes)").font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.")
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
@@ -41,7 +43,8 @@ struct EventDetailBar: View {
|
||||
if bar.zaps > 0 {
|
||||
let dst = ZapsView(state: state, target: .note(id: target, author: target_pk))
|
||||
NavigationLink(destination: dst) {
|
||||
Text("\(Text(verbatim: "\(bar.zaps)").font(.body.bold())) \(Text(String(format: Bundle.main.localizedString(forKey: "zaps_count", value: nil, table: nil), bar.zaps)).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.")
|
||||
let noun = Text(verbatim: "\(zapsCountString(bar.zaps))").foregroundColor(.gray)
|
||||
Text("\(Text("\(bar.zaps)").font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.")
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
@@ -49,6 +52,21 @@ struct EventDetailBar: View {
|
||||
}
|
||||
}
|
||||
|
||||
func repostsCountString(_ count: Int, locale: Locale = Locale.current) -> String {
|
||||
let format = localizedStringFormat(key: "reposts_count", locale: locale)
|
||||
return String(format: format, locale: locale, count)
|
||||
}
|
||||
|
||||
func reactionsCountString(_ count: Int, locale: Locale = Locale.current) -> String {
|
||||
let format = localizedStringFormat(key: "reactions_count", locale: locale)
|
||||
return String(format: format, locale: locale, count)
|
||||
}
|
||||
|
||||
func zapsCountString(_ count: Int, locale: Locale = Locale.current) -> String {
|
||||
let format = localizedStringFormat(key: "zaps_count", locale: locale)
|
||||
return String(format: format, locale: locale, count)
|
||||
}
|
||||
|
||||
struct EventDetailBar_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
EventDetailBar(state: test_damus_state(), target: "", target_pk: "")
|
||||
|
||||
110
damus/Views/ActionBar/ShareAction.swift
Normal file
110
damus/Views/ActionBar/ShareAction.swift
Normal file
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// ShareAction.swift
|
||||
// damus
|
||||
//
|
||||
// Created by eric on 3/8/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ShareAction: View {
|
||||
let event: NostrEvent
|
||||
let bookmarks: BookmarksManager
|
||||
@State private var isBookmarked: Bool = false
|
||||
|
||||
@Binding var show_share_sheet: Bool
|
||||
@Binding var show_share_action: Bool
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
init(event: NostrEvent, bookmarks: BookmarksManager, show_share_sheet: Binding<Bool>, show_share_action: Binding<Bool>) {
|
||||
let bookmarked = bookmarks.isBookmarked(event)
|
||||
self._isBookmarked = State(initialValue: bookmarked)
|
||||
|
||||
self.bookmarks = bookmarks
|
||||
self.event = event
|
||||
self._show_share_sheet = show_share_sheet
|
||||
self._show_share_action = show_share_action
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
let col = colorScheme == .light ? Color("DamusMediumGrey") : Color("DamusWhite")
|
||||
|
||||
VStack {
|
||||
Text("Share Note", comment: "Title text to indicate that the buttons below are meant to be used to share a note with others.")
|
||||
.padding()
|
||||
.font(.system(size: 17, weight: .bold))
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(alignment: .top, spacing: 25) {
|
||||
|
||||
ShareActionButton(img: "link", text: NSLocalizedString("Copy Link", comment: "Button to copy link to note"), col: col) {
|
||||
show_share_action = false
|
||||
UIPasteboard.general.string = "https://damus.io/" + (bech32_note_id(event.id) ?? event.id)
|
||||
}
|
||||
|
||||
let bookmarkImg = isBookmarked ? "bookmark.slash" : "bookmark"
|
||||
let bookmarkTxt = isBookmarked ? "Remove\nBookmark" : "Bookmark"
|
||||
let boomarkCol = isBookmarked ? Color(.red) : col
|
||||
ShareActionButton(img: bookmarkImg, text: NSLocalizedString(bookmarkTxt, comment: "Button to bookmark to note"), col: boomarkCol) {
|
||||
show_share_action = false
|
||||
self.bookmarks.updateBookmark(event)
|
||||
isBookmarked = self.bookmarks.isBookmarked(event)
|
||||
}
|
||||
|
||||
ShareActionButton(img: "globe", text: NSLocalizedString("Broadcast", comment: "Button to broadcast note to all your relays"), col: col) {
|
||||
show_share_action = false
|
||||
NotificationCenter.default.post(name: .broadcast_event, object: event)
|
||||
}
|
||||
|
||||
ShareActionButton(img: "square.and.arrow.up", text: NSLocalizedString("Share Via...", comment: "Button to present iOS share sheet"), col: col) {
|
||||
show_share_action = false
|
||||
show_share_sheet = true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
|
||||
Button(action: {
|
||||
show_share_action = false
|
||||
}) {
|
||||
Text(NSLocalizedString("Cancel", comment: "Button to cancel a repost."))
|
||||
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .center)
|
||||
.foregroundColor(colorScheme == .light ? Color("DamusBlack") : Color("DamusWhite"))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 24)
|
||||
.stroke(colorScheme == .light ? Color("DamusMediumGrey") : Color("DamusWhite"), lineWidth: 1)
|
||||
}
|
||||
.padding(EdgeInsets(top: 10, leading: 50, bottom: 25, trailing: 50))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ShareActionButton(img: String, text: String, col: Color, action: @escaping () -> ()) -> some View {
|
||||
Button(action: action) {
|
||||
VStack() {
|
||||
Image(systemName: img)
|
||||
.foregroundColor(col)
|
||||
.font(.system(size: 23, weight: .bold))
|
||||
.overlay {
|
||||
Circle()
|
||||
.stroke(col, lineWidth: 1)
|
||||
.frame(width: 55.0, height: 55.0)
|
||||
}
|
||||
.frame(height: 25)
|
||||
Text(verbatim: text)
|
||||
.foregroundColor(col)
|
||||
.font(.footnote)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.top)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
218
damus/Views/AttachMediaUtility.swift
Normal file
218
damus/Views/AttachMediaUtility.swift
Normal file
@@ -0,0 +1,218 @@
|
||||
//
|
||||
// AttachMediaUtility.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Swift on 2/17/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import CoreGraphics
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
enum ImageUploadResult {
|
||||
case success(String)
|
||||
case failed(Error?)
|
||||
}
|
||||
|
||||
fileprivate func create_upload_body(imageDataKey: Data, boundary: String, imageUploader: ImageUploader) -> Data {
|
||||
let body = NSMutableData();
|
||||
let contentType = "image/jpg"
|
||||
body.appendString(string: "Content-Type: multipart/form-data; boundary=\(boundary)\r\n\r\n")
|
||||
body.appendString(string: "--\(boundary)\r\n")
|
||||
body.appendString(string: "Content-Disposition: form-data; name=\(imageUploader.nameParam); filename=\"damus_generic_filename.jpg\"\r\n")
|
||||
body.appendString(string: "Content-Type: \(contentType)\r\n\r\n")
|
||||
body.append(imageDataKey as Data)
|
||||
body.appendString(string: "\r\n")
|
||||
body.appendString(string: "--\(boundary)--\r\n")
|
||||
return body as Data
|
||||
}
|
||||
|
||||
|
||||
func create_image_upload_request(imageToUpload: UIImage, imageUploader: ImageUploader, progress: URLSessionTaskDelegate) async -> ImageUploadResult {
|
||||
|
||||
guard let url = URL(string: imageUploader.postAPI) else {
|
||||
return .failed(nil)
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST";
|
||||
let boundary = "Boundary-\(UUID().description)"
|
||||
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
// otherwise convert to jpg
|
||||
guard let jpegData = imageToUpload.jpegData(compressionQuality: 0.8) else {
|
||||
// somehow failed, just return original
|
||||
return .failed(nil)
|
||||
}
|
||||
|
||||
request.httpBody = create_upload_body(imageDataKey: jpegData, boundary: boundary, imageUploader: imageUploader)
|
||||
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(for: request, delegate: progress)
|
||||
|
||||
guard let responseString = String(data: data, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue)) else {
|
||||
print("Upload failed getting response string")
|
||||
return .failed(nil)
|
||||
}
|
||||
|
||||
guard let url = imageUploader.getImageURL(from: responseString) else {
|
||||
print("Upload failed getting image url")
|
||||
return .failed(nil)
|
||||
}
|
||||
|
||||
return .success(url)
|
||||
|
||||
} catch {
|
||||
return .failed(error)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension PostView {
|
||||
struct ImagePicker: UIViewControllerRepresentable {
|
||||
|
||||
@Environment(\.presentationMode)
|
||||
private var presentationMode
|
||||
|
||||
let sourceType: UIImagePickerController.SourceType
|
||||
let onImagePicked: (UIImage) -> Void
|
||||
|
||||
final class Coordinator: NSObject,
|
||||
UINavigationControllerDelegate,
|
||||
UIImagePickerControllerDelegate {
|
||||
|
||||
@Binding
|
||||
private var presentationMode: PresentationMode
|
||||
private let sourceType: UIImagePickerController.SourceType
|
||||
private let onImagePicked: (UIImage) -> Void
|
||||
|
||||
init(presentationMode: Binding<PresentationMode>,
|
||||
sourceType: UIImagePickerController.SourceType,
|
||||
onImagePicked: @escaping (UIImage) -> Void) {
|
||||
_presentationMode = presentationMode
|
||||
self.sourceType = sourceType
|
||||
self.onImagePicked = onImagePicked
|
||||
}
|
||||
|
||||
func imagePickerController(_ picker: UIImagePickerController,
|
||||
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||
let uiImage = info[UIImagePickerController.InfoKey.originalImage] as! UIImage
|
||||
onImagePicked(uiImage)
|
||||
presentationMode.dismiss()
|
||||
|
||||
}
|
||||
|
||||
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||
presentationMode.dismiss()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
return Coordinator(presentationMode: presentationMode,
|
||||
sourceType: sourceType,
|
||||
onImagePicked: onImagePicked)
|
||||
}
|
||||
|
||||
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
|
||||
let picker = UIImagePickerController()
|
||||
picker.sourceType = sourceType
|
||||
picker.delegate = context.coordinator
|
||||
return picker
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIImagePickerController,
|
||||
context: UIViewControllerRepresentableContext<ImagePicker>) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NSMutableData {
|
||||
func appendString(string: String) {
|
||||
guard let data = string.data(using: String.Encoding.utf8, allowLossyConversion: true) else {
|
||||
return
|
||||
}
|
||||
append(data)
|
||||
}
|
||||
}
|
||||
|
||||
enum ImageUploader: String, CaseIterable, Identifiable {
|
||||
var id: String { self.rawValue }
|
||||
case nostrBuild
|
||||
case nostrImg
|
||||
|
||||
var nameParam: String {
|
||||
switch self {
|
||||
case .nostrBuild:
|
||||
return "\"fileToUpload\""
|
||||
case .nostrImg:
|
||||
return "\"image\""
|
||||
}
|
||||
}
|
||||
|
||||
var displayImageUploaderName: String {
|
||||
switch self {
|
||||
case .nostrBuild:
|
||||
return "NostrBuild"
|
||||
case .nostrImg:
|
||||
return "NostrImg"
|
||||
}
|
||||
}
|
||||
|
||||
struct Model: Identifiable, Hashable {
|
||||
var id: String { self.tag }
|
||||
var index: Int
|
||||
var tag: String
|
||||
var displayName : String
|
||||
}
|
||||
|
||||
var model: Model {
|
||||
switch self {
|
||||
case .nostrBuild:
|
||||
return .init(index: -1, tag: "nostrBuild", displayName: NSLocalizedString("NostrBuild", comment: "Dropdown option label for system default for NostrBuild image uploader."))
|
||||
case .nostrImg:
|
||||
return .init(index: 0, tag: "nostrImg", displayName: NSLocalizedString("NostrImg", comment: "Dropdown option label for system default for NostrImg image uploader."))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var postAPI: String {
|
||||
switch self {
|
||||
case .nostrBuild:
|
||||
return "https://nostr.build/upload.php"
|
||||
case .nostrImg:
|
||||
return "https://nostrimg.com/api/upload"
|
||||
}
|
||||
}
|
||||
|
||||
func getImageURL(from responseString: String) -> String? {
|
||||
switch self {
|
||||
case .nostrBuild:
|
||||
guard let startIndex = responseString.range(of: "nostr.build_")?.lowerBound else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let stringContainingName = responseString[startIndex..<responseString.endIndex]
|
||||
guard let endIndex = stringContainingName.range(of: "<")?.lowerBound else {
|
||||
return nil
|
||||
}
|
||||
let nostrBuildImageName = responseString[startIndex..<endIndex]
|
||||
let nostrBuildURL = "https://nostr.build/i/\(nostrBuildImageName)"
|
||||
return nostrBuildURL
|
||||
|
||||
case .nostrImg:
|
||||
guard let startIndex = responseString.range(of: "https://i.nostrimg.com/")?.lowerBound else {
|
||||
return nil
|
||||
}
|
||||
let stringContainingName = responseString[startIndex..<responseString.endIndex]
|
||||
guard let endIndex = stringContainingName.range(of: "\"")?.lowerBound else {
|
||||
return nil
|
||||
}
|
||||
let nostrBuildImageName = responseString[startIndex..<endIndex]
|
||||
let nostrBuildURL = "\(nostrBuildImageName)"
|
||||
return nostrBuildURL
|
||||
}
|
||||
}
|
||||
}
|
||||
61
damus/Views/BookmarksView.swift
Normal file
61
damus/Views/BookmarksView.swift
Normal file
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// BookmarksView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Joel Klabo on 2/18/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct BookmarksView: View {
|
||||
let state: DamusState
|
||||
private let noneFilter: (NostrEvent) -> Bool = { _ in true }
|
||||
private let bookmarksTitle = NSLocalizedString("Bookmarks", comment: "Title of bookmarks view")
|
||||
|
||||
@ObservedObject var manager: BookmarksManager
|
||||
|
||||
init(state: DamusState) {
|
||||
self.state = state
|
||||
self._manager = ObservedObject(initialValue: state.bookmarks)
|
||||
}
|
||||
|
||||
var bookmarks: [NostrEvent] {
|
||||
manager.bookmarks
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if bookmarks.isEmpty {
|
||||
VStack {
|
||||
Image(systemName: "bookmark")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 32.0, height: 32.0)
|
||||
Text(NSLocalizedString("You have no bookmarks yet, add them in the context menu", comment: "Text indicating that there are no bookmarks to be viewed"))
|
||||
}
|
||||
} else {
|
||||
ScrollView {
|
||||
InnerTimelineView(events: EventHolder(events: bookmarks, incoming: []), damus: state, show_friend_icon: true, filter: noneFilter)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationTitle(bookmarksTitle)
|
||||
.toolbar {
|
||||
if !bookmarks.isEmpty {
|
||||
Button(NSLocalizedString("Clear All", comment: "Button for clearing bookmarks data.")) {
|
||||
manager.clearAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
struct BookmarksView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
BookmarksView()
|
||||
}
|
||||
}
|
||||
*/
|
||||
@@ -35,14 +35,14 @@ struct ChatView: View {
|
||||
}
|
||||
|
||||
if let rep = thread.replies.lookup(event.id) {
|
||||
return rep == prev.id
|
||||
return rep.contains(prev.id)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
var is_active: Bool {
|
||||
return thread.initial_event.id == event.id
|
||||
return thread.event.id == event.id
|
||||
}
|
||||
|
||||
func prev_reply_is_same() -> String? {
|
||||
@@ -108,12 +108,13 @@ struct ChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
let show_images = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
|
||||
let show_images = should_show_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
|
||||
NoteContentView(damus_state: damus_state,
|
||||
event: event,
|
||||
show_images: show_images,
|
||||
size: .normal,
|
||||
artifacts: .just_content(event.content))
|
||||
artifacts: .just_content(event.content),
|
||||
options: [])
|
||||
|
||||
if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey {
|
||||
let bar = make_actionbar_model(ev: event.id, damus: damus_state)
|
||||
@@ -160,7 +161,7 @@ func prev_reply_is_same(event: NostrEvent, prev_ev: NostrEvent?, replies: ReplyM
|
||||
if let prev_reply_id = replies.lookup(prev.id) {
|
||||
if let cur_reply_id = replies.lookup(event.id) {
|
||||
if prev_reply_id != cur_reply_id {
|
||||
return cur_reply_id
|
||||
return cur_reply_id.first
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/*
|
||||
struct ChatroomView: View {
|
||||
@EnvironmentObject var thread: ThreadModel
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@@ -24,9 +25,9 @@ struct ChatroomView: View {
|
||||
next_ev: ind == count-1 ? nil : thread.events[ind+1],
|
||||
damus_state: damus
|
||||
)
|
||||
.event_context_menu(ev, keypair: damus.keypair, target_pubkey: ev.pubkey)
|
||||
.contextMenu{MenuItems(event: ev, keypair: damus.keypair, target_pubkey: ev.pubkey, bookmarks: damus.bookmarks)}
|
||||
.onTapGesture {
|
||||
if thread.initial_event.id == ev.id {
|
||||
if thread.event.id == ev.id {
|
||||
//dismiss()
|
||||
toggle_thread_view()
|
||||
} else {
|
||||
@@ -44,7 +45,7 @@ struct ChatroomView: View {
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .select_quote)) { notif in
|
||||
let ev = notif.object as! NostrEvent
|
||||
if ev.id != thread.initial_event.id {
|
||||
if ev.id != thread.event.id {
|
||||
thread.set_active_event(ev, privkey: damus.keypair.privkey)
|
||||
}
|
||||
scroll_to_event(scroller: scroller, id: ev.id, delay: 0, animate: true)
|
||||
@@ -57,7 +58,7 @@ struct ChatroomView: View {
|
||||
once = true
|
||||
}
|
||||
.onAppear() {
|
||||
scroll_to_event(scroller: scroller, id: thread.initial_event.id, delay: 0.1, animate: false)
|
||||
scroll_to_event(scroller: scroller, id: thread.event.id, delay: 0.1, animate: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,7 +77,9 @@ struct ChatroomView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let state = test_damus_state()
|
||||
ChatroomView(damus: state)
|
||||
.environmentObject(ThreadModel(evid: "&849ab9bb263ed2819db06e05f1a1a3b72878464e8c7146718a2fc1bf1912f893", damus_state: state))
|
||||
.environmentObject(ThreadModel(event: test_event, damus_state: state))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
@@ -12,8 +12,10 @@ import Combine
|
||||
|
||||
struct ConfigView: View {
|
||||
let state: DamusState
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State var confirm_logout: Bool = false
|
||||
@State var delete_account_warning: Bool = false
|
||||
@State var confirm_delete_account: Bool = false
|
||||
@State var show_privkey: Bool = false
|
||||
@State var has_authenticated_locally: Bool = false
|
||||
@@ -35,6 +37,10 @@ struct ConfigView: View {
|
||||
_privkey = State(initialValue: self.state.keypair.privkey_bech32 ?? "")
|
||||
_settings = ObservedObject(initialValue: state.settings)
|
||||
}
|
||||
|
||||
func textColor() -> Color {
|
||||
colorScheme == .light ? Color("DamusBlack") : Color("DamusWhite")
|
||||
}
|
||||
|
||||
func authenticateLocally(completion: @escaping (Bool) -> Void) {
|
||||
// Need to authenticate only once while ConfigView is presented
|
||||
@@ -66,12 +72,18 @@ struct ConfigView: View {
|
||||
self.pubkey_copied = is_pk
|
||||
generator.impactOccurred()
|
||||
}
|
||||
if has_authenticated_locally {
|
||||
if is_pk {
|
||||
// When trying to copy npub
|
||||
copyKey()
|
||||
} else {
|
||||
authenticateLocally { success in
|
||||
if success {
|
||||
copyKey()
|
||||
// When trying to copy nsec
|
||||
if has_authenticated_locally {
|
||||
copyKey()
|
||||
} else {
|
||||
authenticateLocally { success in
|
||||
if success {
|
||||
copyKey()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,30 +141,21 @@ struct ConfigView: View {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Section(NSLocalizedString("Default Zap Amount in sats", comment: "Section title for zap configuration")) {
|
||||
TextField(String("1000"), text: $default_zap_amount)
|
||||
.keyboardType(.numberPad)
|
||||
.onReceive(Just(default_zap_amount)) { newValue in
|
||||
let filtered = newValue.filter { Set("0123456789").contains($0) }
|
||||
|
||||
if filtered != newValue {
|
||||
default_zap_amount = filtered
|
||||
if let parsed = handle_string_amount(new_value: newValue) {
|
||||
self.default_zap_amount = String(parsed)
|
||||
set_default_zap_amount(pubkey: self.state.pubkey, amount: parsed)
|
||||
}
|
||||
|
||||
if filtered == "" {
|
||||
set_default_zap_amount(pubkey: state.pubkey, amount: 1000)
|
||||
return
|
||||
}
|
||||
|
||||
guard let amt = Int(filtered) else {
|
||||
return
|
||||
}
|
||||
set_default_zap_amount(pubkey: state.pubkey, amount: amt)
|
||||
}
|
||||
}
|
||||
|
||||
Section(NSLocalizedString("Translations", comment: "Section title for selecting the translation service.")) {
|
||||
Toggle(NSLocalizedString("Show only preferred languages on Universe feed", comment: "Toggle to show notes that are only in the device's preferred languages on the Universe feed and hide notes that are in other languages."), isOn: $settings.show_only_preferred_languages)
|
||||
.toggleStyle(.switch)
|
||||
|
||||
Picker(NSLocalizedString("Service", comment: "Prompt selection of translation service provider."), selection: $settings.translation_service) {
|
||||
ForEach(TranslationService.allCases, id: \.self) { server in
|
||||
Text(server.model.displayName)
|
||||
@@ -197,38 +200,82 @@ struct ConfigView: View {
|
||||
Link(NSLocalizedString("Get API Key", comment: "Button to navigate to DeepL website to get a translation API key."), destination: URL(string: "https://www.deepl.com/pro-api")!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen")) {
|
||||
Toggle(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen"), isOn: $settings.left_handed)
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
|
||||
Section(NSLocalizedString("Clear Cache", comment: "Section title for clearing cached data.")) {
|
||||
Button(NSLocalizedString("Clear", comment: "Button for clearing cached data.")) {
|
||||
KingfisherManager.shared.cache.clearMemoryCache()
|
||||
KingfisherManager.shared.cache.clearDiskCache()
|
||||
KingfisherManager.shared.cache.cleanExpiredDiskCache()
|
||||
if settings.translation_service != .none {
|
||||
Toggle(NSLocalizedString("Automatically translate notes", comment: "Toggle to automatically translate notes."), isOn: $settings.auto_translate)
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
}
|
||||
|
||||
Section(NSLocalizedString("Miscellaneous", comment: "Section header for miscellaneous user configuration")) {
|
||||
Toggle(NSLocalizedString("Left Handed", comment: "Moves the post button to the left side of the screen"), isOn: $settings.left_handed)
|
||||
.toggleStyle(.switch)
|
||||
Toggle(NSLocalizedString("Zap Vibration", comment: "Setting to enable vibration on zap"), isOn: $settings.zap_vibration)
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
|
||||
Section(NSLocalizedString("Images", comment: "Section title for images configuration.")) {
|
||||
Toggle(NSLocalizedString("Disable animations", comment: "Button to disable image animation"), isOn: $settings.disable_animation)
|
||||
.toggleStyle(.switch)
|
||||
.onChange(of: settings.disable_animation) { _ in
|
||||
clear_kingfisher_cache()
|
||||
}
|
||||
Toggle(NSLocalizedString("Always show images", comment: "Setting to always show and never blur images"), isOn: $settings.always_show_images)
|
||||
.toggleStyle(.switch)
|
||||
|
||||
Button(NSLocalizedString("Clear Cache", comment: "Button to clear image cache.")) {
|
||||
clear_kingfisher_cache()
|
||||
}
|
||||
|
||||
Picker(NSLocalizedString("Select image uploader", comment: "Prompt selection of user's image uploader"),
|
||||
selection: $settings.default_image_uploader) {
|
||||
ForEach(ImageUploader.allCases, id: \.self) { uploader in
|
||||
Text(uploader.model.displayName)
|
||||
.tag(uploader.model.tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(NSLocalizedString("Sign Out", comment: "Section title for signing out")) {
|
||||
Button(action: {
|
||||
if state.keypair.privkey == nil {
|
||||
notify(.logout, ())
|
||||
} else {
|
||||
confirm_logout = true
|
||||
}
|
||||
}, label: {
|
||||
Label(NSLocalizedString("Sign out", comment: "Sidebar menu label to sign out of the account."), systemImage: "pip.exit")
|
||||
.foregroundColor(textColor())
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
})
|
||||
}
|
||||
|
||||
if state.is_privkey_user {
|
||||
Section(NSLocalizedString("Delete", comment: "Section title for deleting the user")) {
|
||||
Section(NSLocalizedString("Permanently Delete Account", comment: "Section title for deleting the user")) {
|
||||
Button(NSLocalizedString("Delete Account", comment: "Button to delete the user's account."), role: .destructive) {
|
||||
confirm_delete_account = true
|
||||
delete_account_warning = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let bundleShortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String
|
||||
let bundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] as! String
|
||||
Section(NSLocalizedString("Version", comment: "Section title for displaying the version number of the Damus app.")) {
|
||||
Text(verbatim: "\(bundleShortVersion) (\(bundleVersion))")
|
||||
if let bundleShortVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"], let bundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] {
|
||||
Section(NSLocalizedString("Version", comment: "Section title for displaying the version number of the Damus app.")) {
|
||||
Text(verbatim: "\(bundleShortVersion) (\(bundleVersion))")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(NSLocalizedString("Settings", comment: "Navigation title for Settings view."))
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
.alert(NSLocalizedString("WARNING:\n\nTHIS WILL SIGN AN EVENT THAT DELETES THIS ACCOUNT.\n\nYOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.\n\n ARE YOU SURE YOU WANT TO CONTINUE?", comment: "Alert for deleting the users account."), isPresented: $delete_account_warning) {
|
||||
|
||||
Button(NSLocalizedString("Cancel", comment: "Cancel deleting the user."), role: .cancel) {
|
||||
delete_account_warning = false
|
||||
}
|
||||
Button(NSLocalizedString("Continue", comment: "Continue with deleting the user.")) {
|
||||
confirm_delete_account = true
|
||||
}
|
||||
}
|
||||
.alert(NSLocalizedString("Permanently Delete Account", comment: "Alert for deleting the users account."), isPresented: $confirm_delete_account) {
|
||||
TextField(NSLocalizedString("Type DELETE to delete", comment: "Text field prompt asking user to type the word DELETE to confirm that they want to proceed with deleting their account. The all caps lock DELETE word should not be translated. Everything else should."), text: $delete_text)
|
||||
Button(NSLocalizedString("Cancel", comment: "Cancel deleting the user."), role: .cancel) {
|
||||
@@ -346,3 +393,25 @@ struct ConfigView_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func handle_string_amount(new_value: String) -> Int? {
|
||||
let digits = Set("0123456789")
|
||||
let filtered = new_value.filter { digits.contains($0) }
|
||||
|
||||
if filtered == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let amt = Int(filtered) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return amt
|
||||
}
|
||||
|
||||
func clear_kingfisher_cache() -> Void {
|
||||
KingfisherManager.shared.cache.clearMemoryCache()
|
||||
KingfisherManager.shared.cache.clearDiskCache()
|
||||
KingfisherManager.shared.cache.cleanExpiredDiskCache()
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ struct DMChatView: View {
|
||||
VStack(alignment: .leading) {
|
||||
ForEach(Array(zip(dms.events, dms.events.indices)), id: \.0.id) { (ev, ind) in
|
||||
DMView(event: dms.events[ind], damus_state: damus_state)
|
||||
.event_context_menu(ev, keypair: damus_state.keypair, target_pubkey: ev.pubkey)
|
||||
.contextMenu{MenuItems(event: ev, keypair: damus_state.keypair, target_pubkey: ev.pubkey, bookmarks: damus_state.bookmarks)}
|
||||
}
|
||||
EndBlock(height: 80)
|
||||
}
|
||||
@@ -37,9 +37,7 @@ struct DMChatView: View {
|
||||
|
||||
var Header: some View {
|
||||
let profile = damus_state.profiles.lookup(id: pubkey)
|
||||
let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state)
|
||||
let fmodel = FollowersModel(damus_state: damus_state, target: pubkey)
|
||||
let profile_page = ProfileView(damus_state: damus_state, profile: pmodel, followers: fmodel)
|
||||
let profile_page = ProfileView(damus_state: damus_state, pubkey: pubkey)
|
||||
return NavigationLink(destination: profile_page) {
|
||||
HStack {
|
||||
ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles)
|
||||
@@ -183,13 +181,12 @@ struct DMChatView_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
enum EncEncoding {
|
||||
case base64
|
||||
case bech32
|
||||
}
|
||||
|
||||
func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keypair, created_at: Int64? = nil) -> NostrEvent?
|
||||
{
|
||||
guard let privkey = keypair.privkey else {
|
||||
return nil
|
||||
}
|
||||
|
||||
func encrypt_message(message: String, privkey: String, to_pk: String, encoding: EncEncoding = .base64) -> String? {
|
||||
let iv = random_bytes(count: 16).bytes
|
||||
guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: to_pk) else {
|
||||
return nil
|
||||
@@ -198,7 +195,26 @@ func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keyp
|
||||
guard let enc_message = aes_encrypt(data: utf8_message, iv: iv, shared_sec: shared_sec) else {
|
||||
return nil
|
||||
}
|
||||
let enc_content = encode_dm_base64(content: enc_message.bytes, iv: iv)
|
||||
|
||||
switch encoding {
|
||||
case .base64:
|
||||
return encode_dm_base64(content: enc_message.bytes, iv: iv)
|
||||
case .bech32:
|
||||
return encode_dm_bech32(content: enc_message.bytes, iv: iv)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func create_dm(_ message: String, to_pk: String, tags: [[String]], keypair: Keypair, created_at: Int64? = nil) -> NostrEvent?
|
||||
{
|
||||
guard let privkey = keypair.privkey else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let enc_content = encrypt_message(message: message, privkey: privkey, to_pk: to_pk) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let created = created_at ?? Int64(Date().timeIntervalSince1970)
|
||||
let ev = NostrEvent(content: enc_content, pubkey: keypair.pubkey, kind: 4, tags: tags, createdAt: created)
|
||||
|
||||
|
||||
@@ -21,14 +21,21 @@ struct DMView: View {
|
||||
Spacer(minLength: UIScreen.main.bounds.width * 0.2)
|
||||
}
|
||||
|
||||
let should_show_img = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
|
||||
let should_show_img = should_show_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
|
||||
|
||||
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: .normal, artifacts: .just_content(event.get_content(damus_state.keypair.privkey)))
|
||||
.foregroundColor(is_ours ? Color.white : Color.primary)
|
||||
.padding(10)
|
||||
.background(is_ours ? Color.accentColor : Color.secondary.opacity(0.15))
|
||||
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: .normal, artifacts: .just_content(event.get_content(damus_state.keypair.privkey)), options: [])
|
||||
.padding([.top, .leading, .trailing], 10)
|
||||
.padding([.bottom], 25)
|
||||
.background(VisualEffectView(effect: UIBlurEffect(style: .prominent))
|
||||
.background(is_ours ? Color.accentColor.opacity(0.9) : Color.secondary.opacity(0.15))
|
||||
)
|
||||
.cornerRadius(8.0)
|
||||
.tint(is_ours ? Color.white : Color.accentColor)
|
||||
.overlay(Text(format_relative_time(event.created_at))
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
.opacity(0.8)
|
||||
.offset(x: -10, y: -5), alignment: .bottomTrailing)
|
||||
if !is_ours {
|
||||
Spacer(minLength: UIScreen.main.bounds.width * 0.2)
|
||||
}
|
||||
|
||||
@@ -41,6 +41,9 @@ struct DirectMessagesView: View {
|
||||
ForEach(dms, id: \.0) { tup in
|
||||
MaybeEvent(tup)
|
||||
.padding(.top, 10)
|
||||
|
||||
Divider()
|
||||
.padding([.top], 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,8 @@ struct EditMetadataView: View {
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
@State var confirm_ln_address: Bool = false
|
||||
|
||||
init (damus_state: DamusState) {
|
||||
self.damus_state = damus_state
|
||||
@@ -103,6 +105,10 @@ struct EditMetadataView: View {
|
||||
damus_state.pool.send(.event(metadata_ev))
|
||||
}
|
||||
}
|
||||
|
||||
func is_ln_valid(ln: String) -> Bool {
|
||||
return ln.contains("@") || ln.lowercased().starts(with: "lnurl")
|
||||
}
|
||||
|
||||
var nip05_parts: NIP05? {
|
||||
return NIP05.parse(nip05)
|
||||
@@ -201,12 +207,22 @@ struct EditMetadataView: View {
|
||||
})
|
||||
|
||||
Button(NSLocalizedString("Save", comment: "Button for saving profile.")) {
|
||||
save()
|
||||
dismiss()
|
||||
if !ln.isEmpty && !is_ln_valid(ln: ln) {
|
||||
confirm_ln_address = true
|
||||
} else {
|
||||
save()
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
.alert(NSLocalizedString("Invalid Tip Address", comment: "Title of alerting as invalid tip address."), isPresented: $confirm_ln_address) {
|
||||
Button(NSLocalizedString("Ok", comment: "Button to dismiss the alert.")) {
|
||||
}
|
||||
} message: {
|
||||
Text("The address should either begin with LNURL or should look like an email address.", comment: "Giving the description of the alert message.")
|
||||
}
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.ignoresSafeArea(edges: .top)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,14 +16,14 @@ struct EventDetailView: View {
|
||||
|
||||
func scroll_after_load(thread: ThreadModel, proxy: ScrollViewProxy) {
|
||||
if !thread.loading {
|
||||
let id = thread.initial_event.id
|
||||
let id = thread.event.id
|
||||
scroll_to_event(scroller: proxy, id: id, delay: 0.1, animate: false)
|
||||
}
|
||||
}
|
||||
|
||||
struct EventDetailView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let state = test_damus_state()
|
||||
let _ = test_damus_state()
|
||||
EventDetailView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,29 +29,29 @@ func eventviewsize_to_font(_ size: EventViewKind) -> Font {
|
||||
|
||||
struct EventView: View {
|
||||
let event: NostrEvent
|
||||
let has_action_bar: Bool
|
||||
let options: EventViewOptions
|
||||
let damus: DamusState
|
||||
let pubkey: String
|
||||
|
||||
@EnvironmentObject var action_bar: ActionBarModel
|
||||
|
||||
init(damus: DamusState, event: NostrEvent, has_action_bar: Bool) {
|
||||
init(damus: DamusState, event: NostrEvent, options: EventViewOptions) {
|
||||
self.event = event
|
||||
self.has_action_bar = has_action_bar
|
||||
self.options = options
|
||||
self.damus = damus
|
||||
self.pubkey = event.pubkey
|
||||
}
|
||||
|
||||
init(damus: DamusState, event: NostrEvent) {
|
||||
self.event = event
|
||||
self.has_action_bar = false
|
||||
self.options = []
|
||||
self.damus = damus
|
||||
self.pubkey = event.pubkey
|
||||
}
|
||||
|
||||
init(damus: DamusState, event: NostrEvent, pubkey: String) {
|
||||
self.event = event
|
||||
self.has_action_bar = false
|
||||
self.options = [.no_action_bar]
|
||||
self.damus = damus
|
||||
self.pubkey = pubkey
|
||||
}
|
||||
@@ -60,19 +60,7 @@ struct EventView: View {
|
||||
VStack {
|
||||
if event.known_kind == .boost {
|
||||
if let inner_ev = event.inner_event {
|
||||
VStack(alignment: .leading) {
|
||||
let prof_model = ProfileModel(pubkey: event.pubkey, damus: damus)
|
||||
let follow_model = FollowersModel(damus_state: damus, target: event.pubkey)
|
||||
let prof = damus.profiles.lookup(id: event.pubkey)
|
||||
let booster_profile = ProfileView(damus_state: damus, profile: prof_model, followers: follow_model)
|
||||
|
||||
NavigationLink(destination: booster_profile) {
|
||||
Reposted(damus: damus, pubkey: event.pubkey, profile: prof)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
TextEvent(damus: damus, event: inner_ev, pubkey: inner_ev.pubkey, has_action_bar: has_action_bar, booster_pubkey: event.pubkey)
|
||||
.padding([.top], 1)
|
||||
}
|
||||
RepostedEvent(damus: damus, event: event, inner_ev: inner_ev, options: options)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
@@ -83,18 +71,19 @@ struct EventView: View {
|
||||
EmptyView()
|
||||
}
|
||||
} else {
|
||||
TextEvent(damus: damus, event: event, pubkey: pubkey, has_action_bar: has_action_bar, booster_pubkey: nil)
|
||||
.padding([.top], 6)
|
||||
TextEvent(damus: damus, event: event, pubkey: pubkey, options: options)
|
||||
//.padding([.top], 6)
|
||||
}
|
||||
|
||||
Divider()
|
||||
.padding([.top], 4)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// blame the porn bots for this code
|
||||
func should_show_images(contacts: Contacts, ev: NostrEvent, our_pubkey: String, booster_pubkey: String? = nil) -> Bool {
|
||||
func should_show_images(settings: UserSettingsStore, contacts: Contacts, ev: NostrEvent, our_pubkey: String, booster_pubkey: String? = nil) -> Bool {
|
||||
if settings.always_show_images {
|
||||
return true
|
||||
}
|
||||
|
||||
if ev.pubkey == our_pubkey {
|
||||
return true
|
||||
}
|
||||
@@ -131,9 +120,9 @@ extension View {
|
||||
}
|
||||
}
|
||||
|
||||
func event_context_menu(_ event: NostrEvent, keypair: Keypair, target_pubkey: String) -> some View {
|
||||
func event_context_menu(_ event: NostrEvent, keypair: Keypair, target_pubkey: String, bookmarks: BookmarksManager) -> some View {
|
||||
return self.contextMenu {
|
||||
EventMenuContext(event: event, keypair: keypair, target_pubkey: target_pubkey)
|
||||
EventMenuContext(event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -181,11 +170,7 @@ struct EventView_Previews: PreviewProvider {
|
||||
EventView(damus: test_damus_state(), event: NostrEvent(content: "hello there https://jb55.com/s/Oct12-150217.png https://jb55.com/red-me.jb55 cool", pubkey: "pk"), show_friend_icon: true, size: .big)
|
||||
|
||||
*/
|
||||
EventView(
|
||||
damus: test_damus_state(),
|
||||
event: test_event,
|
||||
has_action_bar: true
|
||||
)
|
||||
EventView( damus: test_damus_state(), event: test_event )
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
@@ -13,6 +13,19 @@ struct BuilderEventView: View {
|
||||
@State var event: NostrEvent?
|
||||
@State var subscription_uuid: String = UUID().description
|
||||
|
||||
init(damus: DamusState, event: NostrEvent) {
|
||||
_event = State(initialValue: event)
|
||||
self.damus = damus
|
||||
self.event_id = event.id
|
||||
}
|
||||
|
||||
init(damus: DamusState, event_id: String) {
|
||||
let event = damus.events.lookup(event_id)
|
||||
self.event_id = event_id
|
||||
self.damus = damus
|
||||
_event = State(initialValue: event)
|
||||
}
|
||||
|
||||
func unsubscribe() {
|
||||
damus.pool.unsubscribe(sub_id: subscription_uuid)
|
||||
}
|
||||
@@ -62,7 +75,9 @@ struct BuilderEventView: View {
|
||||
VStack {
|
||||
if let event = event {
|
||||
let ev = event.inner_event ?? event
|
||||
NavigationLink(destination: BuildThreadV2View(damus: damus, event_id: ev.id)) {
|
||||
let thread = ThreadModel(event: ev, damus_state: damus)
|
||||
let dest = ThreadView(state: damus, thread: thread)
|
||||
NavigationLink(destination: dest) {
|
||||
EmbeddedEventView(damus_state: damus, event: event)
|
||||
.padding(8)
|
||||
}.buttonStyle(.plain)
|
||||
@@ -71,9 +86,14 @@ struct BuilderEventView: View {
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
.cornerRadius(8)
|
||||
.border(Color.gray.opacity(0.2), width: 1)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(Color.gray.opacity(0.2), lineWidth: 1.0)
|
||||
)
|
||||
.onAppear {
|
||||
guard event == nil else {
|
||||
return
|
||||
}
|
||||
self.load()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,20 @@ struct EmbeddedEventView: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
let profile = damus_state.profiles.lookup(id: pubkey)
|
||||
HStack {
|
||||
EventProfile(damus_state: damus_state, pubkey: pubkey, profile: profile, size: .small)
|
||||
|
||||
Spacer()
|
||||
|
||||
EventMenuContext(event: event, keypair: damus_state.keypair, target_pubkey: event.pubkey, bookmarks: damus_state.bookmarks)
|
||||
.padding([.bottom], 4)
|
||||
|
||||
}
|
||||
.minimumScaleFactor(0.75)
|
||||
.lineLimit(1)
|
||||
|
||||
EventProfile(damus_state: damus_state, pubkey: pubkey, profile: profile, size: .small)
|
||||
|
||||
EventBody(damus_state: damus_state, event: event, size: .small)
|
||||
EventBody(damus_state: damus_state, event: event, size: .small, options: [.truncate_content])
|
||||
}
|
||||
.event_context_menu(event, keypair: damus_state.keypair, target_pubkey: pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,25 +11,29 @@ struct EventBody: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
let size: EventViewKind
|
||||
let should_show_img: Bool
|
||||
let options: EventViewOptions
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind, should_show_img: Bool? = nil, options: EventViewOptions) {
|
||||
self.damus_state = damus_state
|
||||
self.event = event
|
||||
self.size = size
|
||||
self.options = options
|
||||
self.should_show_img = should_show_img ?? should_show_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
|
||||
}
|
||||
|
||||
var content: String {
|
||||
event.get_content(damus_state.keypair.privkey)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if event_is_reply(event, privkey: damus_state.keypair.privkey) {
|
||||
ReplyDescription(event: event, profiles: damus_state.profiles)
|
||||
}
|
||||
|
||||
let should_show_img = should_show_images(contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey, booster_pubkey: nil)
|
||||
|
||||
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: size, artifacts: .just_content(content))
|
||||
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: size, artifacts: .just_content(content), options: options)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
struct EventBody_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
EventBody(damus_state: test_damus_state(), event: test_event, size: .normal)
|
||||
EventBody(damus_state: test_damus_state(), event: test_event, size: .normal, options: [])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,52 +11,101 @@ struct EventMenuContext: View {
|
||||
let event: NostrEvent
|
||||
let keypair: Keypair
|
||||
let target_pubkey: String
|
||||
let bookmarks: BookmarksManager
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var body: some View {
|
||||
|
||||
Button {
|
||||
UIPasteboard.general.string = event.get_content(keypair.privkey)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Copy Text", comment: "Context menu option for copying the text from an note."), systemImage: "doc.on.doc")
|
||||
}
|
||||
HStack {
|
||||
Menu {
|
||||
|
||||
Button {
|
||||
UIPasteboard.general.string = bech32_pubkey(target_pubkey)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Copy User Pubkey", comment: "Context menu option for copying the ID of the user who created the note."), systemImage: "person")
|
||||
}
|
||||
|
||||
Button {
|
||||
UIPasteboard.general.string = bech32_note_id(event.id) ?? event.id
|
||||
} label: {
|
||||
Label(NSLocalizedString("Copy Note ID", comment: "Context menu option for copying the ID of the note."), systemImage: "note.text")
|
||||
}
|
||||
|
||||
Button {
|
||||
UIPasteboard.general.string = event_to_json(ev: event)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Copy Note JSON", comment: "Context menu option for copying the JSON text from the note."), systemImage: "square.on.square")
|
||||
}
|
||||
|
||||
Button {
|
||||
NotificationCenter.default.post(name: .broadcast_event, object: event)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Broadcast", comment: "Context menu option for broadcasting the user's note to all of the user's connected relay servers."), systemImage: "globe")
|
||||
}
|
||||
|
||||
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
|
||||
if keypair.pubkey != target_pubkey && keypair.privkey != nil {
|
||||
Button(role: .destructive) {
|
||||
let target: ReportTarget = .note(ReportNoteTarget(pubkey: target_pubkey, note_id: event.id))
|
||||
notify(.report, target)
|
||||
MenuItems(event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks)
|
||||
|
||||
} label: {
|
||||
Label(NSLocalizedString("Report", comment: "Context menu option for reporting content."), systemImage: "exclamationmark.bubble")
|
||||
Label("", systemImage: "ellipsis")
|
||||
.foregroundColor(Color.gray)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {}
|
||||
}
|
||||
}
|
||||
|
||||
struct MenuItems: View {
|
||||
let event: NostrEvent
|
||||
let keypair: Keypair
|
||||
let target_pubkey: String
|
||||
let bookmarks: BookmarksManager
|
||||
|
||||
@State private var isBookmarked: Bool = false
|
||||
|
||||
init(event: NostrEvent, keypair: Keypair, target_pubkey: String, bookmarks: BookmarksManager) {
|
||||
let bookmarked = bookmarks.isBookmarked(event)
|
||||
self._isBookmarked = State(initialValue: bookmarked)
|
||||
|
||||
self.bookmarks = bookmarks
|
||||
self.event = event
|
||||
self.keypair = keypair
|
||||
self.target_pubkey = target_pubkey
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
Group {
|
||||
Button {
|
||||
UIPasteboard.general.string = event.get_content(keypair.privkey)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Copy Text", comment: "Context menu option for copying the text from an note."), systemImage: "doc.on.doc")
|
||||
}
|
||||
|
||||
Button(role: .destructive) {
|
||||
notify(.block, target_pubkey)
|
||||
Button {
|
||||
UIPasteboard.general.string = bech32_pubkey(target_pubkey)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Block", comment: "Context menu option for blocking users."), systemImage: "exclamationmark.octagon")
|
||||
Label(NSLocalizedString("Copy User Pubkey", comment: "Context menu option for copying the ID of the user who created the note."), systemImage: "person")
|
||||
}
|
||||
|
||||
Button {
|
||||
UIPasteboard.general.string = bech32_note_id(event.id) ?? event.id
|
||||
} label: {
|
||||
Label(NSLocalizedString("Copy Note ID", comment: "Context menu option for copying the ID of the note."), systemImage: "note.text")
|
||||
}
|
||||
|
||||
Button {
|
||||
UIPasteboard.general.string = event_to_json(ev: event)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Copy Note JSON", comment: "Context menu option for copying the JSON text from the note."), systemImage: "square.on.square")
|
||||
}
|
||||
|
||||
Button {
|
||||
self.bookmarks.updateBookmark(event)
|
||||
isBookmarked = self.bookmarks.isBookmarked(event)
|
||||
} label: {
|
||||
let imageName = isBookmarked ? "bookmark.fill" : "bookmark"
|
||||
let removeBookmarkString = NSLocalizedString("Remove Bookmark", comment: "Context menu option for removing a note bookmark.")
|
||||
let addBookmarkString = NSLocalizedString("Add Bookmark", comment: "Context menu option for adding a note bookmark.")
|
||||
Label(isBookmarked ? removeBookmarkString : addBookmarkString, systemImage: imageName)
|
||||
}
|
||||
|
||||
Button {
|
||||
NotificationCenter.default.post(name: .broadcast_event, object: event)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Broadcast", comment: "Context menu option for broadcasting the user's note to all of the user's connected relay servers."), systemImage: "globe")
|
||||
}
|
||||
|
||||
// Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile.
|
||||
if keypair.pubkey != target_pubkey && keypair.privkey != nil {
|
||||
Button(role: .destructive) {
|
||||
let target: ReportTarget = .note(ReportNoteTarget(pubkey: target_pubkey, note_id: event.id))
|
||||
notify(.report, target)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Report", comment: "Context menu option for reporting content."), systemImage: "exclamationmark.bubble")
|
||||
}
|
||||
|
||||
Button(role: .destructive) {
|
||||
notify(.block, target_pubkey)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Block", comment: "Context menu option for blocking users."), systemImage: "exclamationmark.octagon")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +31,7 @@ struct EventProfile: View {
|
||||
var body: some View {
|
||||
HStack(alignment: .center) {
|
||||
VStack {
|
||||
let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state)
|
||||
let pv = ProfileView(damus_state: damus_state, profile: pmodel, followers: FollowersModel(damus_state: damus_state, target: pubkey))
|
||||
|
||||
NavigationLink(destination: pv) {
|
||||
NavigationLink(destination: ProfileView(damus_state: damus_state, pubkey: pubkey)) {
|
||||
ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,18 +13,14 @@ struct MutedEventView: View {
|
||||
let scroller: ScrollViewProxy?
|
||||
|
||||
let selected: Bool
|
||||
@Binding var nav_target: String?
|
||||
@Binding var navigating: Bool
|
||||
@State var shown: Bool
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent, scroller: ScrollViewProxy?, nav_target: Binding<String?>, navigating: Binding<Bool>, selected: Bool) {
|
||||
init(damus_state: DamusState, event: NostrEvent, scroller: ScrollViewProxy?, selected: Bool) {
|
||||
self.damus_state = damus_state
|
||||
self.event = event
|
||||
self.scroller = scroller
|
||||
self.selected = selected
|
||||
self._nav_target = nav_target
|
||||
self._navigating = navigating
|
||||
self._shown = State(initialValue: should_show_event(contacts: damus_state.contacts, ev: event))
|
||||
}
|
||||
|
||||
@@ -55,17 +51,9 @@ struct MutedEventView: View {
|
||||
var Event: some View {
|
||||
Group {
|
||||
if selected {
|
||||
SelectedEventView(damus: damus_state, event: event)
|
||||
SelectedEventView(damus: damus_state, event: event, size: .selected)
|
||||
} else {
|
||||
EventView(damus: damus_state, event: event, has_action_bar: true)
|
||||
.onTapGesture {
|
||||
nav_target = event.id
|
||||
navigating = true
|
||||
}
|
||||
.onAppear {
|
||||
// TODO: find another solution to prevent layout shifting and layout blocking on large responses
|
||||
scroller?.scrollTo("main", anchor: .bottom)
|
||||
}
|
||||
EventView(damus: damus_state, event: event)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,12 +89,12 @@ struct MutedEventView: View {
|
||||
}
|
||||
|
||||
struct MutedEventView_Previews: PreviewProvider {
|
||||
@State static var nav_target: String? = nil
|
||||
@State static var nav_target: NostrEvent = test_event
|
||||
@State static var navigating: Bool = false
|
||||
|
||||
static var previews: some View {
|
||||
|
||||
MutedEventView(damus_state: test_damus_state(), event: test_event, scroller: nil, nav_target: $nav_target, navigating: $navigating, selected: false)
|
||||
MutedEventView(damus_state: test_damus_state(), event: test_event, scroller: nil, selected: false)
|
||||
.frame(width: .infinity, height: 50)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,34 +26,34 @@ struct ReplyDescription_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
func reply_desc(profiles: Profiles, event: NostrEvent) -> String {
|
||||
func reply_desc(profiles: Profiles, event: NostrEvent, locale: Locale = Locale.current) -> String {
|
||||
let desc = make_reply_description(event.tags)
|
||||
let pubkeys = desc.pubkeys
|
||||
let n = desc.others
|
||||
|
||||
let bundle = bundleForLocale(locale: locale)
|
||||
|
||||
if desc.pubkeys.count == 0 {
|
||||
return NSLocalizedString("Replying to self", comment: "Label to indicate that the user is replying to themself.")
|
||||
return NSLocalizedString("Replying to self", bundle: bundle, comment: "Label to indicate that the user is replying to themself.")
|
||||
}
|
||||
|
||||
let names: [String] = pubkeys.map {
|
||||
let prof = profiles.lookup(id: $0)
|
||||
return Profile.displayName(profile: prof, pubkey: $0)
|
||||
return Profile.displayName(profile: prof, pubkey: $0).username
|
||||
}
|
||||
|
||||
let uniqueNames = NSOrderedSet(array: names).array as! [String]
|
||||
|
||||
let othersCount = n - pubkeys.count
|
||||
if names.count > 1 {
|
||||
if uniqueNames.count > 1 {
|
||||
let othersCount = n - pubkeys.count
|
||||
if othersCount == 0 {
|
||||
return String(format: "Replying to %@ & %@", names[0], names[1])
|
||||
return String(format: NSLocalizedString("Replying to %@ & %@", bundle: bundle, comment: "Label to indicate that the user is replying to 2 users."), locale: locale, uniqueNames[0], uniqueNames[1])
|
||||
} else {
|
||||
return String(format: "Replying to %@, %@ & %d others", names[0], names[1], othersCount)
|
||||
return String(format: localizedStringFormat(key: "replying_to_two_and_others", locale: locale), locale: locale, othersCount, uniqueNames[0], uniqueNames[1])
|
||||
}
|
||||
}
|
||||
|
||||
if othersCount == 0 {
|
||||
return String(format: "Replying to %@", names[0])
|
||||
} else {
|
||||
return String(format: "Replying to %@ & %d others", names[0], othersCount)
|
||||
}
|
||||
return String(format: NSLocalizedString("Replying to %@", bundle: bundle, comment: "Label to indicate that the user is replying to 1 user."), locale: locale, uniqueNames[0])
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import SwiftUI
|
||||
struct SelectedEventView: View {
|
||||
let damus: DamusState
|
||||
let event: NostrEvent
|
||||
let size: EventViewKind
|
||||
|
||||
var pubkey: String {
|
||||
event.pubkey
|
||||
@@ -17,9 +18,10 @@ struct SelectedEventView: View {
|
||||
|
||||
@StateObject var bar: ActionBarModel
|
||||
|
||||
init(damus: DamusState, event: NostrEvent) {
|
||||
init(damus: DamusState, event: NostrEvent, size: EventViewKind) {
|
||||
self.damus = damus
|
||||
self.event = event
|
||||
self.size = size
|
||||
self._bar = StateObject(wrappedValue: make_actionbar_model(ev: event.id, damus: damus))
|
||||
}
|
||||
|
||||
@@ -28,8 +30,19 @@ struct SelectedEventView: View {
|
||||
let profile = damus.profiles.lookup(id: pubkey)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
EventProfile(damus_state: damus, pubkey: pubkey, profile: profile, size: .normal)
|
||||
EventBody(damus_state: damus, event: event, size: .selected)
|
||||
HStack {
|
||||
EventProfile(damus_state: damus, pubkey: pubkey, profile: profile, size: .normal)
|
||||
|
||||
Spacer()
|
||||
|
||||
EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks)
|
||||
.padding([.bottom], 4)
|
||||
|
||||
}
|
||||
.minimumScaleFactor(0.75)
|
||||
.lineLimit(1)
|
||||
|
||||
EventBody(damus_state: damus, event: event, size: size, options: [])
|
||||
|
||||
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
|
||||
BuilderEventView(damus: damus, event_id: mention.ref.id)
|
||||
@@ -60,14 +73,14 @@ struct SelectedEventView: View {
|
||||
self.bar.update(damus: self.damus, evid: target)
|
||||
}
|
||||
.padding([.leading], 2)
|
||||
.event_context_menu(event, keypair: damus.keypair, target_pubkey: event.pubkey)
|
||||
.compositingGroup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SelectedEventView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SelectedEventView(damus: test_damus_state(), event: test_event)
|
||||
SelectedEventView(damus: test_damus_state(), event: test_event, size: .selected)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,64 +7,188 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EventViewOptions: OptionSet {
|
||||
let rawValue: UInt8
|
||||
static let no_action_bar = EventViewOptions(rawValue: 1 << 0)
|
||||
static let no_replying_to = EventViewOptions(rawValue: 1 << 1)
|
||||
static let no_images = EventViewOptions(rawValue: 1 << 2)
|
||||
static let wide = EventViewOptions(rawValue: 1 << 3)
|
||||
static let truncate_content = EventViewOptions(rawValue: 1 << 4)
|
||||
static let pad_content = EventViewOptions(rawValue: 1 << 5)
|
||||
}
|
||||
|
||||
struct TextEvent: View {
|
||||
let damus: DamusState
|
||||
let event: NostrEvent
|
||||
let pubkey: String
|
||||
let has_action_bar: Bool
|
||||
let booster_pubkey: String?
|
||||
let options: EventViewOptions
|
||||
|
||||
var has_action_bar: Bool {
|
||||
!options.contains(.no_action_bar)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top) {
|
||||
let profile = damus.profiles.lookup(id: pubkey)
|
||||
|
||||
VStack {
|
||||
let pmodel = ProfileModel(pubkey: pubkey, damus: damus)
|
||||
let pv = ProfileView(damus_state: damus, profile: pmodel, followers: FollowersModel(damus_state: damus, target: pubkey))
|
||||
|
||||
NavigationLink(destination: pv) {
|
||||
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus.profiles)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
Group {
|
||||
if options.contains(.wide) {
|
||||
WideStyle
|
||||
} else {
|
||||
ThreadedStyle
|
||||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
HStack(alignment: .center) {
|
||||
EventProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true, size: .normal)
|
||||
|
||||
Text(verbatim: "\(format_relative_time(event.created_at))")
|
||||
.foregroundColor(.gray)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
EventBody(damus_state: damus, event: event, size: .normal)
|
||||
|
||||
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
|
||||
BuilderEventView(damus: damus, event_id: mention.ref.id)
|
||||
}
|
||||
|
||||
if has_action_bar {
|
||||
Rectangle().frame(height: 2).opacity(0)
|
||||
|
||||
EventActionBar(damus_state: damus, event: event)
|
||||
.padding([.top], 4)
|
||||
}
|
||||
}
|
||||
.padding([.leading], 2)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.background(event_validity_color(event.validity))
|
||||
.id(event.id)
|
||||
.frame(maxWidth: .infinity, minHeight: PFP_SIZE)
|
||||
.padding([.bottom], 2)
|
||||
.event_context_menu(event, keypair: damus.keypair, target_pubkey: pubkey)
|
||||
}
|
||||
|
||||
func Pfp(is_anon: Bool) -> some View {
|
||||
MaybeAnonPfpView(state: damus, is_anon: is_anon, pubkey: pubkey)
|
||||
}
|
||||
|
||||
func TopPart(is_anon: Bool) -> some View {
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
ProfileName(is_anon: is_anon)
|
||||
TimeDot
|
||||
Time
|
||||
Spacer()
|
||||
ContextButton
|
||||
}
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
var ReplyPart: some View {
|
||||
Group {
|
||||
if event_is_reply(event, privkey: damus.keypair.privkey) {
|
||||
ReplyDescription(event: event, profiles: damus.profiles)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var WideStyle: some View {
|
||||
VStack(alignment: .leading) {
|
||||
let is_anon = event_is_anonymous(ev: event)
|
||||
|
||||
HStack(spacing: 10) {
|
||||
Pfp(is_anon: is_anon)
|
||||
VStack {
|
||||
TopPart(is_anon: is_anon)
|
||||
ReplyPart
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
EvBody(options: [.truncate_content, .pad_content])
|
||||
|
||||
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
|
||||
Mention(mention)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
if has_action_bar {
|
||||
//EmptyRect
|
||||
ActionBar
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var TimeDot: some View {
|
||||
Text(verbatim: "⋅")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
|
||||
var Time: some View {
|
||||
Text(verbatim: "\(format_relative_time(event.created_at))")
|
||||
.font(.system(size: 16))
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
|
||||
var ContextButton: some View {
|
||||
EventMenuContext(event: event, keypair: damus.keypair, target_pubkey: event.pubkey, bookmarks: damus.bookmarks)
|
||||
.padding([.bottom], 4)
|
||||
}
|
||||
|
||||
func ProfileName(is_anon: Bool) -> some View {
|
||||
let profile = damus.profiles.lookup(id: pubkey)
|
||||
let pk = is_anon ? "anon" : pubkey
|
||||
return EventProfileName(pubkey: pk, profile: profile, damus: damus, show_friend_confirmed: true, size: .normal)
|
||||
}
|
||||
|
||||
func EvBody(options: EventViewOptions) -> some View {
|
||||
return EventBody(damus_state: damus, event: event, size: .normal, options: options)
|
||||
}
|
||||
|
||||
func Mention(_ mention: Mention) -> some View {
|
||||
return BuilderEventView(damus: damus, event_id: mention.ref.id)
|
||||
}
|
||||
|
||||
var ActionBar: some View {
|
||||
return EventActionBar(damus_state: damus, event: event)
|
||||
.padding([.top], 4)
|
||||
}
|
||||
|
||||
var EmptyRect: some View {
|
||||
return Rectangle().frame(height: 2).opacity(0)
|
||||
}
|
||||
|
||||
var ThreadedStyle: some View {
|
||||
HStack(alignment: .top) {
|
||||
|
||||
let is_anon = event_is_anonymous(ev: event)
|
||||
VStack {
|
||||
Pfp(is_anon: is_anon)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
TopPart(is_anon: is_anon)
|
||||
|
||||
ReplyPart
|
||||
EvBody(options: [])
|
||||
|
||||
if let mention = first_eref_mention(ev: event, privkey: damus.keypair.privkey) {
|
||||
Mention(mention)
|
||||
}
|
||||
|
||||
if has_action_bar {
|
||||
EmptyRect
|
||||
ActionBar
|
||||
}
|
||||
}
|
||||
.padding([.leading], 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func event_has_tag(ev: NostrEvent, tag: String) -> Bool {
|
||||
for t in ev.tags {
|
||||
if t.count >= 1 && t[0] == tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
func event_is_anonymous(ev: NostrEvent) -> Bool {
|
||||
return ev.known_kind == .zap_request && event_has_tag(ev: ev, tag: "anon")
|
||||
}
|
||||
|
||||
struct TextEvent_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TextEvent(damus: test_damus_state(), event: test_event, pubkey: "pk", has_action_bar: true, booster_pubkey: nil)
|
||||
VStack(spacing: 20) {
|
||||
TextEvent(damus: test_damus_state(), event: test_event, pubkey: "pk", options: [])
|
||||
.frame(height: 400)
|
||||
|
||||
TextEvent(damus: test_damus_state(), event: test_event, pubkey: "pk", options: [.wide])
|
||||
.frame(height: 400)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
22
damus/Views/Events/WideEventView.swift
Normal file
22
damus/Views/Events/WideEventView.swift
Normal file
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// WideEventView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-03-23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct WideEventView: View {
|
||||
let event: NostrEvent
|
||||
|
||||
var body: some View {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
struct WideEventView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
WideEventView(event: test_event)
|
||||
}
|
||||
}
|
||||
@@ -13,21 +13,44 @@ struct ZapEvent: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("⚡️ \(format_msats(zap.invoice.amount))", comment: "Text indicating the zap amount. i.e. number of satoshis that were tipped to a user")
|
||||
.font(.headline)
|
||||
.padding([.top], 2)
|
||||
HStack(alignment: .center) {
|
||||
Text("⚡️ \(format_msats(zap.invoice.amount))", comment: "Text indicating the zap amount. i.e. number of satoshis that were tipped to a user")
|
||||
.font(.headline)
|
||||
.padding([.top], 2)
|
||||
|
||||
if zap.private_request != nil {
|
||||
Image(systemName: "lock.fill")
|
||||
.foregroundColor(Color("DamusGreen"))
|
||||
.help(NSLocalizedString("Only you can see this message and who sent it.", comment: "Help text on green lock icon that explains that only the current user can see the message of a zap event and who sent the zap."))
|
||||
}
|
||||
}
|
||||
|
||||
TextEvent(damus: damus, event: zap.request.ev, pubkey: zap.request.ev.pubkey, has_action_bar: false, booster_pubkey: nil)
|
||||
.padding([.top], 1)
|
||||
if let priv = zap.private_request {
|
||||
|
||||
TextEvent(damus: damus, event: priv, pubkey: priv.pubkey, options: [.no_action_bar, .no_replying_to])
|
||||
.padding([.top], 1)
|
||||
} else {
|
||||
TextEvent(damus: damus, event: zap.request.ev, pubkey: zap.request.ev.pubkey, options: [.no_action_bar, .no_replying_to])
|
||||
.padding([.top], 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
let test_zap_invoice = ZapInvoice(description: .description("description"), amount: 10000, string: "lnbc1", expiry: 1000000, payment_hash: Data(), created_at: 1000000)
|
||||
let test_zap_request_ev = NostrEvent(content: "hi", pubkey: "pk", kind: 9734)
|
||||
let test_zap_request = ZapRequest(ev: test_zap_request_ev)
|
||||
let test_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), request: test_zap_request, is_anon: false, private_request: nil)
|
||||
|
||||
let test_private_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), request: test_zap_request, is_anon: false, private_request: test_event)
|
||||
|
||||
struct ZapEvent_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ZapEvent()
|
||||
VStack {
|
||||
ZapEvent(damus: test_damus_state(), zap: test_zap)
|
||||
|
||||
ZapEvent(damus: test_damus_state(), zap: test_private_zap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
@@ -19,7 +19,7 @@ struct FollowButtonView: View {
|
||||
Button {
|
||||
follow_state = perform_follow_btn_action(follow_state, target: target)
|
||||
} label: {
|
||||
Text(follow_btn_txt(follow_state, follows_you: follows_you))
|
||||
Text(verbatim: "\(follow_btn_txt(follow_state, follows_you: follows_you))")
|
||||
.frame(width: 105, height: 30)
|
||||
//.padding(.vertical, 10)
|
||||
.font(.caption.weight(.bold))
|
||||
|
||||
@@ -29,7 +29,6 @@ struct FollowersView: View {
|
||||
@EnvironmentObject var followers: FollowersModel
|
||||
|
||||
var body: some View {
|
||||
let profile = damus_state.profiles.lookup(id: whos)
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading) {
|
||||
ForEach(followers.contacts ?? [], id: \.self) { pk in
|
||||
@@ -38,7 +37,7 @@ struct FollowersView: View {
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.navigationBarTitle(NSLocalizedString("\(Profile.displayName(profile: profile, pubkey: whos))'s Followers", comment: "Navigation bar title for view that shows who is following a user."))
|
||||
.navigationBarTitle(NSLocalizedString("Followers", comment: "Navigation bar title for view that shows who is following a user."))
|
||||
.onAppear {
|
||||
followers.subscribe()
|
||||
}
|
||||
@@ -56,8 +55,6 @@ struct FollowingView: View {
|
||||
let whos: String
|
||||
|
||||
var body: some View {
|
||||
let profile = damus_state.profiles.lookup(id: whos)
|
||||
let who = Profile.displayName(profile: profile, pubkey: whos)
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading) {
|
||||
ForEach(following.contacts, id: \.self) { pk in
|
||||
@@ -72,7 +69,7 @@ struct FollowingView: View {
|
||||
.onDisappear {
|
||||
following.unsubscribe()
|
||||
}
|
||||
.navigationBarTitle(NSLocalizedString("\(who) following", comment: "Navigation bar title for view that shows who a user is following."))
|
||||
.navigationBarTitle(NSLocalizedString("Following", comment: "Navigation bar title for view that shows who a user is following."))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
51
damus/Views/Images/ImageContainerView.swift
Normal file
51
damus/Views/Images/ImageContainerView.swift
Normal file
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// CarouselImageContainerView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-03-23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
|
||||
// lots of overlap between this and ImageContainerView
|
||||
struct ImageContainerView: View {
|
||||
|
||||
let url: URL?
|
||||
|
||||
@State private var image: UIImage?
|
||||
@State private var showShareSheet = false
|
||||
|
||||
private struct ImageHandler: ImageModifier {
|
||||
@Binding var handler: UIImage?
|
||||
|
||||
func modify(_ image: UIImage) -> UIImage {
|
||||
handler = image
|
||||
return image
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
|
||||
KFAnimatedImage(url)
|
||||
.imageContext(.note)
|
||||
.configure { view in
|
||||
view.framePreloadCount = 3
|
||||
}
|
||||
.imageModifier(ImageHandler(handler: $image))
|
||||
.clipped()
|
||||
.modifier(ImageContextMenuModifier(url: url, image: image, showShareSheet: $showShareSheet))
|
||||
.sheet(isPresented: $showShareSheet) {
|
||||
ShareSheet(activityItems: [url])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let test_image_url = URL(string: "https://jb55.com/red-me.jpg")!
|
||||
|
||||
struct ImageContainerView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ImageContainerView(url: test_image_url)
|
||||
}
|
||||
}
|
||||
43
damus/Views/Images/ImageContextMenuModifier.swift
Normal file
43
damus/Views/Images/ImageContextMenuModifier.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// ImageContextMenuModifier.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-03-23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct ImageContextMenuModifier: ViewModifier {
|
||||
let url: URL?
|
||||
let image: UIImage?
|
||||
@Binding var showShareSheet: Bool
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
return content.contextMenu {
|
||||
Button {
|
||||
UIPasteboard.general.url = url
|
||||
} label: {
|
||||
Label(NSLocalizedString("Copy Image URL", comment: "Context menu option to copy the URL of an image into clipboard."), systemImage: "doc.on.doc")
|
||||
}
|
||||
if let someImage = image {
|
||||
Button {
|
||||
UIPasteboard.general.image = someImage
|
||||
} label: {
|
||||
Label(NSLocalizedString("Copy Image", comment: "Context menu option to copy an image into clipboard."), systemImage: "photo.on.rectangle")
|
||||
}
|
||||
Button {
|
||||
UIImageWriteToSavedPhotosAlbum(someImage, nil, nil, nil)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Save Image", comment: "Context menu option to save an image."), systemImage: "square.and.arrow.down")
|
||||
}
|
||||
}
|
||||
Button {
|
||||
showShareSheet = true
|
||||
} label: {
|
||||
Label(NSLocalizedString("Share", comment: "Button to share an image."), systemImage: "square.and.arrow.up")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
102
damus/Views/Images/ImageView.swift
Normal file
102
damus/Views/Images/ImageView.swift
Normal file
@@ -0,0 +1,102 @@
|
||||
//
|
||||
// ImageView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-03-23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ImageView: View {
|
||||
|
||||
let urls: [URL?]
|
||||
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
@State private var selectedIndex = 0
|
||||
@State var showMenu = true
|
||||
|
||||
var navBarView: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
/*
|
||||
Text(urls[selectedIndex]?.lastPathComponent ?? "")
|
||||
.bold()
|
||||
*/
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}, label: {
|
||||
Image(systemName: "xmark")
|
||||
})
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
var tabViewIndicator: some View {
|
||||
HStack(spacing: 10) {
|
||||
ForEach(urls.indices, id: \.self) { index in
|
||||
Capsule()
|
||||
.fill(index == selectedIndex ? Color(UIColor.label) : Color.secondary)
|
||||
.frame(width: 7, height: 7)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(.regularMaterial)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color(.systemBackground)
|
||||
.ignoresSafeArea()
|
||||
|
||||
TabView(selection: $selectedIndex) {
|
||||
ForEach(urls.indices, id: \.self) { index in
|
||||
ZoomableScrollView {
|
||||
ImageContainerView(url: urls[index])
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.padding(.top, Theme.safeAreaInsets?.top)
|
||||
.padding(.bottom, Theme.safeAreaInsets?.bottom)
|
||||
}
|
||||
.modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}))
|
||||
.ignoresSafeArea()
|
||||
.tag(index)
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
||||
.gesture(TapGesture(count: 2).onEnded {
|
||||
// Prevents menu from hiding on double tap
|
||||
})
|
||||
.gesture(TapGesture(count: 1).onEnded {
|
||||
showMenu.toggle()
|
||||
})
|
||||
.overlay(
|
||||
VStack {
|
||||
if showMenu {
|
||||
navBarView
|
||||
Spacer()
|
||||
|
||||
if (urls.count > 1) {
|
||||
tabViewIndicator
|
||||
}
|
||||
}
|
||||
}
|
||||
.animation(.easeInOut, value: showMenu)
|
||||
.padding(.bottom, Theme.safeAreaInsets?.bottom)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ImageView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ImageView(urls: [URL(string: "https://jb55.com/red-me.jpg")])
|
||||
}
|
||||
}
|
||||
@@ -27,20 +27,81 @@ struct NoteContentView: View {
|
||||
let event: NostrEvent
|
||||
let show_images: Bool
|
||||
let size: EventViewKind
|
||||
let preview_height: CGFloat?
|
||||
let options: EventViewOptions
|
||||
|
||||
@State var artifacts: NoteArtifacts
|
||||
@State var preview: LinkViewRepresentable? = nil
|
||||
@State var preview: LinkViewRepresentable?
|
||||
|
||||
func MainContent() -> some View {
|
||||
return VStack(alignment: .leading) {
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent, show_images: Bool, size: EventViewKind, artifacts: NoteArtifacts, options: EventViewOptions) {
|
||||
self.damus_state = damus_state
|
||||
self.event = event
|
||||
self.show_images = show_images
|
||||
self.size = size
|
||||
self.options = options
|
||||
self._artifacts = State(initialValue: artifacts)
|
||||
self.preview_height = lookup_cached_preview_size(previews: damus_state.previews, evid: event.id)
|
||||
self._preview = State(initialValue: load_cached_preview(previews: damus_state.previews, evid: event.id))
|
||||
self._artifacts = State(initialValue: render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey))
|
||||
}
|
||||
|
||||
var truncate: Bool {
|
||||
return options.contains(.truncate_content)
|
||||
}
|
||||
|
||||
var with_padding: Bool {
|
||||
return options.contains(.pad_content)
|
||||
}
|
||||
|
||||
var truncatedText: some View {
|
||||
TruncatedText(text: artifacts.content, maxChars: (truncate ? 280 : nil))
|
||||
.font(eventviewsize_to_font(size))
|
||||
}
|
||||
|
||||
var invoicesView: some View {
|
||||
InvoicesView(our_pubkey: damus_state.keypair.pubkey, invoices: artifacts.invoices)
|
||||
}
|
||||
|
||||
var translateView: some View {
|
||||
TranslateView(damus_state: damus_state, event: event)
|
||||
}
|
||||
|
||||
var previewView: some View {
|
||||
Group {
|
||||
if let preview = self.preview, show_images {
|
||||
if let preview_height {
|
||||
preview
|
||||
.frame(height: preview_height)
|
||||
} else {
|
||||
preview
|
||||
}
|
||||
} else if let link = artifacts.links.first {
|
||||
LinkViewRepresentable(meta: .url(link))
|
||||
.frame(height: 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var MainContent: some View {
|
||||
VStack(alignment: .leading) {
|
||||
if size == .selected {
|
||||
SelectableText(attributedString: artifacts.content)
|
||||
TranslateView(damus_state: damus_state, event: event)
|
||||
} else {
|
||||
Text(artifacts.content)
|
||||
.font(eventviewsize_to_font(size))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
if with_padding {
|
||||
truncatedText
|
||||
.padding(.horizontal)
|
||||
} else {
|
||||
truncatedText
|
||||
}
|
||||
}
|
||||
|
||||
if size == .selected || damus_state.settings.auto_translate {
|
||||
if with_padding {
|
||||
translateView
|
||||
.padding(.horizontal)
|
||||
} else {
|
||||
translateView
|
||||
}
|
||||
}
|
||||
|
||||
if show_images && artifacts.images.count > 0 {
|
||||
@@ -51,30 +112,29 @@ struct NoteContentView: View {
|
||||
Blur()
|
||||
.disabled(true)
|
||||
}
|
||||
.cornerRadius(10)
|
||||
}
|
||||
if artifacts.invoices.count > 0 {
|
||||
InvoicesView(our_pubkey: damus_state.keypair.pubkey, invoices: artifacts.invoices)
|
||||
//.cornerRadius(10)
|
||||
}
|
||||
|
||||
if let preview = self.preview, show_images {
|
||||
preview
|
||||
} else {
|
||||
ForEach(artifacts.links, id:\.self) { link in
|
||||
if let url = link {
|
||||
LinkViewRepresentable(meta: .url(url))
|
||||
.frame(height: 50)
|
||||
}
|
||||
if artifacts.invoices.count > 0 {
|
||||
if with_padding {
|
||||
invoicesView
|
||||
.padding(.horizontal)
|
||||
} else {
|
||||
invoicesView
|
||||
}
|
||||
}
|
||||
|
||||
if with_padding {
|
||||
previewView.padding(.horizontal)
|
||||
} else {
|
||||
previewView
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
MainContent()
|
||||
.onAppear() {
|
||||
self.artifacts = render_note_content(ev: event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey)
|
||||
}
|
||||
MainContent
|
||||
.onReceive(handle_notify(.profile_updated)) { notif in
|
||||
let profile = notif.object as! ProfileUpdate
|
||||
let blocks = event.blocks(damus_state.keypair.privkey)
|
||||
@@ -92,21 +152,19 @@ struct NoteContentView: View {
|
||||
}
|
||||
}
|
||||
.task {
|
||||
if let preview = damus_state.previews.lookup(self.event.id) {
|
||||
switch preview {
|
||||
case .value(let view):
|
||||
self.preview = view
|
||||
case .failed:
|
||||
// don't try to refetch meta if we've failed
|
||||
return
|
||||
}
|
||||
guard self.preview == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
if show_images, artifacts.links.count == 1 {
|
||||
let meta = await getMetaData(for: artifacts.links.first!)
|
||||
|
||||
let view = meta.map { LinkViewRepresentable(meta: .linkmeta($0)) }
|
||||
damus_state.previews.store(evid: self.event.id, preview: view)
|
||||
damus_state.previews.store(evid: self.event.id, preview: meta)
|
||||
guard case .value(let cached) = damus_state.previews.lookup(self.event.id) else {
|
||||
return
|
||||
}
|
||||
let view = LinkViewRepresentable(meta: .linkmeta(cached))
|
||||
|
||||
self.preview = view
|
||||
}
|
||||
|
||||
@@ -131,15 +189,15 @@ struct NoteContentView: View {
|
||||
|
||||
func hashtag_str(_ htag: String) -> AttributedString {
|
||||
var attributedString = AttributedString(stringLiteral: "#\(htag)")
|
||||
attributedString.link = URL(string: "nostr:t:\(htag)")
|
||||
attributedString.foregroundColor = .purple
|
||||
attributedString.link = URL(string: "damus:t:\(htag)")
|
||||
attributedString.foregroundColor = Color("DamusPurple")
|
||||
return attributedString
|
||||
}
|
||||
|
||||
func url_str(_ url: URL) -> AttributedString {
|
||||
var attributedString = AttributedString(stringLiteral: url.absoluteString)
|
||||
attributedString.link = url
|
||||
attributedString.foregroundColor = .purple
|
||||
attributedString.foregroundColor = Color("DamusPurple")
|
||||
return attributedString
|
||||
}
|
||||
|
||||
@@ -148,16 +206,16 @@ func mention_str(_ m: Mention, profiles: Profiles) -> AttributedString {
|
||||
case .pubkey:
|
||||
let pk = m.ref.ref_id
|
||||
let profile = profiles.lookup(id: pk)
|
||||
let disp = Profile.displayName(profile: profile, pubkey: pk)
|
||||
let disp = Profile.displayName(profile: profile, pubkey: pk).username
|
||||
var attributedString = AttributedString(stringLiteral: "@\(disp)")
|
||||
attributedString.link = URL(string: "nostr:\(encode_pubkey_uri(m.ref))")
|
||||
attributedString.foregroundColor = .purple
|
||||
attributedString.link = URL(string: "damus:\(encode_pubkey_uri(m.ref))")
|
||||
attributedString.foregroundColor = Color("DamusPurple")
|
||||
return attributedString
|
||||
case .event:
|
||||
let bevid = bech32_note_id(m.ref.ref_id) ?? m.ref.ref_id
|
||||
var attributedString = AttributedString(stringLiteral: "@\(abbrev_pubkey(bevid))")
|
||||
attributedString.link = URL(string: "nostr:\(encode_event_id_uri(m.ref))")
|
||||
attributedString.foregroundColor = .purple
|
||||
attributedString.link = URL(string: "damus:\(encode_event_id_uri(m.ref))")
|
||||
attributedString.foregroundColor = Color("DamusPurple")
|
||||
return attributedString
|
||||
}
|
||||
}
|
||||
@@ -167,17 +225,7 @@ struct NoteContentView_Previews: PreviewProvider {
|
||||
let state = test_damus_state()
|
||||
let content = "hi there ¯\\_(ツ)_/¯ https://jb55.com/s/Oct12-150217.png 5739a762ef6124dd.jpg"
|
||||
let artifacts = NoteArtifacts(content: AttributedString(stringLiteral: content), images: [], invoices: [], links: [])
|
||||
NoteContentView(damus_state: state, event: NostrEvent(content: content, pubkey: "pk"), show_images: true, size: .normal, artifacts: artifacts)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension View {
|
||||
func translate_button_style() -> some View {
|
||||
return self
|
||||
.font(.footnote)
|
||||
.contentShape(Rectangle())
|
||||
.padding([.top, .bottom], 10)
|
||||
NoteContentView(damus_state: state, event: NostrEvent(content: content, pubkey: "pk"), show_images: true, size: .normal, artifacts: artifacts, options: [])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,6 +242,7 @@ struct NoteArtifacts {
|
||||
|
||||
func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -> NoteArtifacts {
|
||||
let blocks = ev.blocks(privkey)
|
||||
|
||||
return render_blocks(blocks: blocks, profiles: profiles, privkey: privkey)
|
||||
}
|
||||
|
||||
@@ -201,9 +250,17 @@ func render_blocks(blocks: [Block], profiles: Profiles, privkey: String?) -> Not
|
||||
var invoices: [Invoice] = []
|
||||
var img_urls: [URL] = []
|
||||
var link_urls: [URL] = []
|
||||
|
||||
let one_note_ref = blocks
|
||||
.filter({ $0.is_note_mention })
|
||||
.count == 1
|
||||
|
||||
let txt: AttributedString = blocks.reduce("") { str, block in
|
||||
switch block {
|
||||
case .mention(let m):
|
||||
if m.type == .event && one_note_ref {
|
||||
return str
|
||||
}
|
||||
return str + mention_str(m, profiles: profiles)
|
||||
case .text(let txt):
|
||||
return str + AttributedString(stringLiteral: txt)
|
||||
@@ -230,6 +287,57 @@ func render_blocks(blocks: [Block], profiles: Profiles, privkey: String?) -> Not
|
||||
|
||||
func is_image_url(_ url: URL) -> Bool {
|
||||
let str = url.lastPathComponent.lowercased()
|
||||
return str.hasSuffix("png") || str.hasSuffix("jpg") || str.hasSuffix("jpeg") || str.hasSuffix("gif")
|
||||
let isUrl = str.hasSuffix(".png") || str.hasSuffix(".jpg") || str.hasSuffix(".jpeg") || str.hasSuffix(".gif")
|
||||
return isUrl
|
||||
}
|
||||
|
||||
func lookup_cached_preview_size(previews: PreviewCache, evid: String) -> CGFloat? {
|
||||
guard case .value(let cached) = previews.lookup(evid) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let height = cached.intrinsic_height else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return height
|
||||
}
|
||||
|
||||
|
||||
func load_cached_preview(previews: PreviewCache, evid: String) -> LinkViewRepresentable? {
|
||||
guard case .value(let meta) = previews.lookup(evid) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return LinkViewRepresentable(meta: .linkmeta(meta))
|
||||
}
|
||||
|
||||
struct TruncatedText: View {
|
||||
|
||||
let text: AttributedString
|
||||
let maxChars: Int?
|
||||
|
||||
var body: some View {
|
||||
let truncatedAttributedString: AttributedString? = getTruncatedString()
|
||||
|
||||
Text(truncatedAttributedString ?? text)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
|
||||
if truncatedAttributedString != nil {
|
||||
Spacer()
|
||||
Button(NSLocalizedString("Show more", comment: "Button to show entire note.")) { }
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
|
||||
func getTruncatedString() -> AttributedString? {
|
||||
guard let maxChars = maxChars else { return nil }
|
||||
let nsAttributedString = NSAttributedString(text)
|
||||
if nsAttributedString.length < maxChars { return nil }
|
||||
|
||||
let range = NSRange(location: 0, length: maxChars)
|
||||
let truncatedAttributedString = nsAttributedString.attributedSubstring(from: range)
|
||||
|
||||
return AttributedString(truncatedAttributedString) + "..."
|
||||
}
|
||||
}
|
||||
|
||||
247
damus/Views/Notifications/EventGroupView.swift
Normal file
247
damus/Views/Notifications/EventGroupView.swift
Normal file
@@ -0,0 +1,247 @@
|
||||
//
|
||||
// RepostGroupView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
||||
enum EventGroupType {
|
||||
case repost(EventGroup)
|
||||
case reaction(EventGroup)
|
||||
case zap(ZapGroup)
|
||||
case profile_zap(ZapGroup)
|
||||
|
||||
var zap_group: ZapGroup? {
|
||||
switch self {
|
||||
case .profile_zap(let grp):
|
||||
return grp
|
||||
case .zap(let grp):
|
||||
return grp
|
||||
case .reaction:
|
||||
return nil
|
||||
case .repost:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var events: [NostrEvent] {
|
||||
switch self {
|
||||
case .repost(let grp):
|
||||
return grp.events
|
||||
case .reaction(let grp):
|
||||
return grp.events
|
||||
case .zap(let zapgrp):
|
||||
return zapgrp.zap_requests()
|
||||
case .profile_zap(let zapgrp):
|
||||
return zapgrp.zap_requests()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ReactingTo {
|
||||
case your_post
|
||||
case tagged_in
|
||||
case your_profile
|
||||
}
|
||||
|
||||
func determine_reacting_to(our_pubkey: String, ev: NostrEvent?) -> ReactingTo {
|
||||
guard let ev else {
|
||||
return .your_profile
|
||||
}
|
||||
|
||||
if ev.pubkey == our_pubkey {
|
||||
return .your_post
|
||||
}
|
||||
|
||||
return .tagged_in
|
||||
}
|
||||
|
||||
func event_author_name(profiles: Profiles, pubkey: String) -> String {
|
||||
let alice_prof = profiles.lookup(id: pubkey)
|
||||
return Profile.displayName(profile: alice_prof, pubkey: pubkey).username
|
||||
}
|
||||
|
||||
func event_group_author_name(profiles: Profiles, ind: Int, group: EventGroupType) -> String {
|
||||
if let zapgrp = group.zap_group {
|
||||
let zap = zapgrp.zaps[ind]
|
||||
|
||||
if let privzap = zap.private_request {
|
||||
return event_author_name(profiles: profiles, pubkey: privzap.pubkey)
|
||||
}
|
||||
|
||||
if zap.is_anon {
|
||||
return "Anonymous"
|
||||
}
|
||||
|
||||
return event_author_name(profiles: profiles, pubkey: zap.request.ev.pubkey)
|
||||
} else {
|
||||
let ev = group.events[ind]
|
||||
return event_author_name(profiles: profiles, pubkey: ev.pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Returns a notification string describing user actions in response to an event group type.
|
||||
|
||||
The localization keys read by this function are the following (although some keys may not actually be used in practice):
|
||||
|
||||
"??" - returned when there are no events associated with the specified event group type.
|
||||
|
||||
"reacted_tagged_in_1" - returned when 1 reaction occurred to a post that the current user was tagged in
|
||||
"reacted_tagged_in_2" - returned when 2 reactions occurred to a post that the current user was tagged in
|
||||
"reacted_tagged_in_3" - returned when 3 or more reactions occurred to a post that the current user was tagged in
|
||||
"reacted_your_post_1" - returned when 1 reaction occurred to the current user's post
|
||||
"reacted_your_post_2" - returned when 2 reactions occurred to the current user's post
|
||||
"reacted_your_post_3" - returned when 3 or more reactions occurred to the current user's post
|
||||
"reacted_your_profile_1" - returned when 1 reaction occurred to the current user's profile
|
||||
"reacted_your_profile_2" - returned when 2 reactions occurred to the current user's profile
|
||||
"reacted_your_profile_3" - returned when 3 or more reactions occurred to the current user's profile
|
||||
|
||||
"reposted_tagged_in_1" - returned when 1 repost occurred to a post that the current user was tagged in
|
||||
"reposted_tagged_in_2" - returned when 2 reposts occurred to a post that the current user was tagged in
|
||||
"reposted_tagged_in_3" - returned when 3 or more reposts occurred to a post that the current user was tagged in
|
||||
"reposted_your_post_1" - returned when 1 repost occurred to the current user's post
|
||||
"reposted_your_post_2" - returned when 2 reposts occurred to the current user's post
|
||||
"reposted_your_post_3" - returned when 3 or more reposts occurred to the current user's post
|
||||
"reposted_your_profile_1" - returned when 1 repost occurred to the current user's profile
|
||||
"reposted_your_profile_2" - returned when 2 reposts occurred to the current user's profile
|
||||
"reposted_your_profile_3" - returned when 3 or more reposts occurred to the current user's profile
|
||||
|
||||
"zapped_tagged_in_1" - returned when 1 zap occurred to a post that the current user was tagged in
|
||||
"zapped_tagged_in_2" - returned when 2 zaps occurred to a post that the current user was tagged in
|
||||
"zapped_tagged_in_3" - returned when 3 or more zaps occurred to a post that the current user was tagged in
|
||||
"zapped_your_post_1" - returned when 1 zap occurred to the current user's post
|
||||
"zapped_your_post_2" - returned when 2 zaps occurred to the current user's post
|
||||
"zapped_your_post_3" - returned when 3 or more zaps occurred to the current user's post
|
||||
"zapped_your_profile_1" - returned when 1 zap occurred to the current user's profile
|
||||
"zapped_your_profile_2" - returned when 2 zaps occurred to the current user's profile
|
||||
"zapped_your_profile_3" - returned when 3 or more zaps occurred to the current user's profile
|
||||
*/
|
||||
func reacting_to_text(profiles: Profiles, our_pubkey: String, group: EventGroupType, ev: NostrEvent?, locale: Locale? = nil) -> String {
|
||||
if group.events.count == 0 {
|
||||
return "??"
|
||||
}
|
||||
|
||||
let verb = reacting_to_verb(group: group)
|
||||
let reacting_to = determine_reacting_to(our_pubkey: our_pubkey, ev: ev)
|
||||
let localization_key = "\(verb)_\(reacting_to)_\(min(group.events.count, 3))"
|
||||
let format = localizedStringFormat(key: localization_key, locale: locale)
|
||||
|
||||
switch group.events.count {
|
||||
case 1:
|
||||
let display_name = event_group_author_name(profiles: profiles, ind: 0, group: group)
|
||||
|
||||
return String(format: format, locale: locale, display_name)
|
||||
case 2:
|
||||
let alice_name = event_group_author_name(profiles: profiles, ind: 0, group: group)
|
||||
let bob_name = event_group_author_name(profiles: profiles, ind: 1, group: group)
|
||||
|
||||
return String(format: format, locale: locale, alice_name, bob_name)
|
||||
default:
|
||||
let alice_name = event_group_author_name(profiles: profiles, ind: 0, group: group)
|
||||
let count = group.events.count - 1
|
||||
|
||||
return String(format: format, locale: locale, count, alice_name)
|
||||
}
|
||||
}
|
||||
|
||||
func reacting_to_verb(group: EventGroupType) -> String {
|
||||
switch group {
|
||||
case .reaction:
|
||||
return "reacted"
|
||||
case .repost:
|
||||
return "reposted"
|
||||
case .zap: fallthrough
|
||||
case .profile_zap:
|
||||
return "zapped"
|
||||
}
|
||||
}
|
||||
|
||||
struct EventGroupView: View {
|
||||
let state: DamusState
|
||||
let event: NostrEvent?
|
||||
let group: EventGroupType
|
||||
|
||||
var GroupDescription: some View {
|
||||
Text(verbatim: "\(reacting_to_text(profiles: state.profiles, our_pubkey: state.pubkey, group: group, ev: event))")
|
||||
}
|
||||
|
||||
func ZapIcon(_ zapgrp: ZapGroup) -> some View {
|
||||
let fmt = format_msats_abbrev(zapgrp.msat_total)
|
||||
return VStack(alignment: .center) {
|
||||
Image(systemName: "bolt.fill")
|
||||
.foregroundColor(.orange)
|
||||
Text("\(fmt)")
|
||||
.foregroundColor(Color.orange)
|
||||
}
|
||||
}
|
||||
|
||||
var GroupIcon: some View {
|
||||
Group {
|
||||
switch group {
|
||||
case .repost:
|
||||
Image(systemName: "arrow.2.squarepath")
|
||||
.foregroundColor(Color("DamusGreen"))
|
||||
case .reaction:
|
||||
Image("shaka-full")
|
||||
.resizable()
|
||||
.frame(width: 24, height: 24)
|
||||
.foregroundColor(.accentColor)
|
||||
case .profile_zap(let zapgrp):
|
||||
ZapIcon(zapgrp)
|
||||
case .zap(let zapgrp):
|
||||
ZapIcon(zapgrp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top) {
|
||||
GroupIcon
|
||||
.frame(width: PFP_SIZE + 10)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
ProfilePicturesView(state: state, events: group.events)
|
||||
|
||||
GroupDescription
|
||||
|
||||
if let event {
|
||||
let thread = ThreadModel(event: event, damus_state: state)
|
||||
let dest = ThreadView(state: state, thread: thread)
|
||||
NavigationLink(destination: dest) {
|
||||
Text(render_note_content(ev: event, profiles: state.profiles, privkey: state.keypair.privkey).content)
|
||||
.padding([.top], 1)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding([.top], 6)
|
||||
}
|
||||
}
|
||||
|
||||
let test_encoded_post = "{\"id\": \"8ba545ab96959fe0ce7db31bc10f3ac3aa5353bc4428dbf1e56a7be7062516db\",\"pubkey\": \"7e27509ccf1e297e1df164912a43406218f8bd80129424c3ef798ca3ef5c8444\",\"created_at\": 1677013417,\"kind\": 1,\"tags\": [],\"content\": \"hello\",\"sig\": \"93684f15eddf11f42afbdd81828ee9fc35350344d8650c78909099d776e9ad8d959cd5c4bff7045be3b0b255144add43d0feef97940794a1bc9c309791bebe4a\"}"
|
||||
let test_repost_1 = NostrEvent(id: "", content: test_encoded_post, pubkey: "pk1", kind: 6, tags: [], createdAt: 1)
|
||||
let test_repost_2 = NostrEvent(id: "", content: test_encoded_post, pubkey: "pk2", kind: 6, tags: [], createdAt: 1)
|
||||
let test_reposts = [test_repost_1, test_repost_2]
|
||||
let test_event_group = EventGroup(events: test_reposts)
|
||||
|
||||
struct EventGroupView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
EventGroupView(state: test_damus_state(), event: test_event, group: .repost(test_event_group))
|
||||
.frame(height: 200)
|
||||
.padding()
|
||||
|
||||
EventGroupView(state: test_damus_state(), event: test_event, group: .reaction(test_event_group))
|
||||
.frame(height: 200)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
86
damus/Views/Notifications/NotificationItemView.swift
Normal file
86
damus/Views/Notifications/NotificationItemView.swift
Normal file
@@ -0,0 +1,86 @@
|
||||
//
|
||||
// NotificationItemView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum ShowItem {
|
||||
case show(NostrEvent?)
|
||||
case dontshow(NostrEvent?)
|
||||
}
|
||||
|
||||
func notification_item_event(events: EventCache, notif: NotificationItem) -> ShowItem {
|
||||
switch notif {
|
||||
case .repost(let evid, _):
|
||||
return .dontshow(events.lookup(evid))
|
||||
case .reply(let ev):
|
||||
return .show(ev)
|
||||
case .reaction(let evid, _):
|
||||
return .dontshow(events.lookup(evid))
|
||||
case .event_zap(let evid, _):
|
||||
return .dontshow(events.lookup(evid))
|
||||
case .profile_zap:
|
||||
return .show(nil)
|
||||
}
|
||||
}
|
||||
|
||||
struct NotificationItemView: View {
|
||||
let state: DamusState
|
||||
let item: NotificationItem
|
||||
|
||||
var show_item: ShowItem {
|
||||
notification_item_event(events: state.events, notif: item)
|
||||
}
|
||||
|
||||
func Item(_ ev: NostrEvent?) -> some View {
|
||||
Group {
|
||||
switch item {
|
||||
case .repost(_, let evgrp):
|
||||
EventGroupView(state: state, event: ev, group: .repost(evgrp))
|
||||
|
||||
case .event_zap(_, let zapgrp):
|
||||
EventGroupView(state: state, event: ev, group: .zap(zapgrp))
|
||||
|
||||
case .profile_zap(let grp):
|
||||
EventGroupView(state: state, event: nil, group: .profile_zap(grp))
|
||||
|
||||
case .reaction(_, let evgrp):
|
||||
EventGroupView(state: state, event: ev, group: .reaction(evgrp))
|
||||
|
||||
case .reply(let ev):
|
||||
NavigationLink(destination: ThreadView(state: state, thread: ThreadModel(event: ev, damus_state: state))) {
|
||||
EventView(damus: state, event: ev)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
Divider()
|
||||
.padding([.top,.bottom], 5)
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
switch show_item {
|
||||
case .show(let ev):
|
||||
Item(ev)
|
||||
|
||||
case .dontshow(let ev):
|
||||
if let ev {
|
||||
Item(ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let test_notification_item: NotificationItem = .repost("evid", test_event_group)
|
||||
|
||||
struct NotificationItemView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NotificationItemView(state: test_damus_state(), item: test_notification_item)
|
||||
}
|
||||
}
|
||||
137
damus/Views/Notifications/NotificationsView.swift
Normal file
137
damus/Views/Notifications/NotificationsView.swift
Normal file
@@ -0,0 +1,137 @@
|
||||
//
|
||||
// NotificationsView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum NotificationFilterState: String {
|
||||
case all
|
||||
case zaps
|
||||
case replies
|
||||
|
||||
func filter(_ item: NotificationItem) -> Bool {
|
||||
switch self {
|
||||
case .all:
|
||||
return true
|
||||
case .replies:
|
||||
return item.is_reply != nil
|
||||
case .zaps:
|
||||
return item.is_zap != nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NotificationsView: View {
|
||||
let state: DamusState
|
||||
@ObservedObject var notifications: NotificationsModel
|
||||
@State var filter_state: NotificationFilterState
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
init(state: DamusState, notifications: NotificationsModel) {
|
||||
self.state = state
|
||||
self._notifications = ObservedObject(initialValue: notifications)
|
||||
self._filter_state = State(initialValue: load_notification_filter_state(pubkey: state.pubkey))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $filter_state) {
|
||||
NotificationTab(NotificationFilterState.all)
|
||||
.tag(NotificationFilterState.all)
|
||||
.id(NotificationFilterState.all)
|
||||
|
||||
NotificationTab(NotificationFilterState.zaps)
|
||||
.tag(NotificationFilterState.zaps)
|
||||
.id(NotificationFilterState.zaps)
|
||||
|
||||
NotificationTab(NotificationFilterState.replies)
|
||||
.tag(NotificationFilterState.replies)
|
||||
.id(NotificationFilterState.replies)
|
||||
}
|
||||
.onChange(of: filter_state) { val in
|
||||
save_notification_filter_state(pubkey: state.pubkey, state: val)
|
||||
}
|
||||
.safeAreaInset(edge: .top, spacing: 0) {
|
||||
VStack(spacing: 0) {
|
||||
CustomPicker(selection: $filter_state, content: {
|
||||
Text("All", comment: "Label for filter for all notifications.")
|
||||
.tag(NotificationFilterState.all)
|
||||
|
||||
Text("Zaps", comment: "Label for filter for zap notifications.")
|
||||
.tag(NotificationFilterState.zaps)
|
||||
|
||||
Text("Mentions", comment: "Label for filter for seeing mention notifications (replies, etc).")
|
||||
.tag(NotificationFilterState.replies)
|
||||
|
||||
})
|
||||
Divider()
|
||||
.frame(height: 1)
|
||||
}
|
||||
.background(colorScheme == .dark ? Color.black : Color.white)
|
||||
}
|
||||
}
|
||||
|
||||
func NotificationTab(_ filter: NotificationFilterState) -> some View {
|
||||
ScrollViewReader { scroller in
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading) {
|
||||
Color.white.opacity(0)
|
||||
.id("startblock")
|
||||
.frame(height: 5)
|
||||
ForEach(notifications.notifications.filter(filter.filter), id: \.id) { item in
|
||||
NotificationItemView(state: state, item: item)
|
||||
}
|
||||
}
|
||||
.background(GeometryReader { proxy -> Color in
|
||||
DispatchQueue.main.async {
|
||||
handle_scroll_queue(proxy, queue: self.notifications)
|
||||
}
|
||||
return Color.clear
|
||||
})
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.coordinateSpace(name: "scroll")
|
||||
.onReceive(handle_notify(.scroll_to_top)) { notif in
|
||||
let _ = notifications.flush()
|
||||
self.notifications.should_queue = false
|
||||
scroll_to_event(scroller: scroller, id: "startblock", delay: 0.0, animate: true, anchor: .top)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
let _ = notifications.flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NotificationsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
NotificationsView(state: test_damus_state(), notifications: NotificationsModel())
|
||||
}
|
||||
}
|
||||
|
||||
func notification_filter_state_key(pubkey: String) -> String {
|
||||
return pk_setting_key(pubkey, key: "notification_filter_state")
|
||||
}
|
||||
|
||||
func load_notification_filter_state(pubkey: String) -> NotificationFilterState {
|
||||
let key = notification_filter_state_key(pubkey: pubkey)
|
||||
|
||||
guard let state_str = UserDefaults.standard.string(forKey: key) else {
|
||||
return .all
|
||||
}
|
||||
|
||||
guard let state = NotificationFilterState(rawValue: state_str) else {
|
||||
return .all
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
|
||||
func save_notification_filter_state(pubkey: String, state: NotificationFilterState) {
|
||||
let key = notification_filter_state_key(pubkey: pubkey)
|
||||
UserDefaults.standard.set(state.rawValue, forKey: key)
|
||||
}
|
||||
37
damus/Views/Notifications/ProfilePicturesView.swift
Normal file
37
damus/Views/Notifications/ProfilePicturesView.swift
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// ProfilePicturesView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ProfilePicturesView: View {
|
||||
let state: DamusState
|
||||
let events: [NostrEvent]
|
||||
|
||||
@State var nav_target: String? = nil
|
||||
@State var navigating: Bool = false
|
||||
|
||||
var body: some View {
|
||||
NavigationLink(destination: ProfileView(damus_state: state, pubkey: nav_target ?? ""), isActive: $navigating) {
|
||||
EmptyView()
|
||||
}
|
||||
HStack {
|
||||
ForEach(events.prefix(8)) { ev in
|
||||
ProfilePicView(pubkey: ev.pubkey, size: 32.0, highlight: .none, profiles: state.profiles)
|
||||
.onTapGesture {
|
||||
nav_target = ev.pubkey
|
||||
navigating = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProfilePicturesView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ProfilePicturesView(state: test_damus_state(), events: [test_event, test_event])
|
||||
}
|
||||
}
|
||||
@@ -36,41 +36,43 @@ struct ParticipantsView: View {
|
||||
Spacer()
|
||||
}
|
||||
VStack {
|
||||
ForEach(originalReferences.pRefs) { participant in
|
||||
let pubkey = participant.id
|
||||
HStack {
|
||||
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
let profile = damus_state.profiles.lookup(id: pubkey)
|
||||
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
|
||||
if let about = profile?.about {
|
||||
Text(FollowUserView.markdown.process(about))
|
||||
.lineLimit(3)
|
||||
.font(.footnote)
|
||||
ScrollView {
|
||||
ForEach(originalReferences.pRefs) { participant in
|
||||
let pubkey = participant.id
|
||||
HStack {
|
||||
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
let profile = damus_state.profiles.lookup(id: pubkey)
|
||||
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_friend_confirmed: false, show_nip5_domain: false)
|
||||
if let about = profile?.about {
|
||||
Text(FollowUserView.markdown.process(about))
|
||||
.lineLimit(3)
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 30))
|
||||
.foregroundColor(references.contains(participant) ? .purple : .gray)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 30))
|
||||
.foregroundColor(references.contains(participant) ? .purple : .gray)
|
||||
}
|
||||
.onTapGesture {
|
||||
if references.contains(participant) {
|
||||
references = references.filter {
|
||||
$0 != participant
|
||||
}
|
||||
} else {
|
||||
.onTapGesture {
|
||||
if references.contains(participant) {
|
||||
// Don't add it twice
|
||||
references = references.filter {
|
||||
$0 != participant
|
||||
}
|
||||
} else {
|
||||
references.append(participant)
|
||||
if references.contains(participant) {
|
||||
// Don't add it twice
|
||||
} else {
|
||||
references.append(participant)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
@@ -15,11 +15,14 @@ enum NostrPostResult {
|
||||
let POST_PLACEHOLDER = NSLocalizedString("Type your post here...", comment: "Text box prompt to ask user to type their post.")
|
||||
|
||||
struct PostView: View {
|
||||
@State var post: String = ""
|
||||
|
||||
@State var post: NSMutableAttributedString = NSMutableAttributedString()
|
||||
@FocusState var focus: Bool
|
||||
@State var showPrivateKeyWarning: Bool = false
|
||||
@State var attach_media: Bool = false
|
||||
@State var error: String? = nil
|
||||
|
||||
@StateObject var image_upload: ImageUploadModel = ImageUploadModel()
|
||||
|
||||
let replying_to: NostrEvent?
|
||||
let references: [ReferencedId]
|
||||
let damus_state: DamusState
|
||||
@@ -38,13 +41,20 @@ struct PostView: View {
|
||||
func dismiss() {
|
||||
self.presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
|
||||
|
||||
func send_post() {
|
||||
var kind: NostrKind = .text
|
||||
if replying_to?.known_kind == .chat {
|
||||
kind = .chat
|
||||
}
|
||||
let content = self.post.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
|
||||
post.enumerateAttributes(in: NSRange(location: 0, length: post.length), options: []) { attributes, range, stop in
|
||||
if let link = attributes[.link] as? String {
|
||||
post.replaceCharacters(in: range, with: link)
|
||||
}
|
||||
}
|
||||
|
||||
let content = self.post.string.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||||
let new_post = NostrPost(content: content, references: references, kind: kind)
|
||||
|
||||
NotificationCenter.default.post(name: .post, object: NostrPostResult.post(new_post))
|
||||
@@ -52,85 +62,167 @@ struct PostView: View {
|
||||
if let replying_to {
|
||||
damus_state.drafts.replies.removeValue(forKey: replying_to)
|
||||
} else {
|
||||
damus_state.drafts.post = ""
|
||||
damus_state.drafts.post = NSMutableAttributedString(string: "")
|
||||
}
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
var is_post_empty: Bool {
|
||||
return post.allSatisfy { $0.isWhitespace }
|
||||
return post.string.allSatisfy { $0.isWhitespace }
|
||||
}
|
||||
|
||||
var ImageButton: some View {
|
||||
Button(action: {
|
||||
attach_media = true
|
||||
}, label: {
|
||||
Image(systemName: "photo")
|
||||
})
|
||||
}
|
||||
|
||||
var AttachmentBar: some View {
|
||||
HStack(alignment: .center) {
|
||||
ImageButton
|
||||
.disabled(image_upload.progress != nil)
|
||||
}
|
||||
}
|
||||
|
||||
var PostButton: some View {
|
||||
Button(NSLocalizedString("Post", comment: "Button to post a note.")) {
|
||||
showPrivateKeyWarning = contentContainsPrivateKey(self.post.string)
|
||||
|
||||
var body: some View {
|
||||
if !showPrivateKeyWarning {
|
||||
self.send_post()
|
||||
}
|
||||
}
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.frame(width: 80, height: 30)
|
||||
.foregroundColor(.white)
|
||||
.background(LINEAR_GRADIENT)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
|
||||
var TextEntry: some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
TextViewWrapper(attributedText: $post)
|
||||
.focused($focus)
|
||||
.textInputAutocapitalization(.sentences)
|
||||
.onChange(of: post) { _ in
|
||||
if let replying_to {
|
||||
damus_state.drafts.replies[replying_to] = post
|
||||
} else {
|
||||
damus_state.drafts.post = post
|
||||
}
|
||||
}
|
||||
|
||||
if post.string.isEmpty {
|
||||
Text(POST_PLACEHOLDER)
|
||||
.padding(.top, 8)
|
||||
.padding(.leading, 4)
|
||||
.foregroundColor(Color(uiColor: .placeholderText))
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var TopBar: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
HStack(spacing: 5.0) {
|
||||
Button(NSLocalizedString("Cancel", comment: "Button to cancel out of posting a note.")) {
|
||||
self.cancel()
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
|
||||
if let error {
|
||||
Text(error)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if !is_post_empty {
|
||||
Button(NSLocalizedString("Post", comment: "Button to post a note.")) {
|
||||
showPrivateKeyWarning = contentContainsPrivateKey(self.post)
|
||||
|
||||
if !showPrivateKeyWarning {
|
||||
self.send_post()
|
||||
}
|
||||
}
|
||||
.font(.system(size: 14, weight: .bold))
|
||||
.frame(width: 80, height: 30)
|
||||
.foregroundColor(.white)
|
||||
.background(LINEAR_GRADIENT)
|
||||
.clipShape(Capsule())
|
||||
PostButton
|
||||
}
|
||||
}
|
||||
.frame(height: 30)
|
||||
.padding([.top, .bottom], 4)
|
||||
|
||||
if let progress = image_upload.progress {
|
||||
ProgressView(value: progress, total: 1.0)
|
||||
.progressViewStyle(.linear)
|
||||
}
|
||||
}
|
||||
.frame(height: 30)
|
||||
.padding([.top, .bottom], 4)
|
||||
}
|
||||
|
||||
func append_url(_ url: String) {
|
||||
let uploadedImageURL = NSMutableAttributedString(string: url)
|
||||
let combinedAttributedString = NSMutableAttributedString()
|
||||
combinedAttributedString.append(post)
|
||||
if !post.string.hasSuffix(" ") {
|
||||
combinedAttributedString.append(NSAttributedString(string: " "))
|
||||
}
|
||||
combinedAttributedString.append(uploadedImageURL)
|
||||
|
||||
// make sure we have a space at the end
|
||||
combinedAttributedString.append(NSAttributedString(string: " "))
|
||||
post = combinedAttributedString
|
||||
}
|
||||
|
||||
func handle_upload(image: UIImage) {
|
||||
let uploader = get_image_uploader(damus_state.pubkey)
|
||||
|
||||
Task.init {
|
||||
let res = await image_upload.start(img: image, uploader: uploader)
|
||||
|
||||
switch res {
|
||||
case .success(let url):
|
||||
append_url(url)
|
||||
|
||||
case .failed(let error):
|
||||
if let error {
|
||||
self.error = error.localizedDescription
|
||||
} else {
|
||||
self.error = "Error uploading image :("
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
let searching = get_searching_string(post.string)
|
||||
|
||||
TopBar
|
||||
|
||||
HStack(alignment: .top) {
|
||||
|
||||
ProfilePicView(pubkey: damus_state.pubkey, size: 45.0, highlight: .none, profiles: damus_state.profiles)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
ZStack(alignment: .topLeading) {
|
||||
|
||||
TextEditor(text: $post)
|
||||
.focused($focus)
|
||||
.textInputAutocapitalization(.sentences)
|
||||
.onChange(of: post) { _ in
|
||||
if let replying_to {
|
||||
damus_state.drafts.replies[replying_to] = post
|
||||
} else {
|
||||
damus_state.drafts.post = post
|
||||
}
|
||||
}
|
||||
|
||||
if post.isEmpty {
|
||||
Text(POST_PLACEHOLDER)
|
||||
.padding(.top, 8)
|
||||
.padding(.leading, 4)
|
||||
.foregroundColor(Color(uiColor: .placeholderText))
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
TextEntry
|
||||
}
|
||||
|
||||
.frame(maxHeight: searching == nil ? .infinity : 50)
|
||||
|
||||
// This if-block observes @ for tagging
|
||||
if let searching = get_searching_string(post) {
|
||||
VStack {
|
||||
Spacer()
|
||||
UserSearch(damus_state: damus_state, search: searching, post: $post)
|
||||
}.zIndex(1)
|
||||
if let searching {
|
||||
UserSearch(damus_state: damus_state, search: searching, post: $post)
|
||||
.frame(maxHeight: .infinity)
|
||||
} else {
|
||||
Divider()
|
||||
.padding([.bottom], 10)
|
||||
|
||||
AttachmentBar
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.sheet(isPresented: $attach_media) {
|
||||
ImagePicker(sourceType: .photoLibrary) { img in
|
||||
handle_upload(image: img)
|
||||
}
|
||||
}
|
||||
.onAppear() {
|
||||
if let replying_to {
|
||||
if damus_state.drafts.replies[replying_to] == nil {
|
||||
damus_state.drafts.replies[replying_to] = ""
|
||||
damus_state.drafts.post = NSMutableAttributedString(string: "")
|
||||
}
|
||||
if let p = damus_state.drafts.replies[replying_to] {
|
||||
post = p
|
||||
@@ -144,13 +236,12 @@ struct PostView: View {
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
if let replying_to, let reply = damus_state.drafts.replies[replying_to], reply.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
if let replying_to, let reply = damus_state.drafts.replies[replying_to], reply.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
damus_state.drafts.replies.removeValue(forKey: replying_to)
|
||||
} else if replying_to == nil && damus_state.drafts.post.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
damus_state.drafts.post = ""
|
||||
} else if replying_to == nil && damus_state.drafts.post.string.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
damus_state.drafts.post = NSMutableAttributedString(string : "")
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.alert(NSLocalizedString("Note contains \"nsec1\" private key. Are you sure?", comment: "Alert user that they might be attempting to paste a private key and ask them to confirm."), isPresented: $showPrivateKeyWarning, actions: {
|
||||
Button(NSLocalizedString("No", comment: "Button to cancel out of posting a note after being alerted that it looks like they might be posting a private key."), role: .cancel) {
|
||||
showPrivateKeyWarning = false
|
||||
|
||||
@@ -20,14 +20,55 @@ struct SearchedUser: Identifiable {
|
||||
struct UserSearch: View {
|
||||
let damus_state: DamusState
|
||||
let search: String
|
||||
@Binding var post: String
|
||||
|
||||
@Binding var post: NSMutableAttributedString
|
||||
|
||||
var users: [SearchedUser] {
|
||||
guard let contacts = damus_state.contacts.event else {
|
||||
return []
|
||||
return search_profiles(profiles: damus_state.profiles, search: search)
|
||||
}
|
||||
|
||||
return search_users(profiles: damus_state.profiles, tags: contacts.tags, search: search)
|
||||
return search_users_for_autocomplete(profiles: damus_state.profiles, tags: contacts.tags, search: search)
|
||||
}
|
||||
|
||||
func on_user_tapped(user: SearchedUser) {
|
||||
guard let pk = bech32_pubkey(user.pubkey) else {
|
||||
return
|
||||
}
|
||||
|
||||
// Remove all characters after the last '@'
|
||||
removeCharactersAfterLastAtSymbol()
|
||||
|
||||
// Create and append the user tag
|
||||
let tagAttributedString = createUserTag(for: user, with: pk)
|
||||
appendUserTag(tagAttributedString)
|
||||
}
|
||||
|
||||
private func removeCharactersAfterLastAtSymbol() {
|
||||
while post.string.last != "@" {
|
||||
post.deleteCharacters(in: NSRange(location: post.length - 1, length: 1))
|
||||
}
|
||||
post.deleteCharacters(in: NSRange(location: post.length - 1, length: 1))
|
||||
}
|
||||
|
||||
private func createUserTag(for user: SearchedUser, with pk: String) -> NSMutableAttributedString {
|
||||
let name = Profile.displayName(profile: user.profile, pubkey: pk).username
|
||||
let tagString = "@\(name)\u{200B} "
|
||||
|
||||
let tagAttributedString = NSMutableAttributedString(string: tagString,
|
||||
attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18.0),
|
||||
NSAttributedString.Key.link: "@\(pk)"])
|
||||
tagAttributedString.removeAttribute(.link, range: NSRange(location: tagAttributedString.length - 2, length: 2))
|
||||
tagAttributedString.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.label], range: NSRange(location: tagAttributedString.length - 2, length: 2))
|
||||
|
||||
return tagAttributedString
|
||||
}
|
||||
|
||||
private func appendUserTag(_ tagAttributedString: NSMutableAttributedString) {
|
||||
let mutableString = NSMutableAttributedString()
|
||||
mutableString.append(post)
|
||||
mutableString.append(tagAttributedString)
|
||||
post = mutableString
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -36,10 +77,7 @@ struct UserSearch: View {
|
||||
ForEach(users) { user in
|
||||
UserView(damus_state: damus_state, pubkey: user.pubkey)
|
||||
.onTapGesture {
|
||||
guard let pk = bech32_pubkey(user.pubkey) else {
|
||||
return
|
||||
}
|
||||
post = post.replacingOccurrences(of: "@"+search, with: "@"+pk+" ")
|
||||
on_user_tapped(user: user)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,7 +87,7 @@ struct UserSearch: View {
|
||||
|
||||
struct UserSearch_Previews: PreviewProvider {
|
||||
static let search: String = "jb55"
|
||||
@State static var post: String = "some @jb55"
|
||||
@State static var post: NSMutableAttributedString = NSMutableAttributedString(string: "some @jb55")
|
||||
|
||||
static var previews: some View {
|
||||
UserSearch(damus_state: test_damus_state(), search: search, post: $post)
|
||||
@@ -57,11 +95,11 @@ struct UserSearch_Previews: PreviewProvider {
|
||||
}
|
||||
|
||||
|
||||
func search_users(profiles: Profiles, tags: [[String]], search _search: String) -> [SearchedUser] {
|
||||
func search_users_for_autocomplete(profiles: Profiles, tags: [[String]], search _search: String) -> [SearchedUser] {
|
||||
var seen_user = Set<String>()
|
||||
let search = _search.lowercased()
|
||||
|
||||
return tags.reduce(into: Array<SearchedUser>()) { arr, tag in
|
||||
var matches = tags.reduce(into: Array<SearchedUser>()) { arr, tag in
|
||||
guard tag.count >= 2 && tag[0] == "p" else {
|
||||
return
|
||||
}
|
||||
@@ -79,11 +117,29 @@ func search_users(profiles: Profiles, tags: [[String]], search _search: String)
|
||||
|
||||
let profile = profiles.lookup(id: pubkey)
|
||||
|
||||
guard ((petname?.lowercased().hasPrefix(search) ?? false) || (profile?.name?.lowercased().hasPrefix(search) ?? false)) else {
|
||||
guard ((petname?.lowercased().hasPrefix(search) ?? false) ||
|
||||
(profile?.name?.lowercased().hasPrefix(search) ?? false) ||
|
||||
(profile?.display_name?.lowercased().hasPrefix(search) ?? false)) else {
|
||||
return
|
||||
}
|
||||
|
||||
let searched_user = SearchedUser(petname: petname, profile: profile, pubkey: pubkey)
|
||||
arr.append(searched_user)
|
||||
}
|
||||
|
||||
// search profile cache as well
|
||||
for tup in profiles.profiles.enumerated() {
|
||||
let pk = tup.element.key
|
||||
let prof = tup.element.value.profile
|
||||
|
||||
guard !seen_user.contains(pk) else {
|
||||
continue
|
||||
}
|
||||
|
||||
if let match = profile_search_matches(profiles: profiles, profile: prof, pubkey: pk, search: search) {
|
||||
matches.append(match)
|
||||
}
|
||||
}
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
96
damus/Views/Profile/EventProfileName.swift
Normal file
96
damus/Views/Profile/EventProfileName.swift
Normal file
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// EventProfileName.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-03-14.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// Profile Name used when displaying an event in the timeline
|
||||
struct EventProfileName: View {
|
||||
let damus_state: DamusState
|
||||
let pubkey: String
|
||||
let profile: Profile?
|
||||
let prefix: String
|
||||
|
||||
let show_friend_confirmed: Bool
|
||||
|
||||
@State var display_name: DisplayName?
|
||||
@State var nip05: NIP05?
|
||||
|
||||
let size: EventViewKind
|
||||
|
||||
init(pubkey: String, profile: Profile?, damus: DamusState, show_friend_confirmed: Bool, size: EventViewKind = .normal) {
|
||||
self.damus_state = damus
|
||||
self.pubkey = pubkey
|
||||
self.profile = profile
|
||||
self.prefix = ""
|
||||
self.show_friend_confirmed = show_friend_confirmed
|
||||
self.size = size
|
||||
}
|
||||
|
||||
init(pubkey: String, profile: Profile?, prefix: String, damus: DamusState, show_friend_confirmed: Bool, size: EventViewKind = .normal) {
|
||||
self.damus_state = damus
|
||||
self.pubkey = pubkey
|
||||
self.profile = profile
|
||||
self.prefix = prefix
|
||||
self.show_friend_confirmed = show_friend_confirmed
|
||||
self.size = size
|
||||
}
|
||||
|
||||
var friend_icon: String? {
|
||||
return get_friend_icon(contacts: damus_state.contacts, pubkey: pubkey, show_confirmed: show_friend_confirmed)
|
||||
}
|
||||
|
||||
var current_nip05: NIP05? {
|
||||
nip05 ?? damus_state.profiles.is_validated(pubkey)
|
||||
}
|
||||
|
||||
var current_display_name: DisplayName {
|
||||
return display_name ?? Profile.displayName(profile: profile, pubkey: pubkey)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 2) {
|
||||
switch current_display_name {
|
||||
case .one(let one):
|
||||
Text(one)
|
||||
.font(.body.weight(.bold))
|
||||
|
||||
case .both(let both):
|
||||
Text(both.display_name)
|
||||
.font(.body.weight(.bold))
|
||||
|
||||
Text(verbatim: "@\(both.username)")
|
||||
.foregroundColor(.gray)
|
||||
.font(eventviewsize_to_font(size))
|
||||
}
|
||||
|
||||
if let nip05 = current_nip05 {
|
||||
NIP05Badge(nip05: nip05, pubkey: pubkey, contacts: damus_state.contacts, show_domain: false, clickable: false)
|
||||
}
|
||||
|
||||
if let frend = friend_icon, current_nip05 == nil {
|
||||
Label("", systemImage: frend)
|
||||
.foregroundColor(.gray)
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.profile_updated)) { notif in
|
||||
let update = notif.object as! ProfileUpdate
|
||||
if update.pubkey != pubkey {
|
||||
return
|
||||
}
|
||||
display_name = Profile.displayName(profile: update.profile, pubkey: pubkey)
|
||||
nip05 = damus_state.profiles.is_validated(pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct EventProfileName_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
EventProfileName(pubkey: "pk", profile: nil, damus: test_damus_state(), show_friend_confirmed: true)
|
||||
}
|
||||
}
|
||||
46
damus/Views/Profile/MaybeAnonPfpView.swift
Normal file
46
damus/Views/Profile/MaybeAnonPfpView.swift
Normal file
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// MaybeAnonPfpView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-02-26.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct MaybeAnonPfpView: View {
|
||||
let state: DamusState
|
||||
let is_anon: Bool
|
||||
let pubkey: String
|
||||
|
||||
init(state: DamusState, event: NostrEvent, pubkey: String) {
|
||||
self.state = state
|
||||
self.is_anon = event_is_anonymous(ev: event)
|
||||
self.pubkey = pubkey
|
||||
}
|
||||
|
||||
init(state: DamusState, is_anon: Bool, pubkey: String) {
|
||||
self.state = state
|
||||
self.is_anon = is_anon
|
||||
self.pubkey = pubkey
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if is_anon {
|
||||
Image(systemName: "person.fill.questionmark")
|
||||
.font(.largeTitle)
|
||||
.frame(width: PFP_SIZE, height: PFP_SIZE)
|
||||
} else {
|
||||
NavigationLink(destination: ProfileView(damus_state: state, pubkey: pubkey)) {
|
||||
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: state.profiles)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MaybeAnonPfpView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MaybeAnonPfpView(state: test_damus_state(), is_anon: true, pubkey: "anon")
|
||||
}
|
||||
}
|
||||
105
damus/Views/Profile/ProfileName.swift
Normal file
105
damus/Views/Profile/ProfileName.swift
Normal file
@@ -0,0 +1,105 @@
|
||||
//
|
||||
// ProfileName.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2022-04-16.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
func get_friend_icon(contacts: Contacts, pubkey: String, show_confirmed: Bool) -> String? {
|
||||
if !show_confirmed {
|
||||
return nil
|
||||
}
|
||||
|
||||
if contacts.is_friend_or_self(pubkey) {
|
||||
return "person.fill.checkmark"
|
||||
}
|
||||
|
||||
if contacts.is_friend_of_friend(pubkey) {
|
||||
return "person.fill.and.arrow.left.and.arrow.right"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
struct ProfileName: View {
|
||||
let damus_state: DamusState
|
||||
let pubkey: String
|
||||
let profile: Profile?
|
||||
let prefix: String
|
||||
|
||||
let show_friend_confirmed: Bool
|
||||
let show_nip5_domain: Bool
|
||||
|
||||
@State var display_name: DisplayName?
|
||||
@State var nip05: NIP05?
|
||||
|
||||
init(pubkey: String, profile: Profile?, damus: DamusState, show_friend_confirmed: Bool, show_nip5_domain: Bool = true) {
|
||||
self.pubkey = pubkey
|
||||
self.profile = profile
|
||||
self.prefix = ""
|
||||
self.show_friend_confirmed = show_friend_confirmed
|
||||
self.show_nip5_domain = show_nip5_domain
|
||||
self.damus_state = damus
|
||||
}
|
||||
|
||||
init(pubkey: String, profile: Profile?, prefix: String, damus: DamusState, show_friend_confirmed: Bool, show_nip5_domain: Bool = true) {
|
||||
self.pubkey = pubkey
|
||||
self.profile = profile
|
||||
self.prefix = prefix
|
||||
self.damus_state = damus
|
||||
self.show_friend_confirmed = show_friend_confirmed
|
||||
self.show_nip5_domain = show_nip5_domain
|
||||
}
|
||||
|
||||
var friend_icon: String? {
|
||||
return get_friend_icon(contacts: damus_state.contacts, pubkey: pubkey, show_confirmed: show_friend_confirmed)
|
||||
}
|
||||
|
||||
var current_nip05: NIP05? {
|
||||
nip05 ?? damus_state.profiles.is_validated(pubkey)
|
||||
}
|
||||
|
||||
var nip05_color: Color {
|
||||
return get_nip05_color(pubkey: pubkey, contacts: damus_state.contacts)
|
||||
}
|
||||
|
||||
var current_display_name: DisplayName {
|
||||
return display_name ?? Profile.displayName(profile: profile, pubkey: pubkey)
|
||||
}
|
||||
|
||||
var name_choice: String {
|
||||
return prefix == "@" ? current_display_name.username : current_display_name.display_name
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 2) {
|
||||
Text(verbatim: "\(prefix)\(name_choice)")
|
||||
.font(.body)
|
||||
.fontWeight(prefix == "@" ? .none : .bold)
|
||||
if let nip05 = current_nip05 {
|
||||
NIP05Badge(nip05: nip05, pubkey: pubkey, contacts: damus_state.contacts, show_domain: show_nip5_domain, clickable: true)
|
||||
}
|
||||
if let friend = friend_icon, current_nip05 == nil {
|
||||
Image(systemName: friend)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.profile_updated)) { notif in
|
||||
let update = notif.object as! ProfileUpdate
|
||||
if update.pubkey != pubkey {
|
||||
return
|
||||
}
|
||||
display_name = Profile.displayName(profile: update.profile, pubkey: pubkey)
|
||||
nip05 = damus_state.profiles.is_validated(pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProfileName_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ProfileName(pubkey:
|
||||
test_damus_state().pubkey, profile: make_test_profile(), damus: test_damus_state(), show_friend_confirmed: true)
|
||||
}
|
||||
}
|
||||
@@ -17,10 +17,20 @@ struct ProfileNameView: View {
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if let real_name = profile?.display_name {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(real_name)
|
||||
VStack(alignment: .leading) {
|
||||
switch Profile.displayName(profile: profile, pubkey: pubkey) {
|
||||
case .one:
|
||||
HStack(alignment: .center, spacing: spacing) {
|
||||
ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true)
|
||||
.font(.title3.weight(.bold))
|
||||
if follows_you {
|
||||
FollowsYou()
|
||||
}
|
||||
}
|
||||
case .both(let both):
|
||||
Text(both.display_name)
|
||||
.font(.title3.weight(.bold))
|
||||
|
||||
HStack(alignment: .center, spacing: spacing) {
|
||||
ProfileName(pubkey: pubkey, profile: profile, prefix: "@", damus: damus, show_friend_confirmed: true)
|
||||
.font(.callout)
|
||||
@@ -30,22 +40,12 @@ struct ProfileNameView: View {
|
||||
FollowsYou()
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
KeyView(pubkey: pubkey)
|
||||
.pubkey_context_menu(bech32_pubkey: pubkey)
|
||||
}
|
||||
} else {
|
||||
VStack(alignment: .leading) {
|
||||
HStack(alignment: .center, spacing: spacing) {
|
||||
ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true)
|
||||
.font(.title3.weight(.bold))
|
||||
if follows_you {
|
||||
FollowsYou()
|
||||
}
|
||||
}
|
||||
KeyView(pubkey: pubkey)
|
||||
.pubkey_context_menu(bech32_pubkey: pubkey)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
KeyView(pubkey: pubkey)
|
||||
.pubkey_context_menu(bech32_pubkey: pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ struct InnerProfilePicView: View {
|
||||
.placeholder { _ in
|
||||
Placeholder
|
||||
}
|
||||
.scaledToFill()
|
||||
}
|
||||
.frame(width: size, height: size)
|
||||
.clipShape(Circle())
|
||||
@@ -49,6 +49,21 @@ func follow_btn_enabled_state(_ fs: FollowState) -> Bool {
|
||||
}
|
||||
}
|
||||
|
||||
func followersCountString(_ count: Int, locale: Locale = Locale.current) -> String {
|
||||
let format = localizedStringFormat(key: "followers_count", locale: locale)
|
||||
return String(format: format, locale: locale, count)
|
||||
}
|
||||
|
||||
func followingCountString(_ count: Int, locale: Locale = Locale.current) -> String {
|
||||
let format = localizedStringFormat(key: "following_count", locale: locale)
|
||||
return String(format: format, locale: locale, count)
|
||||
}
|
||||
|
||||
func relaysCountString(_ count: Int, locale: Locale = Locale.current) -> String {
|
||||
let format = localizedStringFormat(key: "relays_count", locale: locale)
|
||||
return String(format: format, locale: locale, count)
|
||||
}
|
||||
|
||||
struct EditButton: View {
|
||||
let damus_state: DamusState
|
||||
|
||||
@@ -100,8 +115,6 @@ struct ProfileView: View {
|
||||
static let markdown = Markdown()
|
||||
|
||||
@State private var selected_tab: ProfileTab = .posts
|
||||
@StateObject var profile: ProfileModel
|
||||
@StateObject var followers: FollowersModel
|
||||
@State private var showingEditProfile = false
|
||||
@State var showing_select_wallet: Bool = false
|
||||
@State var is_zoomed: Bool = false
|
||||
@@ -110,6 +123,21 @@ struct ProfileView: View {
|
||||
@State var filter_state : FilterState = .posts
|
||||
@State var yOffset: CGFloat = 0
|
||||
|
||||
@StateObject var profile: ProfileModel
|
||||
@StateObject var followers: FollowersModel
|
||||
|
||||
init(damus_state: DamusState, profile: ProfileModel, followers: FollowersModel) {
|
||||
self.damus_state = damus_state
|
||||
self._profile = StateObject(wrappedValue: profile)
|
||||
self._followers = StateObject(wrappedValue: followers)
|
||||
}
|
||||
|
||||
init(damus_state: DamusState, pubkey: String) {
|
||||
self.damus_state = damus_state
|
||||
self._profile = StateObject(wrappedValue: ProfileModel(pubkey: pubkey, damus: damus_state))
|
||||
self._followers = StateObject(wrappedValue: FollowersModel(damus_state: damus_state, target: pubkey))
|
||||
}
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
@Environment(\.openURL) var openURL
|
||||
@@ -156,6 +184,7 @@ struct ProfileView: View {
|
||||
|
||||
}
|
||||
.frame(height: bannerHeight)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
|
||||
var navbarHeight: CGFloat {
|
||||
@@ -305,7 +334,7 @@ struct ProfileView: View {
|
||||
actionSection(profile_data: profile_data)
|
||||
}
|
||||
|
||||
let follows_you = profile.follows(pubkey: damus_state.pubkey)
|
||||
let follows_you = profile.pubkey != damus_state.pubkey && profile.follows(pubkey: damus_state.pubkey)
|
||||
ProfileNameView(pubkey: profile.pubkey, profile: profile_data, follows_you: follows_you, damus: damus_state)
|
||||
}
|
||||
}
|
||||
@@ -319,7 +348,8 @@ struct ProfileView: View {
|
||||
.foregroundColor(.gray)
|
||||
} else {
|
||||
let followerCount = followers.count!
|
||||
Text("\(Text(verbatim: "\(followerCount)").font(.subheadline.weight(.medium))) \(Text(String(format: Bundle.main.localizedString(forKey: "followers_count", value: nil, table: nil), followerCount)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many people are following a user. In source English, the first variable is the number of followers, and the second variable is 'Follower' or 'Followers'.")
|
||||
let noun_text = Text(verbatim: "\(followersCountString(followerCount))").font(.subheadline).foregroundColor(.gray)
|
||||
Text("\(Text("\(followerCount)").font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many people are following a user. In source English, the first variable is the number of followers, and the second variable is 'Follower' or 'Followers'.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -343,7 +373,8 @@ struct ProfileView: View {
|
||||
let following_model = FollowingModel(damus_state: damus_state, contacts: contacts)
|
||||
NavigationLink(destination: FollowingView(damus_state: damus_state, following: following_model, whos: profile.pubkey)) {
|
||||
HStack {
|
||||
Text("\(Text(verbatim: "\(profile.following)").font(.subheadline.weight(.medium))) \(Text("Following", comment: "Part of a larger sentence to describe how many profiles a user is following.").font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.")
|
||||
let noun_text = Text(verbatim: "\(followingCountString(profile.following))").font(.subheadline).foregroundColor(.gray)
|
||||
Text("\(Text("\(profile.following)").font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.")
|
||||
}
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
@@ -366,7 +397,8 @@ struct ProfileView: View {
|
||||
|
||||
if let relays = profile.relays {
|
||||
// Only open relay config view if the user is logged in with private key and they are looking at their own profile.
|
||||
let relay_text = Text("\(Text(verbatim: "\(relays.keys.count)").font(.subheadline.weight(.medium))) \(Text(String(format: Bundle.main.localizedString(forKey: "relays_count", value: nil, table: nil), relays.keys.count)).font(.subheadline).foregroundColor(.gray))", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.")
|
||||
let noun_text = Text(verbatim: "\(relaysCountString(relays.keys.count))").font(.subheadline).foregroundColor(.gray)
|
||||
let relay_text = Text("\(Text("\(relays.keys.count)").font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.")
|
||||
if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user {
|
||||
NavigationLink(destination: RelayConfigView(state: damus_state)) {
|
||||
relay_text
|
||||
@@ -443,9 +475,7 @@ struct ProfileView: View {
|
||||
struct ProfileView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let ds = test_damus_state()
|
||||
let followers = FollowersModel(damus_state: ds, target: ds.pubkey)
|
||||
let profile_model = ProfileModel(pubkey: ds.pubkey, damus: ds)
|
||||
ProfileView(damus_state: ds, profile: profile_model, followers: followers)
|
||||
ProfileView(damus_state: ds, pubkey: ds.pubkey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,26 +521,25 @@ struct KeyView: View {
|
||||
let bech32 = bech32_pubkey(pubkey) ?? pubkey
|
||||
|
||||
HStack {
|
||||
RoundedRectangle(cornerRadius: 11)
|
||||
.frame(height: 22)
|
||||
.foregroundColor(fillColor())
|
||||
.overlay(
|
||||
HStack {
|
||||
Button {
|
||||
copyPubkey(bech32)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Public Key", comment: "Label indicating that the text is a user's public account key."), systemImage: "key.fill")
|
||||
.font(.custom("key", size: 12.0))
|
||||
.labelStyle(IconOnlyLabelStyle())
|
||||
.foregroundStyle(hex_to_rgb(pubkey))
|
||||
.symbolRenderingMode(.palette)
|
||||
}
|
||||
.padding(.leading,4)
|
||||
Text(abbrev_pubkey(bech32, amount: 16))
|
||||
.font(.footnote)
|
||||
.foregroundColor(keyColor())
|
||||
}
|
||||
)
|
||||
HStack {
|
||||
Button {
|
||||
copyPubkey(bech32)
|
||||
} label: {
|
||||
Label(NSLocalizedString("Public Key", comment: "Label indicating that the text is a user's public account key."), systemImage: "key.fill")
|
||||
.font(.custom("key", size: 12.0))
|
||||
.labelStyle(IconOnlyLabelStyle())
|
||||
.foregroundStyle(hex_to_rgb(pubkey))
|
||||
.symbolRenderingMode(.palette)
|
||||
}
|
||||
.padding(.trailing, 2)
|
||||
Text(verbatim: "\(abbrev_pubkey(bech32, amount: 16))")
|
||||
.font(.footnote)
|
||||
.foregroundColor(keyColor())
|
||||
}
|
||||
.padding(2)
|
||||
.padding([.leading, .trailing], 3)
|
||||
.background(RoundedRectangle(cornerRadius: 11).foregroundColor(fillColor()))
|
||||
|
||||
if isCopied != true {
|
||||
Button {
|
||||
copyPubkey(bech32)
|
||||
@@ -1,168 +0,0 @@
|
||||
//
|
||||
// ProfileName.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2022-04-16.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
func get_friend_icon(contacts: Contacts, pubkey: String, show_confirmed: Bool) -> String? {
|
||||
if !show_confirmed {
|
||||
return nil
|
||||
}
|
||||
|
||||
if contacts.is_friend_or_self(pubkey) {
|
||||
return "person.fill.checkmark"
|
||||
}
|
||||
|
||||
if contacts.is_friend_of_friend(pubkey) {
|
||||
return "person.fill.and.arrow.left.and.arrow.right"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
struct ProfileName: View {
|
||||
let damus_state: DamusState
|
||||
let pubkey: String
|
||||
let profile: Profile?
|
||||
let prefix: String
|
||||
|
||||
let show_friend_confirmed: Bool
|
||||
let show_nip5_domain: Bool
|
||||
|
||||
@State var display_name: String?
|
||||
@State var nip05: NIP05?
|
||||
|
||||
init(pubkey: String, profile: Profile?, damus: DamusState, show_friend_confirmed: Bool, show_nip5_domain: Bool = true) {
|
||||
self.pubkey = pubkey
|
||||
self.profile = profile
|
||||
self.prefix = ""
|
||||
self.show_friend_confirmed = show_friend_confirmed
|
||||
self.show_nip5_domain = show_nip5_domain
|
||||
self.damus_state = damus
|
||||
}
|
||||
|
||||
init(pubkey: String, profile: Profile?, prefix: String, damus: DamusState, show_friend_confirmed: Bool, show_nip5_domain: Bool = true) {
|
||||
self.pubkey = pubkey
|
||||
self.profile = profile
|
||||
self.prefix = prefix
|
||||
self.damus_state = damus
|
||||
self.show_friend_confirmed = show_friend_confirmed
|
||||
self.show_nip5_domain = show_nip5_domain
|
||||
}
|
||||
|
||||
var friend_icon: String? {
|
||||
return get_friend_icon(contacts: damus_state.contacts, pubkey: pubkey, show_confirmed: show_friend_confirmed)
|
||||
}
|
||||
|
||||
var current_nip05: NIP05? {
|
||||
nip05 ?? damus_state.profiles.is_validated(pubkey)
|
||||
}
|
||||
|
||||
var nip05_color: Color {
|
||||
return get_nip05_color(pubkey: pubkey, contacts: damus_state.contacts)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 2) {
|
||||
Text(prefix + String(display_name ?? Profile.displayName(profile: profile, pubkey: pubkey)))
|
||||
.font(.body)
|
||||
.fontWeight(prefix == "@" ? .none : .bold)
|
||||
if let nip05 = current_nip05 {
|
||||
NIP05Badge(nip05: nip05, pubkey: pubkey, contacts: damus_state.contacts, show_domain: show_nip5_domain, clickable: true)
|
||||
}
|
||||
if let friend = friend_icon, current_nip05 == nil {
|
||||
Image(systemName: friend)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.profile_updated)) { notif in
|
||||
let update = notif.object as! ProfileUpdate
|
||||
if update.pubkey != pubkey {
|
||||
return
|
||||
}
|
||||
display_name = Profile.displayName(profile: update.profile, pubkey: pubkey)
|
||||
nip05 = damus_state.profiles.is_validated(pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Profile Name used when displaying an event in the timeline
|
||||
struct EventProfileName: View {
|
||||
let damus_state: DamusState
|
||||
let pubkey: String
|
||||
let profile: Profile?
|
||||
let prefix: String
|
||||
|
||||
let show_friend_confirmed: Bool
|
||||
|
||||
@State var display_name: String?
|
||||
@State var nip05: NIP05?
|
||||
|
||||
let size: EventViewKind
|
||||
|
||||
init(pubkey: String, profile: Profile?, damus: DamusState, show_friend_confirmed: Bool, size: EventViewKind = .normal) {
|
||||
self.damus_state = damus
|
||||
self.pubkey = pubkey
|
||||
self.profile = profile
|
||||
self.prefix = ""
|
||||
self.show_friend_confirmed = show_friend_confirmed
|
||||
self.size = size
|
||||
}
|
||||
|
||||
init(pubkey: String, profile: Profile?, prefix: String, damus: DamusState, show_friend_confirmed: Bool, size: EventViewKind = .normal) {
|
||||
self.damus_state = damus
|
||||
self.pubkey = pubkey
|
||||
self.profile = profile
|
||||
self.prefix = prefix
|
||||
self.show_friend_confirmed = show_friend_confirmed
|
||||
self.size = size
|
||||
}
|
||||
|
||||
var friend_icon: String? {
|
||||
return get_friend_icon(contacts: damus_state.contacts, pubkey: pubkey, show_confirmed: show_friend_confirmed)
|
||||
}
|
||||
|
||||
var current_nip05: NIP05? {
|
||||
nip05 ?? damus_state.profiles.is_validated(pubkey)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 2) {
|
||||
if let real_name = profile?.display_name {
|
||||
Text(real_name)
|
||||
.font(.body.weight(.bold))
|
||||
.padding([.trailing], 2)
|
||||
|
||||
Text("@" + String(display_name ?? Profile.displayName(profile: profile, pubkey: pubkey)))
|
||||
.foregroundColor(Color("DamusMediumGrey"))
|
||||
.font(eventviewsize_to_font(size))
|
||||
} else {
|
||||
Text(String(display_name ?? Profile.displayName(profile: profile, pubkey: pubkey)))
|
||||
.font(eventviewsize_to_font(size))
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
|
||||
if let nip05 = current_nip05 {
|
||||
NIP05Badge(nip05: nip05, pubkey: pubkey, contacts: damus_state.contacts, show_domain: false, clickable: false)
|
||||
}
|
||||
|
||||
if let frend = friend_icon, current_nip05 == nil {
|
||||
Label("", systemImage: frend)
|
||||
.foregroundColor(.gray)
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.profile_updated)) { notif in
|
||||
let update = notif.object as! ProfileUpdate
|
||||
if update.pubkey != pubkey {
|
||||
return
|
||||
}
|
||||
display_name = Profile.displayName(profile: update.profile, pubkey: pubkey)
|
||||
nip05 = damus_state.profiles.is_validated(pubkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
private struct ImageContainerView: View {
|
||||
struct ProfileImageContainerView: View {
|
||||
|
||||
let url: URL?
|
||||
|
||||
@@ -68,7 +68,7 @@ struct ProfileZoomView: View {
|
||||
.ignoresSafeArea()
|
||||
|
||||
ZoomableScrollView {
|
||||
ImageContainerView(url: get_profile_url(picture: nil, pubkey: pubkey, profiles: profiles))
|
||||
ProfileImageContainerView(url: get_profile_url(picture: nil, pubkey: pubkey, profiles: profiles))
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.padding(.top, Theme.safeAreaInsets?.top)
|
||||
.padding(.bottom, Theme.safeAreaInsets?.bottom)
|
||||
|
||||
@@ -26,7 +26,7 @@ struct RelayFilterView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Text("To filter your \(timeline.rawValue) feed, please choose applicable relays from the list below:", comment: "Instructions on how to filter a specific timeline feed by choosing relay servers to filter on.")
|
||||
Text("Please choose relays from the list below to filter the current feed:", comment: "Instructions on how to filter a specific timeline feed by choosing relay servers to filter on.")
|
||||
.padding()
|
||||
.padding(.top, 20)
|
||||
.padding(.bottom, 0)
|
||||
|
||||
@@ -25,24 +25,48 @@ struct RecommendedRelayView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(relay)
|
||||
Spacer()
|
||||
if add_button {
|
||||
if let privkey = damus.keypair.privkey {
|
||||
Button(NSLocalizedString("Add", comment: "Button to add recommended relay server.")) {
|
||||
guard let ev_before_add = damus.contacts.event else {
|
||||
return
|
||||
}
|
||||
guard let ev_after_add = add_relay(ev: ev_before_add, privkey: privkey, current_relays: damus.pool.descriptors, relay: relay, info: .rw) else {
|
||||
return
|
||||
}
|
||||
process_contact_event(state: damus, ev: ev_after_add)
|
||||
damus.pool.send(.event(ev_after_add))
|
||||
ZStack {
|
||||
HStack {
|
||||
RelayType(is_paid: damus.relay_metadata.lookup(relay_id: relay)?.is_paid ?? false)
|
||||
Text(relay).layoutPriority(1)
|
||||
|
||||
if let meta = damus.relay_metadata.lookup(relay_id: relay) {
|
||||
NavigationLink ( destination:
|
||||
RelayDetailView(state: damus, relay: relay, nip11: meta)
|
||||
){
|
||||
EmptyView()
|
||||
}
|
||||
.opacity(0.0)
|
||||
|
||||
Spacer()
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(Color.accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
.swipeActions {
|
||||
if add_button {
|
||||
if let privkey = damus.keypair.privkey {
|
||||
AddAction(privkey: privkey)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func AddAction(privkey: String) -> some View {
|
||||
Button {
|
||||
guard let ev_before_add = damus.contacts.event else {
|
||||
return
|
||||
}
|
||||
guard let ev_after_add = add_relay(ev: ev_before_add, privkey: privkey, current_relays: damus.pool.descriptors, relay: relay, info: .rw) else {
|
||||
return
|
||||
}
|
||||
process_contact_event(state: damus, ev: ev_after_add)
|
||||
damus.pool.send(.event(ev_after_add))
|
||||
} label: {
|
||||
Label(NSLocalizedString("Add Relay", comment: "Button to add recommended relay server."), systemImage: "plus.circle")
|
||||
}
|
||||
.tint(.accentColor)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,12 +26,13 @@ struct ReplyView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Replying to:", comment: "Indicating that the user is replying to the following listed people.")
|
||||
|
||||
HStack(alignment: .top) {
|
||||
let names = references.pRefs
|
||||
.map { pubkey in
|
||||
let pk = pubkey.ref_id
|
||||
let prof = damus.profiles.lookup(id: pk)
|
||||
return Profile.displayName(profile: prof, pubkey: pk)
|
||||
return Profile.displayName(profile: prof, pubkey: pk).username
|
||||
}
|
||||
.joined(separator: ", ")
|
||||
Text(names)
|
||||
@@ -44,16 +45,25 @@ struct ReplyView: View {
|
||||
.sheet(isPresented: $participantsShown) {
|
||||
ParticipantsView(damus_state: damus, references: $references, originalReferences: $originalReferences)
|
||||
}
|
||||
ScrollView {
|
||||
EventView(damus: damus, event: replying_to, has_action_bar: false)
|
||||
|
||||
ScrollViewReader { scroller in
|
||||
ScrollView {
|
||||
EventView(damus: damus, event: replying_to, options: [.no_action_bar])
|
||||
|
||||
PostView(replying_to: replying_to, references: references, damus_state: damus)
|
||||
.frame(minHeight: 500, maxHeight: .infinity)
|
||||
.id("post")
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
.onAppear {
|
||||
scroll_to_event(scroller: scroller, id: "post", delay: 1.0, animate: true, anchor: .top)
|
||||
}
|
||||
}
|
||||
PostView(replying_to: replying_to, references: references, damus_state: damus)
|
||||
}
|
||||
.onAppear {
|
||||
references = gather_reply_ids(our_pubkey: damus.pubkey, from: replying_to)
|
||||
originalReferences = references
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
|
||||
|
||||
37
damus/Views/Reposts/RepostedEvent.swift
Normal file
37
damus/Views/Reposts/RepostedEvent.swift
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// RepostedEvent.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-03-23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct RepostedEvent: View {
|
||||
let damus: DamusState
|
||||
let event: NostrEvent
|
||||
let inner_ev: NostrEvent
|
||||
let options: EventViewOptions
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
let prof = damus.profiles.lookup(id: event.pubkey)
|
||||
let booster_profile = ProfileView(damus_state: damus, pubkey: event.pubkey)
|
||||
|
||||
NavigationLink(destination: booster_profile) {
|
||||
Reposted(damus: damus, pubkey: event.pubkey, profile: prof)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
|
||||
//SelectedEventView(damus: damus, event: inner_ev, size: .normal)
|
||||
TextEvent(damus: damus, event: inner_ev, pubkey: inner_ev.pubkey, options: options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RepostedEvent_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
RepostedEvent(damus: test_damus_state(), event: test_event, inner_ev: test_event, options: [])
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user