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