Compare commits
98 Commits
preferred-
...
profile_co
| Author | SHA1 | Date | |
|---|---|---|---|
|
caa4bfe864
|
|||
|
|
4324b185fe | ||
|
|
1ab9b30b85 | ||
|
|
81cf6ad297 | ||
|
|
1b3be3a13b | ||
|
|
3a2ce04d6b | ||
|
|
981821a6bc | ||
|
|
98f83769bd | ||
|
|
7684f53281 | ||
|
|
15af686a58 | ||
|
|
aad8f9e8d4 | ||
| b2ee44c0ab | |||
|
|
a696ac5084 | ||
|
|
28237c3a63 | ||
|
|
1cae4640c0 | ||
|
|
21a07d54cb | ||
|
|
1efd07b852 | ||
|
|
e5eb7d44a2 | ||
|
|
ec9a89ee4d | ||
|
|
4741c2a3e8 | ||
|
|
0111c5e2dc | ||
|
|
bed4e00b53 | ||
|
|
bf14d7138a | ||
|
|
0c5da08a42 | ||
|
|
a6e123e928 | ||
|
|
69b1173e08 | ||
|
|
c3326213e9 | ||
|
325109d7b8
|
|||
|
|
f16d76605b
|
||
|
|
3eee1b205a
|
||
|
|
9545c6446d
|
||
|
|
40a75f65ab
|
||
|
|
98f42c9896
|
||
|
|
5c22989675
|
||
|
999f16f6a4
|
|||
|
|
3f5fd6eee8
|
||
|
|
7c195aa75c
|
||
|
|
2071efc129
|
||
|
|
9db2e9b464
|
||
|
|
5f6cb568ff
|
||
|
|
045399a065
|
||
|
|
1b526143d0
|
||
|
|
8a046c0d1b
|
||
|
|
2893e4234d
|
||
|
|
973a5ce2cb
|
||
|
|
1e81e90341
|
||
| 9e7943e0e9 | |||
|
|
bb7ac4fea5 | ||
|
|
05d0e15359 | ||
|
|
d4d17fcbad | ||
|
|
c21d29a897 | ||
|
|
6e117ac39c | ||
|
|
79407f17e8 | ||
|
|
72c19fc411 | ||
|
|
24c3e61a4b | ||
|
|
74d5bee1f6 | ||
|
|
8066fa1bf8 | ||
| 26df547605 | |||
| a97532b90d | |||
|
|
e8ba1ec806 | ||
|
|
e8c265a4d8 | ||
|
|
b33dc63fe4 | ||
|
|
c4852f1309 | ||
|
|
39a4be7076 | ||
|
|
50c7edc420 | ||
|
|
67fa3c1ce5 | ||
|
|
cd671da3e7 | ||
|
|
3b60ca04f1 | ||
|
|
e2e58499f5 | ||
|
5cadf09665
|
|||
|
|
1ca7b3462f
|
||
|
|
8a552d2b0f
|
||
|
|
9fa0f18f78
|
||
|
|
db672ca048
|
||
|
|
18ad73cd35
|
||
|
|
5719e9b37e
|
||
|
|
9fb2b3c0e5
|
||
|
|
5ec66feb06
|
||
|
|
ccc301cfcc
|
||
|
|
c1b9d0b55e
|
||
|
|
d9daa27016
|
||
|
|
fa3b5d57ed
|
||
|
|
7c3e598ca6
|
||
|
|
563d5c7881
|
||
|
|
b8cba0ee17
|
||
|
|
8556586af4
|
||
|
|
5fc52bb31b
|
||
|
|
a92c9f2c38
|
||
|
|
61e137696e
|
||
|
|
8fc3b124da
|
||
|
|
7852822295
|
||
|
|
85e55953b3
|
||
|
|
077f633f33
|
||
|
|
1c3d1598a3
|
||
|
|
314608627e
|
||
|
|
aeecc04b29
|
||
|
|
341389d438
|
||
|
|
fbeae64123
|
52
.github/ISSUE_TEMPLATE/app_release.md
vendored
Normal file
52
.github/ISSUE_TEMPLATE/app_release.md
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: App release process
|
||||
about: Begin preparing for a new app release
|
||||
title: 'Release: '
|
||||
labels: release-tasks
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
A new version release. Please attempt to follow the release process steps below in the order they are shown.
|
||||
|
||||
## TestFlight release candidates
|
||||
|
||||
### Release candidate 1
|
||||
|
||||
**Version:** _[Enter full build information for the release candidate, including major and minor version number, build number, and commit hash]_
|
||||
|
||||
1. [ ] Merge in all needed changes to `master`
|
||||
2. [ ] Check CI, make sure it is passing
|
||||
3. [ ] Prepare preliminary changelog as a draft PR: _[Enter PR link to changelog here]_
|
||||
4. [ ] Make a _release_ build and submit to the internal TestFlight group via our new Release candidate workflow in Xcode Cloud.
|
||||
5. [ ] Prepare short screencast style video with main changes for the announcement
|
||||
6. [ ] Publish release build to these TestFlight groups:
|
||||
- [ ] Alpha testers group
|
||||
- [ ] Translators group
|
||||
- [ ] Purple group
|
||||
7. [ ] Publish announcement on Nostr
|
||||
|
||||
|
||||
_[Duplicate this release candidate section if there is more than one release candidate]_
|
||||
|
||||
|
||||
## App Store release
|
||||
|
||||
1. [ ] Release candidate checks:
|
||||
- [ ] Release candidate has been on Purple TestFlight for at least one week
|
||||
- [ ] No blocker issues came from feedback from Purple users (double-check)
|
||||
- [ ] Check with stakeholders
|
||||
- [ ] Check with developers & product for any release showstoppers (e.g., critical newfound bugs)
|
||||
2. [ ] Thorough check on release notes
|
||||
3. [ ] Submit to App Store review (with manual publishing setting enabled)
|
||||
4. [ ] Get App Store approval from Apple
|
||||
5. [ ] Prepare announcement
|
||||
7. [ ] Publish on the App Store and make announcement
|
||||
8. [ ] Publish changelog and tag commit hash corresponding to the release
|
||||
9. [ ] Perform a version bump on the repository, in preparation for the next release
|
||||
|
||||
|
||||
## Notes/others
|
||||
|
||||
_Enter any relevant notes here_
|
||||
|
||||
5
ACKNOWLEDGEMENTS.md
Normal file
5
ACKNOWLEDGEMENTS.md
Normal file
@@ -0,0 +1,5 @@
|
||||
### Acknowledgements and licenses
|
||||
|
||||
1. This product contains code derived from [Nostr SDK iOS](https://github.com/nostr-sdk/nostr-sdk-ios). [License](https://github.com/nostr-sdk/nostr-sdk-ios/blob/40df800c6749d7ce0b6fd7328e76cbc0dc71c87b/LICENSE)
|
||||
2. This product includes software developed by the "Marcin Krzyzanowski" (http://krzyzanowskim.com/). [License](https://github.com/krzyzanowskim/CryptoSwift/blob/e74bbbfbef939224b242ae7c342a90e60b88b5ce/LICENSE)
|
||||
|
||||
53
CHANGELOG.md
53
CHANGELOG.md
@@ -1,3 +1,56 @@
|
||||
## [1.12.3] - 2025-02-06
|
||||
|
||||
### Added
|
||||
|
||||
- Purple members who have been active for more than a year now get a special badge (Daniel D’Aquino)
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved clarity of the mute button to indicate it can be used for blocking a user (Daniel D’Aquino)
|
||||
- Made the microphone access request message more clear to users (Daniel D’Aquino)
|
||||
|
||||
[v1.12.3]: https://github.com/damus-io/damus/releases/tag/v1.12.3
|
||||
|
||||
|
||||
## [1.12](https://github.com/damus-io/damus/releases/tag/v1.12) - 2024-12-20
|
||||
|
||||
### Added
|
||||
|
||||
- Render Gif and video files while composing posts (Swift Coder)
|
||||
- Add profile info text in stretchable banner with follow button (Swift Coder)
|
||||
- Paste Gif image similar to jpeg and png files (Swift Coder)
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved UX around the label for searching words (Daniel D’Aquino)
|
||||
- Improved accessibility support on some elements (Daniel D’Aquino)
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed issue where the "next" button would appear hidden and hard to click on the create account view (Daniel D’Aquino)
|
||||
- Fix non scrollable wallet screen (Swift Coder)
|
||||
- Fixed suggested users category titles to be localizable (Terry Yiu)
|
||||
- Fixed GradientFollowButton to have consistent width and autoscale text limited to 1 line (Terry Yiu)
|
||||
- Fixed right-to-left localization issues (Terry Yiu)
|
||||
- Fixed AddMuteItemView to trim leading and trailing whitespaces from mute text and disallow adding text with only whitespaces (Terry Yiu)
|
||||
- Fixed SideMenuView text to autoscale and limit to 1 line (Terry Yiu)
|
||||
- Fixed an issue where a profile would need to be input twice in the search to be found (Daniel D’Aquino)
|
||||
- Fixed non-breaking spaces in localized strings (Terry Yiu)
|
||||
- Fixed localization issue on Add mute item button (Terry Yiu)
|
||||
- Replace non-breaking spaces with regular spaces as Apple's NSLocalizedString macro does not seem to work with it (Terry Yiu)
|
||||
- Fixed localization issues in RelayConfigView (Terry Yiu)
|
||||
- Fix duplicate uploads (Swift Coder)
|
||||
- Remove duplicate pubkey from Follow Suggestion list (Swift Coder)
|
||||
- Fix Page control indicator (Swift Coder)
|
||||
- Fix damus sharing issues (Swift Coder)
|
||||
- Fixed issue where banner edit button is unclickable (Daniel D’Aquino)
|
||||
- Handle empty notification pages by displaying suitable text (Swift Coder)
|
||||
|
||||
[v1.12](https://github.com/damus-io/damus/releases/tag/v1.12): [https://github.com/damus-io/damus/releases/tag/v1.12]
|
||||
|
||||
|
||||
## [v1.11(10)](https://github.com/damus-io/damus/releases/tag/v1.11-10) - 2024-11-18
|
||||
|
||||
### Added
|
||||
|
||||
@@ -8,7 +8,7 @@ A twitter-like [nostr][nostr] client for iPhone, iPad and MacOS.
|
||||
|
||||
[nostr]: https://github.com/fiatjaf/nostr
|
||||
|
||||
## How is Damus better than twitter?
|
||||
## How is Damus better than X/Twitter?
|
||||
There are no toxic algorithms.\
|
||||
You can send or receive zaps (satoshis) without asking for permission.\
|
||||
[There is no central database](https://fiatjaf.com/nostr.html). Therefore, Damus is censorship resistant.\
|
||||
|
||||
@@ -875,7 +875,6 @@
|
||||
82D6FC682CD99F7900C925F4 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363AA128296A7E006E126D /* SearchView.swift */; };
|
||||
82D6FC692CD99F7900C925F4 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
|
||||
82D6FC6A2CD99F7900C925F4 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3AC7A02835A81400E1F516 /* SetupView.swift */; };
|
||||
82D6FC6B2CD99F7900C925F4 /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadView.swift */; };
|
||||
82D6FC6C2CD99F7900C925F4 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; };
|
||||
82D6FC6D2CD99F7900C925F4 /* UserRelaysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB55EF4295E679D007FD187 /* UserRelaysView.swift */; };
|
||||
82D6FC6E2CD99F7900C925F4 /* SideMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D9A8C2968520300A295DE /* SideMenuView.swift */; };
|
||||
@@ -1046,6 +1045,12 @@
|
||||
D703D7B62C67118200A400EA /* String+extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B9472A9AD44700DC3548 /* String+extension.swift */; };
|
||||
D703D7B72C67118F00A400EA /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; };
|
||||
D703D7B82C6711A000A400EA /* NativeObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C32B9462A9AD44700DC3548 /* NativeObject.swift */; };
|
||||
D706C5AF2D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D706C5AE2D5D31B20027C627 /* AutoSaveIndicatorView.swift */; };
|
||||
D706C5B02D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D706C5AE2D5D31B20027C627 /* AutoSaveIndicatorView.swift */; };
|
||||
D706C5B12D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D706C5AE2D5D31B20027C627 /* AutoSaveIndicatorView.swift */; };
|
||||
D706C5B72D602A110027C627 /* QueueableNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D706C5B62D602A050027C627 /* QueueableNotify.swift */; };
|
||||
D706C5B82D602A110027C627 /* QueueableNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D706C5B62D602A050027C627 /* QueueableNotify.swift */; };
|
||||
D706C5B92D602A110027C627 /* QueueableNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D706C5B62D602A050027C627 /* QueueableNotify.swift */; };
|
||||
D70A3B172B02DCE5008BD568 /* NotificationFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = D70A3B162B02DCE5008BD568 /* NotificationFormatter.swift */; };
|
||||
D70D90982CDED61800CD0534 /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = D70D90972CDED61800CD0534 /* CodeScanner */; };
|
||||
D70D909C2CDED7B200CD0534 /* CodeScanner in Frameworks */ = {isa = PBXBuildFile; productRef = D70D909B2CDED7B200CD0534 /* CodeScanner */; };
|
||||
@@ -1388,7 +1393,6 @@
|
||||
D73E5F602C6A97F5007EB227 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */; };
|
||||
D73E5F612C6A97F5007EB227 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C363AA128296A7E006E126D /* SearchView.swift */; };
|
||||
D73E5F622C6A97F5007EB227 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
|
||||
D73E5F642C6A97F5007EB227 /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadView.swift */; };
|
||||
D73E5F652C6A97F5007EB227 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; };
|
||||
D73E5F662C6A97F5007EB227 /* UserRelaysView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB55EF4295E679D007FD187 /* UserRelaysView.swift */; };
|
||||
D73E5F682C6A97F5007EB227 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; };
|
||||
@@ -1445,10 +1449,22 @@
|
||||
D74AAFD22B155E78006CF0F4 /* WalletConnect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09612A098D0E00943473 /* WalletConnect.swift */; };
|
||||
D74AAFD42B155ECB006CF0F4 /* Zaps+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */; };
|
||||
D74AAFD62B155F0C006CF0F4 /* WalletConnect+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */; };
|
||||
D74EA08A2D2BF2A7002290DD /* URLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D767066E2C8BB3CE00F09726 /* URLHandler.swift */; };
|
||||
D74EA08E2D2E271E002290DD /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA08D2D2E271E002290DD /* ErrorView.swift */; };
|
||||
D74EA08F2D2E271E002290DD /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA08D2D2E271E002290DD /* ErrorView.swift */; };
|
||||
D74EA0902D2E271E002290DD /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA08D2D2E271E002290DD /* ErrorView.swift */; };
|
||||
D74EA0912D2E3464002290DD /* URLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D767066E2C8BB3CE00F09726 /* URLHandler.swift */; };
|
||||
D74EA0932D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */; };
|
||||
D74EA0942D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */; };
|
||||
D74EA0952D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */; };
|
||||
D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F43092B23F0BE00425B75 /* DamusPurple.swift */; };
|
||||
D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; };
|
||||
D753CEAA2BE9DE04001C3A5D /* MutingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D753CEA92BE9DE04001C3A5D /* MutingTests.swift */; };
|
||||
D755B28D2D3E7D8800BBEEFA /* NIP37Draft.swift in Sources */ = {isa = PBXBuildFile; fileRef = D755B28C2D3E7D7D00BBEEFA /* NIP37Draft.swift */; };
|
||||
D755B28E2D3E7D8800BBEEFA /* NIP37Draft.swift in Sources */ = {isa = PBXBuildFile; fileRef = D755B28C2D3E7D7D00BBEEFA /* NIP37Draft.swift */; };
|
||||
D755B28F2D3E7D8800BBEEFA /* NIP37Draft.swift in Sources */ = {isa = PBXBuildFile; fileRef = D755B28C2D3E7D7D00BBEEFA /* NIP37Draft.swift */; };
|
||||
D76556D62B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */; };
|
||||
D767066F2C8BB3CF00F09726 /* URLHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D767066E2C8BB3CE00F09726 /* URLHandler.swift */; };
|
||||
D76874F32AE3632B00FB0F68 /* ProfileZapLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */; };
|
||||
D773BC5F2C6D538500349F0A /* CommentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D773BC5E2C6D538500349F0A /* CommentItem.swift */; };
|
||||
D773BC602C6D538500349F0A /* CommentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D773BC5E2C6D538500349F0A /* CommentItem.swift */; };
|
||||
@@ -1490,6 +1506,7 @@
|
||||
D7ADD3E22B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7ADD3E12B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift */; };
|
||||
D7B76C902C825042003A16CB /* PushNotificationClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */; };
|
||||
D7B76C912C82507F003A16CB /* NIP98AuthenticatedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C6787D2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift */; };
|
||||
D7BEE6F92D37B37400CF659F /* DraftTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7BEE6F82D37B37400CF659F /* DraftTests.swift */; };
|
||||
D7C48C0B2D12DE0C00A3BACF /* SwiftyCrop in Frameworks */ = {isa = PBXBuildFile; productRef = D7C48C0A2D12DE0C00A3BACF /* SwiftyCrop */; };
|
||||
D7C48C0D2D12E34900A3BACF /* SwiftyCrop in Frameworks */ = {isa = PBXBuildFile; productRef = D7C48C0C2D12E34900A3BACF /* SwiftyCrop */; };
|
||||
D7C48C0F2D12E35600A3BACF /* SwiftyCrop in Frameworks */ = {isa = PBXBuildFile; productRef = D7C48C0E2D12E35600A3BACF /* SwiftyCrop */; };
|
||||
@@ -1593,6 +1610,19 @@
|
||||
D7D2A3812BF815D000E4B42B /* PushNotificationClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */; };
|
||||
D7D68FF92C9E01BE0015A515 /* KFClickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D68FF82C9E01B60015A515 /* KFClickable.swift */; };
|
||||
D7D68FFA2C9E01BE0015A515 /* KFClickable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D68FF82C9E01B60015A515 /* KFClickable.swift */; };
|
||||
D7DB1FDE2D5A78CE00CF06DA /* NIP44.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB1FDD2D5A78CE00CF06DA /* NIP44.swift */; };
|
||||
D7DB1FDF2D5A78CE00CF06DA /* NIP44.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB1FDD2D5A78CE00CF06DA /* NIP44.swift */; };
|
||||
D7DB1FE02D5A78CE00CF06DA /* NIP44.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB1FDD2D5A78CE00CF06DA /* NIP44.swift */; };
|
||||
D7DB1FE42D5A9AC900CF06DA /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = D7DB1FE32D5A9AC900CF06DA /* CryptoSwift */; };
|
||||
D7DB1FE82D5A9F5300CF06DA /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = D7DB1FE72D5A9F5300CF06DA /* CryptoSwift */; };
|
||||
D7DB1FEA2D5A9F5A00CF06DA /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = D7DB1FE92D5A9F5A00CF06DA /* CryptoSwift */; };
|
||||
D7DB1FEC2D5A9F6500CF06DA /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = D7DB1FEB2D5A9F6500CF06DA /* CryptoSwift */; };
|
||||
D7DB1FEE2D5AC51B00CF06DA /* NIP44v2EncryptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB1FED2D5AC50F00CF06DA /* NIP44v2EncryptionTests.swift */; };
|
||||
D7DB1FF12D5AC5D700CF06DA /* nip44.vectors.json in Resources */ = {isa = PBXBuildFile; fileRef = D7DB1FF02D5AC5D700CF06DA /* nip44.vectors.json */; };
|
||||
D7DB1FF32D5AC5EA00CF06DA /* LICENSES in Resources */ = {isa = PBXBuildFile; fileRef = D7DB1FF22D5AC5E400CF06DA /* LICENSES */; };
|
||||
D7DB93052D66A44100DA1EE5 /* Undistractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */; };
|
||||
D7DB93062D66A44100DA1EE5 /* Undistractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */; };
|
||||
D7DB93072D66A44100DA1EE5 /* Undistractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */; };
|
||||
D7DBD41F2B02F15E002A6197 /* NostrKind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */; };
|
||||
D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */; };
|
||||
D7EB00B02CD59C8D00660C07 /* PresentFullScreenItemNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */; };
|
||||
@@ -1639,7 +1669,6 @@
|
||||
E0EE9DD42B8E5FEA00F3002D /* ImageProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0EE9DD32B8E5FEA00F3002D /* ImageProcessing.swift */; };
|
||||
E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */; };
|
||||
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; };
|
||||
E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9E4ED0A295867B900DD7078 /* ThreadView.swift */; };
|
||||
F71694EA2A662232001F4053 /* OnboardingSuggestionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71694E92A662232001F4053 /* OnboardingSuggestionsView.swift */; };
|
||||
F71694EC2A662292001F4053 /* SuggestedUsersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F71694EB2A662292001F4053 /* SuggestedUsersViewModel.swift */; };
|
||||
F71694EE2A6624F9001F4053 /* suggested_users.json in Resources */ = {isa = PBXBuildFile; fileRef = F71694ED2A6624F9001F4053 /* suggested_users.json */; };
|
||||
@@ -2393,6 +2422,8 @@
|
||||
D703D7222C66E47100A400EA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
D703D7262C66E47100A400EA /* highlighter action extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "highlighter action extension.entitlements"; sourceTree = "<group>"; };
|
||||
D703D72A2C66F29500A400EA /* getSelection.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = getSelection.js; sourceTree = "<group>"; };
|
||||
D706C5AE2D5D31B20027C627 /* AutoSaveIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoSaveIndicatorView.swift; sourceTree = "<group>"; };
|
||||
D706C5B62D602A050027C627 /* QueueableNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueueableNotify.swift; sourceTree = "<group>"; };
|
||||
D70A3B162B02DCE5008BD568 /* NotificationFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationFormatter.swift; sourceTree = "<group>"; };
|
||||
D7100C552B76F8E600C59298 /* PurpleViewPrimitives.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurpleViewPrimitives.swift; sourceTree = "<group>"; };
|
||||
D7100C572B76FC8400C59298 /* MarketingContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketingContentView.swift; sourceTree = "<group>"; };
|
||||
@@ -2425,10 +2456,14 @@
|
||||
D74AAFCE2B155D8C006CF0F4 /* ZapDataModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapDataModel.swift; sourceTree = "<group>"; };
|
||||
D74AAFD32B155ECB006CF0F4 /* Zaps+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Zaps+.swift"; sourceTree = "<group>"; };
|
||||
D74AAFD52B155F0C006CF0F4 /* WalletConnect+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WalletConnect+.swift"; sourceTree = "<group>"; };
|
||||
D74EA08D2D2E271E002290DD /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
|
||||
D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableNostrEventView.swift; sourceTree = "<group>"; };
|
||||
D74F43092B23F0BE00425B75 /* DamusPurple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurple.swift; sourceTree = "<group>"; };
|
||||
D74F430B2B23FB9B00425B75 /* StoreObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreObserver.swift; sourceTree = "<group>"; };
|
||||
D753CEA92BE9DE04001C3A5D /* MutingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutingTests.swift; sourceTree = "<group>"; };
|
||||
D755B28C2D3E7D7D00BBEEFA /* NIP37Draft.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP37Draft.swift; sourceTree = "<group>"; };
|
||||
D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleWelcomeView.swift; sourceTree = "<group>"; };
|
||||
D767066E2C8BB3CE00F09726 /* URLHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLHandler.swift; sourceTree = "<group>"; };
|
||||
D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZapLinkView.swift; sourceTree = "<group>"; };
|
||||
D773BC5E2C6D538500349F0A /* CommentItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentItem.swift; sourceTree = "<group>"; };
|
||||
D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileActionSheetView.swift; sourceTree = "<group>"; };
|
||||
@@ -2450,6 +2485,7 @@
|
||||
D7ADD3DD2B53854300F104C4 /* DamusPurpleURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleURL.swift; sourceTree = "<group>"; };
|
||||
D7ADD3DF2B538D4200F104C4 /* DamusPurpleURLSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleURLSheetView.swift; sourceTree = "<group>"; };
|
||||
D7ADD3E12B538E3500F104C4 /* DamusPurpleVerifyNpubView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleVerifyNpubView.swift; sourceTree = "<group>"; };
|
||||
D7BEE6F82D37B37400CF659F /* DraftTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DraftTests.swift; sourceTree = "<group>"; };
|
||||
D7C6787D2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP98AuthenticatedRequest.swift; sourceTree = "<group>"; };
|
||||
D7CB5D3D2B116DAD00AD4105 /* NotificationsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsManager.swift; sourceTree = "<group>"; };
|
||||
D7CB5D442B116FE800AD4105 /* Contacts+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Contacts+.swift"; sourceTree = "<group>"; };
|
||||
@@ -2462,6 +2498,11 @@
|
||||
D7CBD1D52B8D509800BFD889 /* DamusPurpleImpendingExpirationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleImpendingExpirationTests.swift; sourceTree = "<group>"; };
|
||||
D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationClient.swift; sourceTree = "<group>"; };
|
||||
D7D68FF82C9E01B60015A515 /* KFClickable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFClickable.swift; sourceTree = "<group>"; };
|
||||
D7DB1FDD2D5A78CE00CF06DA /* NIP44.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP44.swift; sourceTree = "<group>"; };
|
||||
D7DB1FED2D5AC50F00CF06DA /* NIP44v2EncryptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP44v2EncryptionTests.swift; sourceTree = "<group>"; };
|
||||
D7DB1FF02D5AC5D700CF06DA /* nip44.vectors.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = nip44.vectors.json; sourceTree = "<group>"; };
|
||||
D7DB1FF22D5AC5E400CF06DA /* LICENSES */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSES; sourceTree = "<group>"; };
|
||||
D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Undistractor.swift; sourceTree = "<group>"; };
|
||||
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = "<group>"; };
|
||||
D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentFullScreenItemNotify.swift; sourceTree = "<group>"; };
|
||||
D7EDED1B2B1178FE0018B19C /* NoteContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContent.swift; sourceTree = "<group>"; };
|
||||
@@ -2483,7 +2524,6 @@
|
||||
E0EE9DD32B8E5FEA00F3002D /* ImageProcessing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProcessing.swift; sourceTree = "<group>"; };
|
||||
E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = "<group>"; };
|
||||
E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; };
|
||||
E9E4ED0A295867B900DD7078 /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; };
|
||||
F71694E92A662232001F4053 /* OnboardingSuggestionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingSuggestionsView.swift; sourceTree = "<group>"; };
|
||||
F71694EB2A662292001F4053 /* SuggestedUsersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedUsersViewModel.swift; sourceTree = "<group>"; };
|
||||
F71694ED2A6624F9001F4053 /* suggested_users.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = suggested_users.json; sourceTree = "<group>"; };
|
||||
@@ -2507,6 +2547,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
|
||||
D7DB1FE42D5A9AC900CF06DA /* CryptoSwift in Frameworks */,
|
||||
3A0A30BB2C21397A00F8C9BC /* EmojiPicker in Frameworks */,
|
||||
D70D90982CDED61800CD0534 /* CodeScanner in Frameworks */,
|
||||
D7C48C0B2D12DE0C00A3BACF /* SwiftyCrop in Frameworks */,
|
||||
@@ -2537,6 +2578,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
82D6FC862CD9A4A600C925F4 /* MarkdownUI in Frameworks */,
|
||||
D7DB1FEC2D5A9F6500CF06DA /* CryptoSwift in Frameworks */,
|
||||
82D6FC8A2CD9A54600C925F4 /* SwipeActions in Frameworks */,
|
||||
D7F360292CEBBE34009D34DA /* CodeScanner in Frameworks */,
|
||||
D7C48C0D2D12E34900A3BACF /* SwiftyCrop in Frameworks */,
|
||||
@@ -2552,6 +2594,7 @@
|
||||
files = (
|
||||
D703D7AF2C670FB700A400EA /* MarkdownUI in Frameworks */,
|
||||
D73E5F9D2C6AA8E3007EB227 /* SwipeActions in Frameworks */,
|
||||
D7DB1FE82D5A9F5300CF06DA /* CryptoSwift in Frameworks */,
|
||||
D73E5F762C6A997E007EB227 /* EmojiPicker in Frameworks */,
|
||||
D703D7192C66E47100A400EA /* UniformTypeIdentifiers.framework in Frameworks */,
|
||||
D7C48C0F2D12E35600A3BACF /* SwiftyCrop in Frameworks */,
|
||||
@@ -2567,6 +2610,7 @@
|
||||
files = (
|
||||
D789D1202AFEFBF20083A7AB /* secp256k1 in Frameworks */,
|
||||
D7EDED312B1290B80018B19C /* MarkdownUI in Frameworks */,
|
||||
D7DB1FEA2D5A9F5A00CF06DA /* CryptoSwift in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -2723,6 +2767,7 @@
|
||||
D7D2A3802BF815D000E4B42B /* PushNotificationClient.swift */,
|
||||
5CC8529C2BD741CD0039FFC5 /* HighlightEvent.swift */,
|
||||
D773BC5E2C6D538500349F0A /* CommentItem.swift */,
|
||||
D767066E2C8BB3CE00F09726 /* URLHandler.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
@@ -2732,6 +2777,8 @@
|
||||
children = (
|
||||
4C0C03982A61E27B0098B3B8 /* bool_setting.wasm */,
|
||||
4C0C03972A61E27B0098B3B8 /* primal.wasm */,
|
||||
D7DB1FF22D5AC5E400CF06DA /* LICENSES */,
|
||||
D7DB1FF02D5AC5D700CF06DA /* nip44.vectors.json */,
|
||||
);
|
||||
name = Fixtures;
|
||||
sourceTree = "<group>";
|
||||
@@ -3063,6 +3110,7 @@
|
||||
4C75EFA227FA576C0006080F /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D74EA08C2D2E26E6002290DD /* ErrorHandling */,
|
||||
D7D68FF72C9E01A80015A515 /* Utils */,
|
||||
D78DB85D2C20FE9E00F0AB12 /* Chat */,
|
||||
D71AC4CA2BA8E3320076268E /* Extensions */,
|
||||
@@ -3119,7 +3167,6 @@
|
||||
4C363AA128296A7E006E126D /* SearchView.swift */,
|
||||
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */,
|
||||
4C3AC7A02835A81400E1F516 /* SetupView.swift */,
|
||||
E9E4ED0A295867B900DD7078 /* ThreadView.swift */,
|
||||
4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */,
|
||||
4CB55EF4295E679D007FD187 /* UserRelaysView.swift */,
|
||||
647D9A8C2968520300A295DE /* SideMenuView.swift */,
|
||||
@@ -3134,6 +3181,7 @@
|
||||
D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */,
|
||||
D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */,
|
||||
D71AD8FC2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift */,
|
||||
D74EA0922D2E77B9002290DD /* LoadableNostrEventView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@@ -3195,6 +3243,7 @@
|
||||
4C7FF7D628233637009601DB /* Util */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D7DB93042D66A43B00DA1EE5 /* Undistractor.swift */,
|
||||
D73E5F7E2C6AA066007EB227 /* DamusAliases.swift */,
|
||||
E04A37C52B544F090029650D /* URIParsing.swift */,
|
||||
4C1D4FB02A7958E60024F453 /* VersionInfo.swift */,
|
||||
@@ -3310,6 +3359,7 @@
|
||||
4CA3529C2A76AE47003BB08B /* Notify */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D706C5B62D602A050027C627 /* QueueableNotify.swift */,
|
||||
D7EB00AF2CD59C8300660C07 /* PresentFullScreenItemNotify.swift */,
|
||||
4C86F7C52A76C51100EC0817 /* AttachedWalletNotify.swift */,
|
||||
4C9D6D152B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift */,
|
||||
@@ -3557,6 +3607,8 @@
|
||||
4CE6DEE527F7A08100C66700 /* damus */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D7DB1FDC2D5A77E500CF06DA /* NIP44 */,
|
||||
D755B28B2D3E7D6500BBEEFA /* NIP37 */,
|
||||
4C45E5002BED4CE10025A428 /* NIP10 */,
|
||||
4C1D4FB32A7967990024F453 /* build-git-hash.txt */,
|
||||
4CA3529C2A76AE47003BB08B /* Notify */,
|
||||
@@ -3593,6 +3645,7 @@
|
||||
4CE6DEF627F7A08200C66700 /* damusTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D7DB1FED2D5AC50F00CF06DA /* NIP44v2EncryptionTests.swift */,
|
||||
D7A0D8742D1FE66A00DCBE59 /* EditPictureControlTests.swift */,
|
||||
E06336A72B7582D600A88E6B /* Assets */,
|
||||
D72A2D032AD9C165002AFF62 /* Mocking */,
|
||||
@@ -3603,6 +3656,7 @@
|
||||
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */,
|
||||
4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */,
|
||||
E02B54172B4DFADA0077FF42 /* Bech32ObjectTests.swift */,
|
||||
D7BEE6F82D37B37400CF659F /* DraftTests.swift */,
|
||||
4C363A9F2828A8DD006E126D /* LikeTests.swift */,
|
||||
4C363A9D2828A822006E126D /* ReplyTests.swift */,
|
||||
4CE6DEF727F7A08200C66700 /* damusTests.swift */,
|
||||
@@ -3722,6 +3776,7 @@
|
||||
4CF0ABF42985CD4200D66079 /* Posting */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D706C5AE2D5D31B20027C627 /* AutoSaveIndicatorView.swift */,
|
||||
4CF0ABF52985CD5500D66079 /* UserSearch.swift */,
|
||||
);
|
||||
path = Posting;
|
||||
@@ -3853,6 +3908,14 @@
|
||||
path = Mocking;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D74EA08C2D2E26E6002290DD /* ErrorHandling */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D74EA08D2D2E271E002290DD /* ErrorView.swift */,
|
||||
);
|
||||
path = ErrorHandling;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D74F43082B23F09300425B75 /* Purple */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -3866,6 +3929,14 @@
|
||||
path = Purple;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D755B28B2D3E7D6500BBEEFA /* NIP37 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D755B28C2D3E7D7D00BBEEFA /* NIP37Draft.swift */,
|
||||
);
|
||||
path = NIP37;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D78DB85D2C20FE9E00F0AB12 /* Chat */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -3906,6 +3977,14 @@
|
||||
path = Utils;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D7DB1FDC2D5A77E500CF06DA /* NIP44 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D7DB1FDD2D5A78CE00CF06DA /* NIP44.swift */,
|
||||
);
|
||||
path = NIP44;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
E06336A72B7582D600A88E6B /* Assets */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -3972,6 +4051,7 @@
|
||||
D78DB8582C1CE9CA00F0AB12 /* SwipeActions */,
|
||||
D70D90972CDED61800CD0534 /* CodeScanner */,
|
||||
D7C48C0A2D12DE0C00A3BACF /* SwiftyCrop */,
|
||||
D7DB1FE32D5A9AC900CF06DA /* CryptoSwift */,
|
||||
);
|
||||
productName = damus;
|
||||
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
|
||||
@@ -4038,6 +4118,7 @@
|
||||
82D6FC892CD9A54600C925F4 /* SwipeActions */,
|
||||
D7F360282CEBBE34009D34DA /* CodeScanner */,
|
||||
D7C48C0C2D12E34900A3BACF /* SwiftyCrop */,
|
||||
D7DB1FEB2D5A9F6500CF06DA /* CryptoSwift */,
|
||||
);
|
||||
productName = "share extension";
|
||||
productReference = 82D6FA972CD9820500C925F4 /* ShareExtension.appex */;
|
||||
@@ -4066,6 +4147,7 @@
|
||||
D73E5F9C2C6AA8E3007EB227 /* SwipeActions */,
|
||||
D70D909B2CDED7B200CD0534 /* CodeScanner */,
|
||||
D7C48C0E2D12E35600A3BACF /* SwiftyCrop */,
|
||||
D7DB1FE72D5A9F5300CF06DA /* CryptoSwift */,
|
||||
);
|
||||
productName = "highlighter action extension";
|
||||
productReference = D703D7172C66E47100A400EA /* HighlighterActionExtension.appex */;
|
||||
@@ -4088,6 +4170,7 @@
|
||||
packageProductDependencies = (
|
||||
D789D11F2AFEFBF20083A7AB /* secp256k1 */,
|
||||
D7EDED302B1290B80018B19C /* MarkdownUI */,
|
||||
D7DB1FE92D5A9F5A00CF06DA /* CryptoSwift */,
|
||||
);
|
||||
productName = DamusNotificationService;
|
||||
productReference = D79C4C142AFEB061003A41B4 /* DamusNotificationService.appex */;
|
||||
@@ -4175,6 +4258,7 @@
|
||||
D78DB8572C1CE9CA00F0AB12 /* XCRemoteSwiftPackageReference "SwipeActions" */,
|
||||
D70D90962CDED61800CD0534 /* XCRemoteSwiftPackageReference "CodeScanner" */,
|
||||
D7C48C092D12DE0C00A3BACF /* XCRemoteSwiftPackageReference "SwiftyCrop" */,
|
||||
D7DB1FE22D5A9AC900CF06DA /* XCRemoteSwiftPackageReference "CryptoSwift" */,
|
||||
);
|
||||
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -4214,7 +4298,9 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E06336AB2B75850100A88E6B /* img_with_location.jpeg in Resources */,
|
||||
D7DB1FF12D5AC5D700CF06DA /* nip44.vectors.json in Resources */,
|
||||
4C0C039A2A61E27B0098B3B8 /* bool_setting.wasm in Resources */,
|
||||
D7DB1FF32D5AC5EA00CF06DA /* LICENSES in Resources */,
|
||||
4C0C03992A61E27B0098B3B8 /* primal.wasm in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -4436,7 +4522,6 @@
|
||||
4CA3FA1029F593D000FDB3C3 /* ZapTypePicker.swift in Sources */,
|
||||
4C32B95D2A9AD44700DC3548 /* Documentation.docc in Sources */,
|
||||
4C3EA66828FF5F9900C48A62 /* hex.c in Sources */,
|
||||
E9E4ED0B295867B900DD7078 /* ThreadView.swift in Sources */,
|
||||
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */,
|
||||
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
|
||||
4CF0ABE7298444FD00D66079 /* EventMutingContainerView.swift in Sources */,
|
||||
@@ -4468,6 +4553,7 @@
|
||||
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */,
|
||||
4CEE2B02280B39E800AB5EEF /* EventActionBar.swift in Sources */,
|
||||
4C3BEFE0281DE1ED00B3DE84 /* DamusState.swift in Sources */,
|
||||
D7DB93062D66A44100DA1EE5 /* Undistractor.swift in Sources */,
|
||||
D72E12782BEED22500F4F781 /* Array.swift in Sources */,
|
||||
4C198DF529F88D2E004C165C /* ImageMetadata.swift in Sources */,
|
||||
4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */,
|
||||
@@ -4543,12 +4629,14 @@
|
||||
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */,
|
||||
D734B1452CCC19B1000B5C97 /* DamusFullScreenCover.swift in Sources */,
|
||||
4C4E137D2A76D63600BDD832 /* UnmuteThreadNotify.swift in Sources */,
|
||||
D706C5B72D602A110027C627 /* QueueableNotify.swift in Sources */,
|
||||
4CE4F0F829DB7399005914DB /* ThiccDivider.swift in Sources */,
|
||||
4CFF8F5929C9FD1E008DB934 /* DamusPurpleView.swift in Sources */,
|
||||
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */,
|
||||
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
|
||||
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
|
||||
3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */,
|
||||
D755B28D2D3E7D8800BBEEFA /* NIP37Draft.swift in Sources */,
|
||||
F71694F82A6983AF001F4053 /* GrayGradient.swift in Sources */,
|
||||
4C1D4FB12A7958E60024F453 /* VersionInfo.swift in Sources */,
|
||||
D7FF94002AC7AC5300FD969D /* RelayURL.swift in Sources */,
|
||||
@@ -4626,6 +4714,7 @@
|
||||
4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */,
|
||||
D7EFBA372CC322F300F45588 /* DamusVideoControlsView.swift in Sources */,
|
||||
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
|
||||
D706C5AF2D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */,
|
||||
6439E014296790CF0020672B /* ProfilePicImageView.swift in Sources */,
|
||||
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */,
|
||||
4C1253682A76D2470004F4B8 /* MuteNotify.swift in Sources */,
|
||||
@@ -4635,6 +4724,7 @@
|
||||
4CA927612A290E340098A105 /* EventShell.swift in Sources */,
|
||||
4C363AA428296DEE006E126D /* SearchModel.swift in Sources */,
|
||||
4C8D00CC29DF92DF0036AF10 /* Hashtags.swift in Sources */,
|
||||
D74EA0942D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */,
|
||||
4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */,
|
||||
4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */,
|
||||
D76556D62B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift in Sources */,
|
||||
@@ -4673,6 +4763,7 @@
|
||||
50A60D142A28BEEE00186190 /* RelayLog.swift in Sources */,
|
||||
D7EDED212B117DCA0018B19C /* SequenceUtils.swift in Sources */,
|
||||
BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */,
|
||||
D7DB1FDE2D5A78CE00CF06DA /* NIP44.swift in Sources */,
|
||||
B51C1CEB2B55A60A00E312A9 /* MuteDurationMenu.swift in Sources */,
|
||||
4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */,
|
||||
4C32B9512A9AD44700DC3548 /* FlatbuffersErrors.swift in Sources */,
|
||||
@@ -4742,6 +4833,7 @@
|
||||
D7CB5D4E2B11728000AD4105 /* NewEventsBits.swift in Sources */,
|
||||
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
|
||||
B57B4C622B312BD700A232C0 /* ReconnectRelaysNotify.swift in Sources */,
|
||||
D767066F2C8BB3CF00F09726 /* URLHandler.swift in Sources */,
|
||||
D7ADD3DE2B53854300F104C4 /* DamusPurpleURL.swift in Sources */,
|
||||
E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */,
|
||||
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
|
||||
@@ -4754,6 +4846,7 @@
|
||||
4C9AA14A2A4587A6003F49FD /* NotificationStatusModel.swift in Sources */,
|
||||
D7100C5C2B77016700C59298 /* IAPProductStateView.swift in Sources */,
|
||||
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
|
||||
D74EA0902D2E271E002290DD /* ErrorView.swift in Sources */,
|
||||
4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */,
|
||||
BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */,
|
||||
4C9B0DF32A65C46800CBDA21 /* ProfileEditButton.swift in Sources */,
|
||||
@@ -4784,6 +4877,7 @@
|
||||
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */,
|
||||
D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */,
|
||||
B5A75C2A2B546D94007AFBC0 /* MuteItemTests.swift in Sources */,
|
||||
D7DB1FEE2D5AC51B00CF06DA /* NIP44v2EncryptionTests.swift in Sources */,
|
||||
4C4F14A72A2A61A30045A0B9 /* NostrScriptTests.swift in Sources */,
|
||||
D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */,
|
||||
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
|
||||
@@ -4797,6 +4891,7 @@
|
||||
75AD872B2AA23A460085EF2C /* Block+Tests.swift in Sources */,
|
||||
E0E024112B7C19C20075735D /* TranslationTests.swift in Sources */,
|
||||
F944F56E29EA9CCC0067B3BF /* DamusParseContentTests.swift in Sources */,
|
||||
D7BEE6F92D37B37400CF659F /* DraftTests.swift in Sources */,
|
||||
B501062D2B363036003874F5 /* AuthIntegrationTests.swift in Sources */,
|
||||
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */,
|
||||
D72A2D052AD9C1B5002AFF62 /* MockDamusState.swift in Sources */,
|
||||
@@ -4893,12 +4988,14 @@
|
||||
82D6FAE62CD99F7900C925F4 /* LocalNotificationNotify.swift in Sources */,
|
||||
82D6FAE72CD99F7900C925F4 /* LoginNotify.swift in Sources */,
|
||||
82D6FAE82CD99F7900C925F4 /* LogoutNotify.swift in Sources */,
|
||||
D706C5B12D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */,
|
||||
82D6FAE92CD99F7900C925F4 /* NewMutesNotify.swift in Sources */,
|
||||
82D6FAEA2CD99F7900C925F4 /* NewUnmutesNotify.swift in Sources */,
|
||||
82D6FAEB2CD99F7900C925F4 /* Notify.swift in Sources */,
|
||||
82D6FAEC2CD99F7900C925F4 /* OnlyZapsNotify.swift in Sources */,
|
||||
82D6FAED2CD99F7900C925F4 /* PostNotify.swift in Sources */,
|
||||
82D6FAEE2CD99F7900C925F4 /* PresentSheetNotify.swift in Sources */,
|
||||
D74EA0932D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */,
|
||||
82D6FAEF2CD99F7900C925F4 /* ProfileUpdatedNotify.swift in Sources */,
|
||||
82D6FAF02CD99F7900C925F4 /* ReportNotify.swift in Sources */,
|
||||
82D6FAF12CD99F7900C925F4 /* ScrollToTopNotify.swift in Sources */,
|
||||
@@ -4927,6 +5024,7 @@
|
||||
82D6FB082CD99F7900C925F4 /* UserStatusSheet.swift in Sources */,
|
||||
82D6FB092CD99F7900C925F4 /* SearchHeaderView.swift in Sources */,
|
||||
82D6FB0A2CD99F7900C925F4 /* DamusGradient.swift in Sources */,
|
||||
D7DB93052D66A44100DA1EE5 /* Undistractor.swift in Sources */,
|
||||
82D6FB0B2CD99F7900C925F4 /* AlbyGradient.swift in Sources */,
|
||||
82D6FB0C2CD99F7900C925F4 /* GoldSupportGradient.swift in Sources */,
|
||||
82D6FB0D2CD99F7900C925F4 /* PinkGradient.swift in Sources */,
|
||||
@@ -4987,6 +5085,7 @@
|
||||
82D6FB452CD99F7900C925F4 /* InputDismissKeyboard.swift in Sources */,
|
||||
82D6FB462CD99F7900C925F4 /* Constants.swift in Sources */,
|
||||
82D6FB472CD99F7900C925F4 /* LinkView.swift in Sources */,
|
||||
D7DB1FDF2D5A78CE00CF06DA /* NIP44.swift in Sources */,
|
||||
82D6FB482CD99F7900C925F4 /* PreviewCache.swift in Sources */,
|
||||
82D6FB492CD99F7900C925F4 /* Theme.swift in Sources */,
|
||||
82D6FB4A2CD99F7900C925F4 /* NIP05.swift in Sources */,
|
||||
@@ -5064,6 +5163,7 @@
|
||||
82D6FB922CD99F7900C925F4 /* Wallet.swift in Sources */,
|
||||
82D6FB932CD99F7900C925F4 /* Report.swift in Sources */,
|
||||
82D6FB942CD99F7900C925F4 /* LibreTranslateServer.swift in Sources */,
|
||||
D74EA08E2D2E271E002290DD /* ErrorView.swift in Sources */,
|
||||
82D6FB952CD99F7900C925F4 /* TranslationService.swift in Sources */,
|
||||
82D6FB962CD99F7900C925F4 /* DeepLPlan.swift in Sources */,
|
||||
82D6FB972CD99F7900C925F4 /* ZapsModel.swift in Sources */,
|
||||
@@ -5076,6 +5176,7 @@
|
||||
82D6FB9E2CD99F7900C925F4 /* ContentFilters.swift in Sources */,
|
||||
82D6FB9F2CD99F7900C925F4 /* DamusCacheManager.swift in Sources */,
|
||||
82D6FBA02CD99F7900C925F4 /* NotificationsManager.swift in Sources */,
|
||||
D755B28E2D3E7D8800BBEEFA /* NIP37Draft.swift in Sources */,
|
||||
82D6FBA12CD99F7900C925F4 /* Contacts+.swift in Sources */,
|
||||
82D6FBA22CD99F7900C925F4 /* ZapType.swift in Sources */,
|
||||
82D6FBA32CD99F7900C925F4 /* NewEventsBits.swift in Sources */,
|
||||
@@ -5245,6 +5346,7 @@
|
||||
82D6FC492CD99F7900C925F4 /* BigButton.swift in Sources */,
|
||||
82D6FC4A2CD99F7900C925F4 /* AddRelayView.swift in Sources */,
|
||||
82D6FC4B2CD99F7900C925F4 /* BlocksView.swift in Sources */,
|
||||
D74EA0912D2E3464002290DD /* URLHandler.swift in Sources */,
|
||||
82D6FC4C2CD99F7900C925F4 /* BookmarksView.swift in Sources */,
|
||||
82D6FC4D2CD99F7900C925F4 /* CarouselView.swift in Sources */,
|
||||
82D6FC4E2CD99F7900C925F4 /* ConfigView.swift in Sources */,
|
||||
@@ -5254,6 +5356,7 @@
|
||||
82D6FC522CD99F7900C925F4 /* DMView.swift in Sources */,
|
||||
82D6FC532CD99F7900C925F4 /* EmptyTimelineView.swift in Sources */,
|
||||
82D6FC542CD99F7900C925F4 /* EmptyUserSearchView.swift in Sources */,
|
||||
D706C5B82D602A110027C627 /* QueueableNotify.swift in Sources */,
|
||||
82D6FC552CD99F7900C925F4 /* EventView.swift in Sources */,
|
||||
82D6FC562CD99F7900C925F4 /* EventDetailView.swift in Sources */,
|
||||
82D6FC572CD99F7900C925F4 /* FollowButtonView.swift in Sources */,
|
||||
@@ -5277,7 +5380,6 @@
|
||||
82D6FC682CD99F7900C925F4 /* SearchView.swift in Sources */,
|
||||
82D6FC692CD99F7900C925F4 /* SelectWalletView.swift in Sources */,
|
||||
82D6FC6A2CD99F7900C925F4 /* SetupView.swift in Sources */,
|
||||
82D6FC6B2CD99F7900C925F4 /* ThreadView.swift in Sources */,
|
||||
82D6FC6C2CD99F7900C925F4 /* TimelineView.swift in Sources */,
|
||||
82D6FC6D2CD99F7900C925F4 /* UserRelaysView.swift in Sources */,
|
||||
82D6FC6E2CD99F7900C925F4 /* SideMenuView.swift in Sources */,
|
||||
@@ -5335,12 +5437,14 @@
|
||||
D73E5E3A2C6A97F4007EB227 /* SwipeToDismiss.swift in Sources */,
|
||||
D73E5E3B2C6A97F4007EB227 /* MusicController.swift in Sources */,
|
||||
D73E5E3C2C6A97F4007EB227 /* UserStatusView.swift in Sources */,
|
||||
D74EA08F2D2E271E002290DD /* ErrorView.swift in Sources */,
|
||||
D73E5E3E2C6A97F4007EB227 /* SearchHeaderView.swift in Sources */,
|
||||
D73E5E3F2C6A97F4007EB227 /* DamusGradient.swift in Sources */,
|
||||
D73E5E402C6A97F4007EB227 /* AlbyGradient.swift in Sources */,
|
||||
D73E5E412C6A97F4007EB227 /* GoldSupportGradient.swift in Sources */,
|
||||
D73E5E422C6A97F4007EB227 /* PinkGradient.swift in Sources */,
|
||||
D73E5E432C6A97F4007EB227 /* GrayGradient.swift in Sources */,
|
||||
D7DB93072D66A44100DA1EE5 /* Undistractor.swift in Sources */,
|
||||
D73E5E442C6A97F4007EB227 /* DamusLogoGradient.swift in Sources */,
|
||||
D73E5E452C6A97F4007EB227 /* DamusBackground.swift in Sources */,
|
||||
D73E5E462C6A97F4007EB227 /* DamusLightGradient.swift in Sources */,
|
||||
@@ -5429,6 +5533,7 @@
|
||||
D73E5E9C2C6A97F4007EB227 /* Reply.swift in Sources */,
|
||||
D73E5E9D2C6A97F4007EB227 /* SearchModel.swift in Sources */,
|
||||
D73E5E9E2C6A97F4007EB227 /* NostrFilter+Hashable.swift in Sources */,
|
||||
D74EA0952D2E77B9002290DD /* LoadableNostrEventView.swift in Sources */,
|
||||
D73E5F912C6AA71B007EB227 /* InputDismissKeyboard.swift in Sources */,
|
||||
D73E5E9F2C6A97F4007EB227 /* CreateAccountModel.swift in Sources */,
|
||||
D73E5EA12C6A97F4007EB227 /* SignalModel.swift in Sources */,
|
||||
@@ -5455,6 +5560,7 @@
|
||||
D73E5EB42C6A97F4007EB227 /* NoteContent.swift in Sources */,
|
||||
D73E5EB52C6A97F4007EB227 /* LongformEvent.swift in Sources */,
|
||||
D73E5EB62C6A97F4007EB227 /* PushNotificationClient.swift in Sources */,
|
||||
D706C5B92D602A110027C627 /* QueueableNotify.swift in Sources */,
|
||||
D71AD8FD2CEC176A002E2C3C /* AppAccessibilityIdentifiers.swift in Sources */,
|
||||
D73E5EB72C6A97F4007EB227 /* HighlightEvent.swift in Sources */,
|
||||
D73E5EB82C6A97F4007EB227 /* RelayConnection.swift in Sources */,
|
||||
@@ -5634,7 +5740,6 @@
|
||||
D73E5F602C6A97F5007EB227 /* SearchResultsView.swift in Sources */,
|
||||
D73E5F612C6A97F5007EB227 /* SearchView.swift in Sources */,
|
||||
D73E5F622C6A97F5007EB227 /* SelectWalletView.swift in Sources */,
|
||||
D73E5F642C6A97F5007EB227 /* ThreadView.swift in Sources */,
|
||||
D73E5F652C6A97F5007EB227 /* TimelineView.swift in Sources */,
|
||||
D73E5F662C6A97F5007EB227 /* UserRelaysView.swift in Sources */,
|
||||
D73E5F682C6A97F5007EB227 /* BannerImageView.swift in Sources */,
|
||||
@@ -5715,6 +5820,8 @@
|
||||
D703D7712C670B6D00A400EA /* NdbProfile.swift in Sources */,
|
||||
D703D7A22C670E1A00A400EA /* list.c in Sources */,
|
||||
D703D7A42C670E3C00A400EA /* midl.c in Sources */,
|
||||
D7DB1FE02D5A78CE00CF06DA /* NIP44.swift in Sources */,
|
||||
D706C5B02D5D31C20027C627 /* AutoSaveIndicatorView.swift in Sources */,
|
||||
D703D7982C670DF200A400EA /* utf8.c in Sources */,
|
||||
D703D78B2C670C9500A400EA /* MakeZapRequest.swift in Sources */,
|
||||
D703D7862C670C6500A400EA /* NewUnmutesNotify.swift in Sources */,
|
||||
@@ -5757,6 +5864,7 @@
|
||||
D703D7622C670ACB00A400EA /* ByteBuffer.swift in Sources */,
|
||||
D703D79A2C670DFD00A400EA /* bech32.c in Sources */,
|
||||
D703D7B62C67118200A400EA /* String+extension.swift in Sources */,
|
||||
D74EA08A2D2BF2A7002290DD /* URLHandler.swift in Sources */,
|
||||
D703D76C2C670B3900A400EA /* Post.swift in Sources */,
|
||||
D703D77A2C670BEB00A400EA /* VeriferOptions.swift in Sources */,
|
||||
D73E5F9E2C6AA9F7007EB227 /* nostrscript.c in Sources */,
|
||||
@@ -5764,6 +5872,7 @@
|
||||
D703D7472C67092700A400EA /* UserSettingsStore.swift in Sources */,
|
||||
D703D7852C670C6100A400EA /* Notify.swift in Sources */,
|
||||
D703D7532C670A2600A400EA /* Wallet.swift in Sources */,
|
||||
D755B28F2D3E7D8800BBEEFA /* NIP37Draft.swift in Sources */,
|
||||
D703D75F2C670AA200A400EA /* NostrEvent.swift in Sources */,
|
||||
D703D7442C67086800A400EA /* HeadlessDamusState.swift in Sources */,
|
||||
D703D7922C670D2900A400EA /* RelayURL.swift in Sources */,
|
||||
@@ -6230,7 +6339,7 @@
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "Damus needs access to your camera if you want to scan QR codes and upload photos from it";
|
||||
INFOPLIST_KEY_NSFaceIDUsageDescription = "Local authentication to access private key";
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Damus needs access to your microphone if you want to upload recorded videos from it";
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Damus needs access to your microphone to allow you to create video recordings that you can choose to post publicly on the network";
|
||||
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Granting Damus access to your photos allows you to save images.";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
@@ -6247,7 +6356,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 1.12;
|
||||
MARKETING_VERSION = 1.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
@@ -6282,7 +6391,7 @@
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
||||
INFOPLIST_KEY_NSCameraUsageDescription = "Damus needs access to your camera if you want to scan QR codes and upload photos from it";
|
||||
INFOPLIST_KEY_NSFaceIDUsageDescription = "Local authentication to access private key";
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Damus needs access to your microphone if you want to upload recorded videos from it";
|
||||
INFOPLIST_KEY_NSMicrophoneUsageDescription = "Damus needs access to your microphone to allow you to create video recordings that you can choose to post publicly on the network";
|
||||
INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Granting Damus access to your photos allows you to save images.";
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
@@ -6299,7 +6408,7 @@
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
);
|
||||
MARKETING_VERSION = 1.12;
|
||||
MARKETING_VERSION = 1.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
@@ -6406,7 +6515,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 1.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jb55.damus2.share-extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
@@ -6440,7 +6549,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 1.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jb55.damus2.share-extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
@@ -6474,7 +6583,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 1.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jb55.damus2.highlighter-action-extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
@@ -6509,7 +6618,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 1.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.jb55.damus2.highlighter-action-extension";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
@@ -6528,6 +6637,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_ENTITLEMENTS = DamusNotificationService/DamusNotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
@@ -6542,6 +6652,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2.DamusNotificationService;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
@@ -6561,6 +6672,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_ENTITLEMENTS = DamusNotificationService/DamusNotificationService.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
@@ -6575,6 +6687,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.13;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damus2.DamusNotificationService;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
@@ -6727,6 +6840,14 @@
|
||||
revision = 454d0a0d4faf6f3a19c8d817ab9d7d27524bd79f;
|
||||
};
|
||||
};
|
||||
D7DB1FE22D5A9AC900CF06DA /* XCRemoteSwiftPackageReference "CryptoSwift" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/krzyzanowskim/CryptoSwift.git";
|
||||
requirement = {
|
||||
kind = revision;
|
||||
revision = e74bbbfbef939224b242ae7c342a90e60b88b5ce;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
@@ -6855,6 +6976,26 @@
|
||||
package = D7C48C092D12DE0C00A3BACF /* XCRemoteSwiftPackageReference "SwiftyCrop" */;
|
||||
productName = SwiftyCrop;
|
||||
};
|
||||
D7DB1FE32D5A9AC900CF06DA /* CryptoSwift */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D7DB1FE22D5A9AC900CF06DA /* XCRemoteSwiftPackageReference "CryptoSwift" */;
|
||||
productName = CryptoSwift;
|
||||
};
|
||||
D7DB1FE72D5A9F5300CF06DA /* CryptoSwift */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D7DB1FE22D5A9AC900CF06DA /* XCRemoteSwiftPackageReference "CryptoSwift" */;
|
||||
productName = CryptoSwift;
|
||||
};
|
||||
D7DB1FE92D5A9F5A00CF06DA /* CryptoSwift */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D7DB1FE22D5A9AC900CF06DA /* XCRemoteSwiftPackageReference "CryptoSwift" */;
|
||||
productName = CryptoSwift;
|
||||
};
|
||||
D7DB1FEB2D5A9F6500CF06DA /* CryptoSwift */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = D7DB1FE22D5A9AC900CF06DA /* XCRemoteSwiftPackageReference "CryptoSwift" */;
|
||||
productName = CryptoSwift;
|
||||
};
|
||||
D7EDED242B117F7C0018B19C /* MarkdownUI */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 4C27C9302A64766F007DBC75 /* XCRemoteSwiftPackageReference "swift-markdown-ui" */;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "0d806129a33991730dd1aa3d38c47a745f9e9e6ff44999f4a7f28b695f024832",
|
||||
"originHash" : "085cf0f645323bf77edb52886489bf77b309a0a2d2b78a54beaf8520b540d596",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "codescanner",
|
||||
@@ -9,6 +9,14 @@
|
||||
"revision" : "9fa582f4b36c69c2a55bff5fb3377eb170ae273c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "cryptoswift",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/krzyzanowskim/CryptoSwift.git",
|
||||
"state" : {
|
||||
"revision" : "e74bbbfbef939224b242ae7c342a90e60b88b5ce"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "emojikit",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
@@ -10,6 +10,15 @@ import SwiftUI
|
||||
struct Reposted: View {
|
||||
let damus: DamusState
|
||||
let pubkey: Pubkey
|
||||
let target: NoteId
|
||||
@State var reposts: Int
|
||||
|
||||
init(damus: DamusState, pubkey: Pubkey, target: NoteId) {
|
||||
self.damus = damus
|
||||
self.pubkey = pubkey
|
||||
self.target = target
|
||||
self.reposts = damus.boosts.counts[target] ?? 1
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .center) {
|
||||
@@ -17,15 +26,30 @@ struct Reposted: View {
|
||||
.foregroundColor(Color.gray)
|
||||
ProfileName(pubkey: pubkey, damus: damus, show_nip5_domain: false)
|
||||
.foregroundColor(Color.gray)
|
||||
Text("Reposted", comment: "Text indicating that the note was reposted (i.e. re-shared).")
|
||||
.foregroundColor(Color.gray)
|
||||
NavigationLink(value: Route.Reposts(reposts: .reposts(state: damus, target: target))) {
|
||||
let other_reposts = reposts - 1
|
||||
if other_reposts > 0 {
|
||||
Text(" and \(other_reposts) others reposted", comment: "Text indicating that the note was reposted (i.e. re-shared) by multiple people")
|
||||
.foregroundColor(Color.gray)
|
||||
} else {
|
||||
Text("reposted", comment: "Text indicating that the note was reposted (i.e. re-shared).")
|
||||
.foregroundColor(Color.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.update_stats), perform: { note_id in
|
||||
guard note_id == target else { return }
|
||||
let repost_count = damus.boosts.counts[target]
|
||||
if let repost_count, reposts != repost_count {
|
||||
reposts = repost_count
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct Reposted_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
let test_state = test_damus_state
|
||||
Reposted(damus: test_state, pubkey: test_state.pubkey)
|
||||
Reposted(damus: test_state, pubkey: test_state.pubkey, target: test_note.id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ struct SelectableText: View {
|
||||
})) {
|
||||
if let event, case .show_highlight_post_view(let highlighted_text) = self.selectedTextActionState {
|
||||
PostView(
|
||||
action: .highlighting(.init(selected_text: highlighted_text, source: .event(event))),
|
||||
action: .highlighting(.init(selected_text: highlighted_text, source: .event(event.id))),
|
||||
damus_state: damus_state
|
||||
)
|
||||
.presentationDragIndicator(.visible)
|
||||
|
||||
@@ -12,6 +12,14 @@ struct SupporterBadge: View {
|
||||
let purple_account: DamusPurple.Account?
|
||||
let style: Style
|
||||
let text_color: Color
|
||||
var badge_variant: BadgeVariant {
|
||||
if purple_account?.attributes.contains(.memberForMoreThanOneYear) == true {
|
||||
return .oneYearSpecial
|
||||
}
|
||||
else {
|
||||
return .normal
|
||||
}
|
||||
}
|
||||
|
||||
init(percent: Int?, purple_account: DamusPurple.Account? = nil, style: Style, text_color: Color = .secondary) {
|
||||
self.percent = percent
|
||||
@@ -26,13 +34,18 @@ struct SupporterBadge: View {
|
||||
HStack {
|
||||
if let purple_account, purple_account.active == true {
|
||||
HStack(spacing: 1) {
|
||||
Image("star.fill")
|
||||
.resizable()
|
||||
.frame(width:size, height:size)
|
||||
.foregroundStyle(GoldGradient)
|
||||
if self.style == .full {
|
||||
let date = format_date(date: purple_account.created_at, time_style: .none)
|
||||
Text(date)
|
||||
switch self.badge_variant {
|
||||
case .normal:
|
||||
StarShape()
|
||||
.frame(width:size, height:size)
|
||||
.foregroundStyle(GoldGradient)
|
||||
case .oneYearSpecial:
|
||||
DoubleStar(size: size)
|
||||
}
|
||||
|
||||
if self.style == .full,
|
||||
let ordinal = self.purple_account?.ordinal() {
|
||||
Text(ordinal)
|
||||
.foregroundStyle(text_color)
|
||||
.font(.caption)
|
||||
}
|
||||
@@ -56,8 +69,102 @@ struct SupporterBadge: View {
|
||||
case full // Shows the entire badge with a purple subscriber number if present
|
||||
case compact // Does not show purple subscriber number. Only shows the star (if applicable)
|
||||
}
|
||||
|
||||
enum BadgeVariant {
|
||||
/// A normal badge that people are used to
|
||||
case normal
|
||||
/// A special badge for users who have been members for more than a year
|
||||
case oneYearSpecial
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct StarShape: Shape {
|
||||
func path(in rect: CGRect) -> Path {
|
||||
var path = Path()
|
||||
let center = CGPoint(x: rect.midX, y: rect.midY)
|
||||
let radius: CGFloat = min(rect.width, rect.height) / 2
|
||||
let points = 5
|
||||
let adjustment: CGFloat = .pi / 2
|
||||
|
||||
for i in 0..<points * 2 {
|
||||
let angle = (CGFloat(i) * .pi / CGFloat(points)) - adjustment
|
||||
let pointRadius = i % 2 == 0 ? radius : radius * 0.4
|
||||
let point = CGPoint(x: center.x + pointRadius * cos(angle), y: center.y + pointRadius * sin(angle))
|
||||
if i == 0 {
|
||||
path.move(to: point)
|
||||
} else {
|
||||
path.addLine(to: point)
|
||||
}
|
||||
}
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
struct DoubleStar: View {
|
||||
let size: CGFloat
|
||||
var starOffset: CGFloat = 5
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 17.0, *) {
|
||||
DoubleStarShape(starOffset: starOffset)
|
||||
.frame(width: size, height: size)
|
||||
.foregroundStyle(GoldGradient)
|
||||
.padding(.trailing, starOffset)
|
||||
} else {
|
||||
Fallback(size: size, starOffset: starOffset)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 17.0, *)
|
||||
struct DoubleStarShape: Shape {
|
||||
var strokeSize: CGFloat = 3
|
||||
var starOffset: CGFloat
|
||||
|
||||
func path(in rect: CGRect) -> Path {
|
||||
let normalSizedStarPath = StarShape().path(in: rect)
|
||||
let largerStarPath = StarShape().path(in: rect.insetBy(dx: -strokeSize, dy: -strokeSize))
|
||||
|
||||
let finalPath = normalSizedStarPath
|
||||
.subtracting(
|
||||
largerStarPath.offsetBy(dx: starOffset, dy: 0)
|
||||
)
|
||||
.union(
|
||||
normalSizedStarPath.offsetBy(dx: starOffset, dy: 0)
|
||||
)
|
||||
|
||||
return finalPath
|
||||
}
|
||||
}
|
||||
|
||||
/// A fallback view for those who cannot run iOS 17
|
||||
struct Fallback: View {
|
||||
var size: CGFloat
|
||||
var starOffset: CGFloat
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
StarShape()
|
||||
.frame(width: size, height: size)
|
||||
.foregroundStyle(GoldGradient)
|
||||
|
||||
StarShape()
|
||||
.fill(GoldGradient)
|
||||
.overlay(
|
||||
StarShape()
|
||||
.stroke(Color.damusAdaptableWhite, lineWidth: 1)
|
||||
)
|
||||
.frame(width: size + 1, height: size + 1)
|
||||
.padding(.leading, -size - starOffset)
|
||||
}
|
||||
.padding(.trailing, -3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func support_level_color(_ percent: Int) -> Color {
|
||||
if percent == 0 {
|
||||
return .gray
|
||||
@@ -86,7 +193,7 @@ struct SupporterBadge_Previews: PreviewProvider {
|
||||
HStack(alignment: .center) {
|
||||
SupporterBadge(
|
||||
percent: nil,
|
||||
purple_account: DamusPurple.Account(pubkey: test_pubkey, created_at: .now, expiry: .now.addingTimeInterval(10000), subscriber_number: subscriber_number, active: true),
|
||||
purple_account: DamusPurple.Account(pubkey: test_pubkey, created_at: .now, expiry: .now.addingTimeInterval(10000), subscriber_number: subscriber_number, active: true, attributes: []),
|
||||
style: .full
|
||||
)
|
||||
.frame(width: 100)
|
||||
@@ -118,4 +225,52 @@ struct SupporterBadge_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("1 yr badge") {
|
||||
VStack {
|
||||
HStack(alignment: .center) {
|
||||
SupporterBadge(
|
||||
percent: nil,
|
||||
purple_account: DamusPurple.Account(pubkey: test_pubkey, created_at: .now, expiry: .now.addingTimeInterval(10000), subscriber_number: 3, active: true, attributes: []),
|
||||
style: .full
|
||||
)
|
||||
.frame(width: 100)
|
||||
}
|
||||
|
||||
HStack(alignment: .center) {
|
||||
SupporterBadge(
|
||||
percent: nil,
|
||||
purple_account: DamusPurple.Account(pubkey: test_pubkey, created_at: .now, expiry: .now.addingTimeInterval(10000), subscriber_number: 3, active: true, attributes: [.memberForMoreThanOneYear]),
|
||||
style: .full
|
||||
)
|
||||
.frame(width: 100)
|
||||
}
|
||||
|
||||
Text(verbatim: "Double star (just shape itself, with alt background color, to show it adapts to background well)")
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
if #available(iOS 17.0, *) {
|
||||
HStack(alignment: .center) {
|
||||
DoubleStar.DoubleStarShape(starOffset: 5)
|
||||
.frame(width: 17, height: 17)
|
||||
.padding(.trailing, -8)
|
||||
}
|
||||
.background(Color.blue)
|
||||
}
|
||||
|
||||
Text(verbatim: "Double star (fallback for iOS 16 and below)")
|
||||
|
||||
HStack(alignment: .center) {
|
||||
DoubleStar.Fallback(size: 17, starOffset: 5)
|
||||
}
|
||||
|
||||
Text(verbatim: "Double star (fallback for iOS 16 and below, with alt color limitation shown)")
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
HStack(alignment: .center) {
|
||||
DoubleStar.Fallback(size: 17, starOffset: 5)
|
||||
}
|
||||
.background(Color.blue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,8 @@ enum Sheets: Identifiable {
|
||||
case onboardingSuggestions
|
||||
case purple(DamusPurpleURL)
|
||||
case purple_onboarding
|
||||
|
||||
case error(ErrorView.UserPresentableError)
|
||||
|
||||
static func zap(target: ZapTarget, lnurl: String) -> Sheets {
|
||||
return .zap(ZapSheet(target: target, lnurl: lnurl))
|
||||
}
|
||||
@@ -53,6 +54,7 @@ enum Sheets: Identifiable {
|
||||
case .onboardingSuggestions: return "onboarding-suggestions"
|
||||
case .purple(let purple_url): return "purple" + purple_url.url_string()
|
||||
case .purple_onboarding: return "purple_onboarding"
|
||||
case .error(_): return "error"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,12 +222,6 @@ struct ContentView: View {
|
||||
navigationCoordinator.push(route: Route.Script(script: model))
|
||||
}
|
||||
|
||||
func open_profile(pubkey: Pubkey) {
|
||||
let profile_model = ProfileModel(pubkey: pubkey, damus: damus_state!)
|
||||
let followers = FollowersModel(damus_state: damus_state!, target: pubkey)
|
||||
navigationCoordinator.push(route: Route.Profile(profile: profile_model, followers: followers))
|
||||
}
|
||||
|
||||
func open_search(filt: NostrFilter) {
|
||||
let search = SearchModel(state: damus_state!, search: filt)
|
||||
navigationCoordinator.push(route: Route.Search(search: search))
|
||||
@@ -310,6 +306,9 @@ struct ContentView: View {
|
||||
hasSeenOnboardingSuggestions = true
|
||||
}
|
||||
self.appDelegate?.state = damus_state
|
||||
Task { // We probably don't need this to be a detached task. According to https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/#Defining-and-Calling-Asynchronous-Functions, awaits are only suspension points that do not block the thread.
|
||||
await self.listenAndHandleLocalNotifications()
|
||||
}
|
||||
}
|
||||
.sheet(item: $active_sheet) { item in
|
||||
switch item {
|
||||
@@ -339,36 +338,14 @@ struct ContentView: View {
|
||||
DamusPurpleURLSheetView(damus_state: damus_state!, purple_url: purple_url)
|
||||
case .purple_onboarding:
|
||||
DamusPurpleNewUserOnboardingView(damus_state: damus_state)
|
||||
case .error(let error):
|
||||
ErrorView(damus_state: damus_state!, error: error)
|
||||
}
|
||||
}
|
||||
.onOpenURL { url in
|
||||
on_open_url(state: damus_state!, url: url) { res in
|
||||
guard let res else {
|
||||
return
|
||||
}
|
||||
|
||||
switch res {
|
||||
case .filter(let filt): self.open_search(filt: filt)
|
||||
case .profile(let pk): self.open_profile(pubkey: pk)
|
||||
case .event(let ev): self.open_event(ev: ev)
|
||||
case .wallet_connect(let nwc): self.open_wallet(nwc: nwc)
|
||||
case .script(let data): self.open_script(data)
|
||||
case .purple(let purple_url):
|
||||
if case let .welcome(checkout_id) = purple_url.variant {
|
||||
// If this is a welcome link, do the following before showing the onboarding screen:
|
||||
// 1. Check if this is legitimate and good to go.
|
||||
// 2. Mark as complete if this is good to go.
|
||||
Task {
|
||||
let is_good_to_go = try? await damus_state.purple.check_and_mark_ln_checkout_is_good_to_go(checkout_id: checkout_id)
|
||||
if is_good_to_go == true {
|
||||
self.active_sheet = .purple(purple_url)
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
self.active_sheet = .purple(purple_url)
|
||||
}
|
||||
}
|
||||
Task {
|
||||
let open_action = await DamusURLHandler.handle_opening_url_and_compute_view_action(damus_state: self.damus_state, url: url)
|
||||
self.execute_open_action(open_action)
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.compose)) { action in
|
||||
@@ -390,6 +367,8 @@ struct ContentView: View {
|
||||
self.confirm_mute = true
|
||||
}
|
||||
.onReceive(handle_notify(.attached_wallet)) { nwc in
|
||||
try? damus_state.pool.add_relay(.nwc(url: nwc.relay))
|
||||
|
||||
// update the lightning address on our profile when we attach a
|
||||
// wallet with an associated
|
||||
guard let ds = self.damus_state,
|
||||
@@ -531,27 +510,6 @@ struct ContentView: View {
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.local_notification)) { local in
|
||||
guard let damus_state else { return }
|
||||
|
||||
switch local.mention {
|
||||
case .pubkey(let pubkey):
|
||||
open_profile(pubkey: pubkey)
|
||||
|
||||
case .note(let noteId):
|
||||
openEvent(noteId: noteId, notificationType: local.type)
|
||||
case .nevent(let nevent):
|
||||
openEvent(noteId: nevent.noteid, notificationType: local.type)
|
||||
case .nprofile(let nprofile):
|
||||
open_profile(pubkey: nprofile.author)
|
||||
case .nrelay(_):
|
||||
break
|
||||
case .naddr(let naddr):
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
.onReceive(handle_notify(.onlyzaps_mode)) { hide in
|
||||
home.filter_events()
|
||||
@@ -608,7 +566,7 @@ struct ContentView: View {
|
||||
}, message: {
|
||||
Text("No mute list found, create a new one? This will overwrite any previous mute lists.", comment: "Alert message prompt that asks if the user wants to create a new mute list, overwriting previous mute lists.")
|
||||
})
|
||||
.alert(NSLocalizedString("Mute User", comment: "Title of alert for muting a user."), isPresented: $confirm_mute, actions: {
|
||||
.alert(NSLocalizedString("Mute/Block User", comment: "Title of alert for muting/blocking a user."), isPresented: $confirm_mute, actions: {
|
||||
Button(NSLocalizedString("Cancel", comment: "Alert button to cancel out of alert for muting a user."), role: .cancel) {
|
||||
confirm_mute = false
|
||||
}
|
||||
@@ -661,6 +619,28 @@ struct ContentView: View {
|
||||
self.selected_timeline = timeline
|
||||
}
|
||||
|
||||
/// Listens to requests to open a push/local user notification
|
||||
///
|
||||
/// This function never returns, it just keeps streaming
|
||||
func listenAndHandleLocalNotifications() async {
|
||||
for await notification in await QueueableNotify<LossyLocalNotification>.shared.stream {
|
||||
self.handleNotification(notification: notification)
|
||||
}
|
||||
}
|
||||
|
||||
func handleNotification(notification: LossyLocalNotification) {
|
||||
Log.info("ContentView is handling a notification", for: .push_notifications)
|
||||
guard let damus_state else {
|
||||
// This should never happen because `listenAndHandleLocalNotifications` is called after damus state is initialized in `onAppear`
|
||||
assertionFailure("DamusState not loaded when ContentView (new handler) was handling a notification")
|
||||
Log.error("DamusState not loaded when ContentView (new handler) was handling a notification", for: .push_notifications)
|
||||
return
|
||||
}
|
||||
let local = notification
|
||||
let openAction = local.toViewOpenAction()
|
||||
self.execute_open_action(openAction)
|
||||
}
|
||||
|
||||
func connect() {
|
||||
// nostrdb
|
||||
var mndb = Ndb()
|
||||
@@ -766,22 +746,38 @@ struct ContentView: View {
|
||||
damus_state.postbox.send(ev)
|
||||
}
|
||||
}
|
||||
|
||||
private func openEvent(noteId: NoteId, notificationType: LocalNotificationType) {
|
||||
guard let target = damus_state.events.lookup(noteId) else {
|
||||
|
||||
/// An open action within the app
|
||||
/// This is used to model, store, and communicate a desired view action to be taken as a result of opening an object,
|
||||
/// for example a URL
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - The reason this was created was to separate URL parsing logic, the underlying actions that mutate the state of the app, and the action to be taken on the view layer as a result. This makes it easier to test, to read the URL handling code, and to add new functionality in between the two (e.g. a confirmation screen before proceeding with a given open action)
|
||||
enum ViewOpenAction {
|
||||
/// Open a page route
|
||||
case route(Route)
|
||||
/// Open a sheet
|
||||
case sheet(Sheets)
|
||||
/// Do nothing.
|
||||
///
|
||||
/// ## Implementation notes
|
||||
/// - This is used here instead of Optional values to make semantics explicit and force better programming intent, instead of accidentally doing nothing because of Swift's syntax sugar.
|
||||
case no_action
|
||||
}
|
||||
|
||||
/// Executes an action to open something in the app view
|
||||
///
|
||||
/// - Parameter open_action: The action to perform
|
||||
func execute_open_action(_ open_action: ViewOpenAction) {
|
||||
switch open_action {
|
||||
case .route(let route):
|
||||
navigationCoordinator.push(route: route)
|
||||
case .sheet(let sheet):
|
||||
self.active_sheet = sheet
|
||||
case .no_action:
|
||||
return
|
||||
}
|
||||
|
||||
switch notificationType {
|
||||
case .dm:
|
||||
selected_timeline = .dms
|
||||
damus_state.dms.set_active_dm(target.pubkey)
|
||||
navigationCoordinator.push(route: Route.DMChat(dms: damus_state.dms.active_model))
|
||||
case .like, .zap, .mention, .repost, .reply, .tagged:
|
||||
open_event(ev: target)
|
||||
case .profile_zap:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -934,10 +930,38 @@ enum FoundEvent {
|
||||
case event(NostrEvent)
|
||||
}
|
||||
|
||||
/// Finds an event from NostrDB if it exists, or from the network
|
||||
///
|
||||
/// This is the callback version. There is also an asyc/await version of this function.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - state: Damus state
|
||||
/// - query_: The query, including the event being looked for, and the relays to use when looking
|
||||
/// - callback: The function to call with results
|
||||
func find_event(state: DamusState, query query_: FindEvent, callback: @escaping (FoundEvent?) -> ()) {
|
||||
return find_event_with_subid(state: state, query: query_, subid: UUID().description, callback: callback)
|
||||
}
|
||||
|
||||
/// Finds an event from NostrDB if it exists, or from the network
|
||||
///
|
||||
/// This is a the async/await version of `find_event`. Use this when using callbacks is impossible or cumbersome.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - state: Damus state
|
||||
/// - query_: The query, including the event being looked for, and the relays to use when looking
|
||||
/// - callback: The function to call with results
|
||||
func find_event(state: DamusState, query query_: FindEvent) async -> FoundEvent? {
|
||||
await withCheckedContinuation { continuation in
|
||||
find_event(state: state, query: query_) { event in
|
||||
var already_resumed = false
|
||||
if !already_resumed { // Ensure we do not resume twice, as it causes a crash
|
||||
continuation.resume(returning: event)
|
||||
already_resumed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: String, callback: @escaping (FoundEvent?) -> ()) {
|
||||
|
||||
var filter: NostrFilter? = nil
|
||||
@@ -1008,6 +1032,15 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Finds a replaceable event based on an `naddr` address.
|
||||
///
|
||||
/// This is the callback version of the function. There is another function that makes use of async/await
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - damus_state: The Damus state
|
||||
/// - naddr: the `naddr` address
|
||||
/// - callback: A function to handle the found event
|
||||
func naddrLookup(damus_state: DamusState, naddr: NAddr, callback: @escaping (NostrEvent?) -> ()) {
|
||||
var nostrKinds: [NostrKind]? = NostrKind(rawValue: naddr.kind).map { [$0] }
|
||||
|
||||
@@ -1036,6 +1069,26 @@ func naddrLookup(damus_state: DamusState, naddr: NAddr, callback: @escaping (Nos
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds a replaceable event based on an `naddr` address.
|
||||
///
|
||||
/// This is the async/await version of the function. Another version of this function which makes use of callback functions also exists .
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - damus_state: The Damus state
|
||||
/// - naddr: the `naddr` address
|
||||
/// - callback: A function to handle the found event
|
||||
func naddrLookup(damus_state: DamusState, naddr: NAddr) async -> NostrEvent? {
|
||||
await withCheckedContinuation { continuation in
|
||||
var already_resumed = false
|
||||
naddrLookup(damus_state: damus_state, naddr: naddr) { event in
|
||||
if !already_resumed { // Ensure we do not resume twice, as it causes a crash
|
||||
continuation.resume(returning: event)
|
||||
already_resumed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func timeline_name(_ timeline: Timeline?) -> String {
|
||||
guard let timeline else {
|
||||
return ""
|
||||
@@ -1146,60 +1199,32 @@ func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: Ev
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum OpenResult {
|
||||
case profile(Pubkey)
|
||||
case filter(NostrFilter)
|
||||
case event(NostrEvent)
|
||||
case wallet_connect(WalletConnectURL)
|
||||
case script([UInt8])
|
||||
case purple(DamusPurpleURL)
|
||||
}
|
||||
|
||||
func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) -> Void) {
|
||||
if let purple_url = DamusPurpleURL(url: url) {
|
||||
result(.purple(purple_url))
|
||||
return
|
||||
}
|
||||
|
||||
if let nwc = WalletConnectURL(str: url.absoluteString) {
|
||||
result(.wallet_connect(nwc))
|
||||
return
|
||||
}
|
||||
|
||||
guard let link = decode_nostr_uri(url.absoluteString) else {
|
||||
result(nil)
|
||||
return
|
||||
}
|
||||
|
||||
switch link {
|
||||
case .ref(let ref):
|
||||
switch ref {
|
||||
case .pubkey(let pk):
|
||||
result(.profile(pk))
|
||||
case .event(let noteid):
|
||||
find_event(state: state, query: .event(evid: noteid)) { res in
|
||||
guard let res, case .event(let ev) = res else { return }
|
||||
result(.event(ev))
|
||||
}
|
||||
case .hashtag(let ht):
|
||||
result(.filter(.filter_hashtag([ht.hashtag])))
|
||||
case .param, .quote, .reference:
|
||||
// doesn't really make sense here
|
||||
break
|
||||
case .naddr(let naddr):
|
||||
naddrLookup(damus_state: state, naddr: naddr) { res in
|
||||
guard let res = res else { return }
|
||||
result(.event(res))
|
||||
}
|
||||
extension LossyLocalNotification {
|
||||
/// Computes a view open action from a mention reference.
|
||||
/// Use this when opening a user-presentable interface to a specific mention reference.
|
||||
func toViewOpenAction() -> ContentView.ViewOpenAction {
|
||||
switch self.mention {
|
||||
case .pubkey(let pubkey):
|
||||
return .route(.ProfileByKey(pubkey: pubkey))
|
||||
case .note(let noteId):
|
||||
return .route(.LoadableNostrEvent(note_reference: .note_id(noteId)))
|
||||
case .nevent(let nEvent):
|
||||
// TODO: Improve this by implementing a route that handles nevents with their relay hints.
|
||||
return .route(.LoadableNostrEvent(note_reference: .note_id(nEvent.noteid)))
|
||||
case .nprofile(let nProfile):
|
||||
// TODO: Improve this by implementing a profile route that handles nprofiles with their relay hints.
|
||||
return .route(.ProfileByKey(pubkey: nProfile.author))
|
||||
case .nrelay(let string):
|
||||
// We do not need to implement `nrelay` support, it has been deprecated.
|
||||
// See https://github.com/nostr-protocol/nips/blob/6e7a618e7f873bb91e743caacc3b09edab7796a0/BREAKING.md?plain=1#L21
|
||||
return .sheet(.error(ErrorView.UserPresentableError(
|
||||
user_visible_description: NSLocalizedString("You opened an invalid link. The link you tried to open refers to \"nrelay\", which has been deprecated and is not supported.", comment: "User-visible error description for a user who tries to open a deprecated \"nrelay\" link."),
|
||||
tip: NSLocalizedString("Please contact the person who provided the link, and ask for another link.", comment: "User-visible tip on what to do if a link contains a deprecated \"nrelay\" reference."),
|
||||
technical_info: "`MentionRef.toViewOpenAction` detected deprecated `nrelay` contents"
|
||||
)))
|
||||
case .naddr(let nAddr):
|
||||
return .route(.LoadableNostrEvent(note_reference: .naddr(nAddr)))
|
||||
}
|
||||
case .filter(let filt):
|
||||
result(.filter(filt))
|
||||
break
|
||||
// TODO: handle filter searches?
|
||||
case .script(let script):
|
||||
result(.script(script))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,6 @@
|
||||
<key>NSAppleMusicUsageDescription</key>
|
||||
<string>Damus needs access to your media library for playback statuses</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Damus needs access to your microphone for creating video recording posts</string>
|
||||
<string>Damus needs access to your microphone to allow you to create video recordings that you can choose to post publicly on the network</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -10,8 +10,9 @@ import Foundation
|
||||
|
||||
/// Simple filter to determine whether to show posts or all posts and replies.
|
||||
enum FilterState : Int {
|
||||
case posts_and_replies = 1
|
||||
case posts = 0
|
||||
case posts_and_replies = 1
|
||||
case conversations = 2
|
||||
|
||||
func filter(ev: NostrEvent) -> Bool {
|
||||
switch self {
|
||||
@@ -19,6 +20,8 @@ enum FilterState : Int {
|
||||
return ev.known_kind == .boost || ev.known_kind == .highlight || !ev.is_reply()
|
||||
case .posts_and_replies:
|
||||
return true
|
||||
case .conversations:
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,6 +175,9 @@ class DamusState: HeadlessDamusState {
|
||||
|
||||
func close() {
|
||||
print("txn: damus close")
|
||||
Task {
|
||||
try await self.push_notification_client.revoke_token()
|
||||
}
|
||||
wallet.disconnect()
|
||||
pool.close()
|
||||
ndb.close()
|
||||
|
||||
@@ -6,14 +6,45 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUICore
|
||||
import UIKit
|
||||
|
||||
/// Represents artifacts in a post draft, which is rendered by `PostView`
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - This is NOT `Codable` because we store these persistently as NIP-37 drafts in NostrDB, instead of directly encoding the object.
|
||||
/// - `NSMutableAttributedString` is the bottleneck for making this `Codable`, and replacing that with another type requires a very large refactor.
|
||||
/// - Encoding/decoding logic is lossy, and is not fully round-trippable. This class does a best effort attempt at encoding and recovering as much information as possible, but the information is dispersed into many different places, types, and functions around the code, making round-trip guarantees very difficult without severely refactoring `PostView`, `TextViewWrapper`, and other associated classes, unfortunately. These are the known limitations at the moment:
|
||||
/// - Image metadata is lost on decoding
|
||||
/// - The `filtered_pubkeys` filter effectively gets applied upon encoding, causing them to change upon decoding
|
||||
///
|
||||
class DraftArtifacts: Equatable {
|
||||
/// The text content of the note draft
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - This serves as the backing model for `PostView` and `TextViewWrapper`. It might be cleaner to use a specialized data model for this in the future and render to attributed string in real time, but that will require a big refactor. See https://github.com/damus-io/damus/issues/1862#issuecomment-2585756932
|
||||
var content: NSMutableAttributedString
|
||||
/// A list of media items that have been attached to the note draft.
|
||||
var media: [UploadedMedia]
|
||||
/// The references for this note, which will be translated into tags once the event is published.
|
||||
var references: [RefId]
|
||||
/// Pubkeys that should be filtered out from the references
|
||||
///
|
||||
/// For example, when replying to an event, the user can select which pubkey mentions they want to keep, and which ones to remove.
|
||||
var filtered_pubkeys: Set<Pubkey> = []
|
||||
|
||||
init(content: NSMutableAttributedString = NSMutableAttributedString(string: ""), media: [UploadedMedia] = []) {
|
||||
/// A unique ID for this draft that allows us to address these if we need to.
|
||||
///
|
||||
/// This will be the unique identifier in the NIP-37 note
|
||||
let id: String
|
||||
|
||||
init(content: NSMutableAttributedString = NSMutableAttributedString(string: ""), media: [UploadedMedia] = [], references: [RefId], id: String) {
|
||||
self.content = content
|
||||
self.media = media
|
||||
self.references = references
|
||||
self.id = id
|
||||
}
|
||||
|
||||
static func == (lhs: DraftArtifacts, rhs: DraftArtifacts) -> Bool {
|
||||
@@ -22,11 +53,217 @@ class DraftArtifacts: Equatable {
|
||||
lhs.content.string == rhs.content.string // Comparing the text content is not perfect but acceptable in this case because attributes for our post editor are determined purely from text content
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Encoding and decoding functions to and from NIP-37 nostr events
|
||||
|
||||
/// Converts the draft artifacts into a NIP-37 draft event that can be saved into NostrDB or any Nostr relay
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - action: The post action for this draft, which provides necessary context for the draft (e.g. Is it meant to highlight something? Reply to something?)
|
||||
/// - damus_state: The damus state, needed for encrypting, fetching Nostr data depedencies, and forming the NIP-37 draft
|
||||
/// - references: references in the post?
|
||||
/// - Returns: The NIP-37 draft packaged in a way that can be easily wrapped/unwrapped.
|
||||
func to_nip37_draft(action: PostAction, damus_state: DamusState) throws -> NIP37Draft? {
|
||||
guard let keypair = damus_state.keypair.to_full() else { return nil }
|
||||
let post = build_post(state: damus_state, action: action, draft: self)
|
||||
guard let note = post.to_event(keypair: keypair) else { return nil }
|
||||
return try NIP37Draft(unwrapped_note: note, draft_id: self.id, keypair: keypair)
|
||||
}
|
||||
|
||||
/// Instantiates a draft object from a NIP-37 draft
|
||||
/// - Parameters:
|
||||
/// - nip37_draft: The NIP-37 draft object
|
||||
/// - damus_state: Damus state of the user who wants to load this draft object. Needed for pulling profiles from Ndb, and decrypting contents.
|
||||
/// - Returns: A draft artifacts object, or `nil` if such cannot be loaded.
|
||||
static func from(nip37_draft: NIP37Draft, damus_state: DamusState) -> DraftArtifacts? {
|
||||
return Self.from(
|
||||
event: nip37_draft.unwrapped_note,
|
||||
draft_id: nip37_draft.id ?? UUID().uuidString, // Generate random UUID as the draft ID if none is specified. It is always better to have an ID that we can use for addressing later.
|
||||
damus_state: damus_state
|
||||
)
|
||||
}
|
||||
|
||||
/// Load a draft artifacts object from a plain, unwrapped NostrEvent
|
||||
///
|
||||
/// This function will parse the contents of a Nostr Event and turn it into an editable draft that we can use.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - event: The Nostr event to use as a template
|
||||
/// - draft_id: The unique ID of this draft, used for keeping draft identities stable. UUIDs are recommended but not required.
|
||||
/// - damus_state: The user's Damus state, used for fetching profiles in NostrDB
|
||||
/// - Returns: The draft that can be loaded into `PostView`.
|
||||
static func from(event: NostrEvent, draft_id: String, damus_state: DamusState) -> DraftArtifacts {
|
||||
let parsed_blocks = parse_note_content(content: .init(note: event, keypair: damus_state.keypair))
|
||||
return Self.from(parsed_blocks: parsed_blocks, references: Array(event.references), draft_id: draft_id, damus_state: damus_state)
|
||||
}
|
||||
|
||||
/// Load a draft artifacts object from parsed Nostr event blocks
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - parsed_blocks: The blocks parsed from a Nostr event
|
||||
/// - references: The references in the Nostr event
|
||||
/// - draft_id: The unique ID of the draft as per NIP-37
|
||||
/// - damus_state: Damus state, used for fetching profile info in NostrDB
|
||||
/// - Returns: The draft that can be loaded into `PostView`.
|
||||
static func from(parsed_blocks: Blocks, references: [RefId], draft_id: String, damus_state: DamusState) -> DraftArtifacts {
|
||||
let rich_text_content: NSMutableAttributedString = .init(string: "")
|
||||
var media: [UploadedMedia] = []
|
||||
for block in parsed_blocks.blocks {
|
||||
switch block {
|
||||
case .mention(let mention):
|
||||
if case .pubkey(let pubkey) = mention.ref {
|
||||
// A profile reference, format things properly.
|
||||
let profile = damus_state.ndb.lookup_profile(pubkey)?.unsafeUnownedValue?.profile
|
||||
let profile_name = parse_display_name(profile: profile, pubkey: pubkey).username
|
||||
guard let url_address = URL(string: block.asString) else {
|
||||
rich_text_content.append(.init(string: block.asString))
|
||||
continue
|
||||
}
|
||||
let attributed_string = NSMutableAttributedString(
|
||||
string: "@\(profile_name)",
|
||||
attributes: [
|
||||
.link: url_address,
|
||||
.foregroundColor: UIColor(Color.accentColor)
|
||||
]
|
||||
)
|
||||
rich_text_content.append(attributed_string)
|
||||
}
|
||||
else if case .note(_) = mention.ref {
|
||||
// These note references occur when we quote a note, and since that is tracked via `PostAction` in `PostView`, ignore it here to avoid attaching the same event twice in a note
|
||||
continue
|
||||
}
|
||||
else {
|
||||
// Other references
|
||||
rich_text_content.append(.init(string: block.asString))
|
||||
}
|
||||
case .url(let url):
|
||||
if isSupportedImage(url: url) {
|
||||
// Image, add that to our media attachments
|
||||
// TODO: Add metadata decoding support
|
||||
media.append(UploadedMedia(localURL: url, uploadedURL: url, metadata: .none))
|
||||
continue
|
||||
}
|
||||
else {
|
||||
// Normal URL, plain text
|
||||
rich_text_content.append(.init(string: block.asString))
|
||||
}
|
||||
case .invoice(_), .relay(_), .hashtag(_), .text(_):
|
||||
// Everything else is currently plain text.
|
||||
rich_text_content.append(.init(string: block.asString))
|
||||
}
|
||||
}
|
||||
return DraftArtifacts(content: rich_text_content, media: media, references: references, id: draft_id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Holds and keeps track of the note post drafts throughout the app.
|
||||
class Drafts: ObservableObject {
|
||||
@Published var post: DraftArtifacts? = nil
|
||||
@Published var replies: [NostrEvent: DraftArtifacts] = [:]
|
||||
@Published var quotes: [NostrEvent: DraftArtifacts] = [:]
|
||||
@Published var highlights: [HighlightSource: DraftArtifacts] = [:]
|
||||
@Published var replies: [NoteId: DraftArtifacts] = [:]
|
||||
@Published var quotes: [NoteId: DraftArtifacts] = [:]
|
||||
/// The drafts we have for highlights
|
||||
///
|
||||
/// ## Implementation notes
|
||||
/// - Although in practice we also load drafts based on the highlight source for better UX (making it easier to find a draft), we need the keys to be of type `HighlightContentDraft` because we need the selected text information to be able to construct the NIP-37 draft, as well as to load that into post view.
|
||||
@Published var highlights: [HighlightContentDraft: DraftArtifacts] = [:]
|
||||
|
||||
/// Loads drafts from storage (NostrDB + UserDefaults)
|
||||
func load(from damus_state: DamusState) {
|
||||
guard let note_ids = damus_state.settings.draft_event_ids?.compactMap({ NoteId(hex: $0) }) else { return }
|
||||
for note_id in note_ids {
|
||||
let txn = damus_state.ndb.lookup_note(note_id)
|
||||
guard let note = txn?.unsafeUnownedValue else { continue }
|
||||
// Implementation note: This currently fails silently, because:
|
||||
// 1. Errors are unlikely and not expected
|
||||
// 2. It is not mission critical to recover from this error
|
||||
// 3. The changes that add a error view sheet with useful info is not yet merged in as of writing.
|
||||
try? self.load(wrapped_draft_note: note, with: damus_state)
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads a specific NIP-37 note into this class
|
||||
func load(wrapped_draft_note: NdbNote, with damus_state: DamusState) throws {
|
||||
// Extract draft info from the NIP-37 note
|
||||
guard let full_keypair = damus_state.keypair.to_full() else { return }
|
||||
guard let nip37_draft = try NIP37Draft(wrapped_note: wrapped_draft_note, keypair: full_keypair) else { return }
|
||||
guard let known_kind = nip37_draft.unwrapped_note.known_kind else { return }
|
||||
guard let draft_artifacts = DraftArtifacts.from(
|
||||
nip37_draft: nip37_draft,
|
||||
damus_state: damus_state
|
||||
) else { return }
|
||||
|
||||
// Find out where to place these drafts
|
||||
let blocks = parse_note_content(content: .note(nip37_draft.unwrapped_note))
|
||||
switch known_kind {
|
||||
case .text:
|
||||
if let replied_to_note_id = nip37_draft.unwrapped_note.direct_replies() {
|
||||
self.replies[replied_to_note_id] = draft_artifacts
|
||||
}
|
||||
else {
|
||||
for block in blocks.blocks {
|
||||
if case .mention(let mention) = block {
|
||||
if case .note(let note_id) = mention.ref {
|
||||
self.quotes[note_id] = draft_artifacts
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
self.post = draft_artifacts
|
||||
}
|
||||
case .highlight:
|
||||
guard let highlight = HighlightContentDraft(from: nip37_draft.unwrapped_note) else { return }
|
||||
self.highlights[highlight] = draft_artifacts
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/// Saves the drafts tracked by this class persistently using NostrDB + UserDefaults
|
||||
func save(damus_state: DamusState) {
|
||||
var draft_events: [NdbNote] = []
|
||||
post_artifact_block: if let post_artifacts = self.post {
|
||||
let nip37_draft = try? post_artifacts.to_nip37_draft(action: .posting(.user(damus_state.pubkey)), damus_state: damus_state)
|
||||
guard let wrapped_note = nip37_draft?.wrapped_note else { break post_artifact_block }
|
||||
draft_events.append(wrapped_note)
|
||||
}
|
||||
for (replied_to_note_id, reply_artifacts) in self.replies {
|
||||
guard let replied_to_note = damus_state.ndb.lookup_note(replied_to_note_id)?.unsafeUnownedValue?.to_owned() else { continue }
|
||||
let nip37_draft = try? reply_artifacts.to_nip37_draft(action: .replying_to(replied_to_note), damus_state: damus_state)
|
||||
guard let wrapped_note = nip37_draft?.wrapped_note else { continue }
|
||||
draft_events.append(wrapped_note)
|
||||
}
|
||||
for (quoted_note_id, quote_note_artifacts) in self.quotes {
|
||||
guard let quoted_note = damus_state.ndb.lookup_note(quoted_note_id)?.unsafeUnownedValue?.to_owned() else { continue }
|
||||
let nip37_draft = try? quote_note_artifacts.to_nip37_draft(action: .quoting(quoted_note), damus_state: damus_state)
|
||||
guard let wrapped_note = nip37_draft?.wrapped_note else { continue }
|
||||
draft_events.append(wrapped_note)
|
||||
}
|
||||
for (highlight, highlight_note_artifacts) in self.highlights {
|
||||
let nip37_draft = try? highlight_note_artifacts.to_nip37_draft(action: .highlighting(highlight), damus_state: damus_state)
|
||||
guard let wrapped_note = nip37_draft?.wrapped_note else { continue }
|
||||
draft_events.append(wrapped_note)
|
||||
}
|
||||
|
||||
for draft_event in draft_events {
|
||||
// Implementation note: We do not support draft synchronization with relays yet.
|
||||
// TODO: Once it is time to implement draft syncing with relays, please consider the following:
|
||||
// - Privacy: Sending drafts to the network leaks metadata about app activity, and may break user expectations
|
||||
// - Down-sync conflict resolution: Consider how to solve conflicts for different draft versions holding the same ID (e.g. edited in Damus, then another client, then Damus again)
|
||||
damus_state.pool.send_raw_to_local_ndb(.typical(.event(draft_event)))
|
||||
}
|
||||
|
||||
damus_state.settings.draft_event_ids = draft_events.map({ $0.id.hex() })
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience extensions
|
||||
|
||||
fileprivate extension Array {
|
||||
mutating func appendIfNotNil(_ element: Element?) {
|
||||
if let element = element {
|
||||
self.append(element)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,17 +188,29 @@ extension HighlightEvent {
|
||||
struct HighlightContentDraft: Hashable {
|
||||
let selected_text: String
|
||||
let source: HighlightSource
|
||||
|
||||
|
||||
init(selected_text: String, source: HighlightSource) {
|
||||
self.selected_text = selected_text
|
||||
self.source = source
|
||||
}
|
||||
|
||||
init?(from note: NdbNote) {
|
||||
guard let source = HighlightSource.from(tags: note.tags.strings()) else { return nil }
|
||||
self.source = source
|
||||
self.selected_text = note.content
|
||||
}
|
||||
}
|
||||
|
||||
enum HighlightSource: Hashable {
|
||||
static let TAG_SOURCE_ELEMENT = "source"
|
||||
case event(NostrEvent)
|
||||
case event(NoteId)
|
||||
case external_url(URL)
|
||||
|
||||
func tags() -> [[String]] {
|
||||
switch self {
|
||||
case .event(let event):
|
||||
return [ ["e", "\(event.id)", HighlightSource.TAG_SOURCE_ELEMENT] ]
|
||||
case .event(let event_id):
|
||||
return [ ["e", "\(event_id)", HighlightSource.TAG_SOURCE_ELEMENT] ]
|
||||
case .external_url(let url):
|
||||
return [ ["r", "\(url)", HighlightSource.TAG_SOURCE_ELEMENT] ]
|
||||
}
|
||||
@@ -206,12 +218,26 @@ enum HighlightSource: Hashable {
|
||||
|
||||
func ref() -> RefId {
|
||||
switch self {
|
||||
case .event(let event):
|
||||
return .event(event.id)
|
||||
case .event(let event_id):
|
||||
return .event(event_id)
|
||||
case .external_url(let url):
|
||||
return .reference(url.absoluteString)
|
||||
}
|
||||
}
|
||||
|
||||
static func from(tags: [[String]]) -> HighlightSource? {
|
||||
for tag in tags {
|
||||
if tag.count == 3 && tag[0] == "e" && tag[2] == HighlightSource.TAG_SOURCE_ELEMENT {
|
||||
guard let event_id = NoteId(hex: tag[1]) else { continue }
|
||||
return .event(event_id)
|
||||
}
|
||||
if tag.count == 3 && tag[0] == "r" && tag[2] == HighlightSource.TAG_SOURCE_ELEMENT {
|
||||
guard let url = URL(string: tag[1]) else { continue }
|
||||
return .external_url(url)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
struct ShareContent {
|
||||
|
||||
@@ -79,6 +79,7 @@ class HomeModel: ContactsDelegate {
|
||||
var notifications = NotificationsModel()
|
||||
var notification_status = NotificationStatusModel()
|
||||
var events: EventHolder = EventHolder()
|
||||
var already_reposted: Set<NoteId> = Set()
|
||||
var zap_button: ZapButtonModel = ZapButtonModel()
|
||||
|
||||
init() {
|
||||
@@ -122,6 +123,7 @@ class HomeModel: ContactsDelegate {
|
||||
/// This is called whenever DamusState gets set. This function is used to load or setup anything we need from the new DamusState
|
||||
func load_our_stuff_from_damus_state() {
|
||||
self.load_latest_contact_event_from_damus_state()
|
||||
self.load_drafts_from_damus_state()
|
||||
}
|
||||
|
||||
/// This loads the latest contact event we have on file from NostrDB. This should be called as soon as we get the new DamusState
|
||||
@@ -134,6 +136,10 @@ class HomeModel: ContactsDelegate {
|
||||
process_contact_event(state: damus_state, ev: latest_contact_event)
|
||||
}
|
||||
|
||||
func load_drafts_from_damus_state() {
|
||||
damus_state.drafts.load(from: damus_state)
|
||||
}
|
||||
|
||||
// MARK: - ContactsDelegate functions
|
||||
|
||||
func latest_contact_event_changed(new_event: NostrEvent) {
|
||||
@@ -215,6 +221,10 @@ class HomeModel: ContactsDelegate {
|
||||
break
|
||||
case .status:
|
||||
handle_status_event(ev)
|
||||
case .draft:
|
||||
// TODO: Implement draft syncing with relays. We intentionally do not support that as of writing. See `DraftsModel.swift` for other details
|
||||
// try? damus_state.drafts.load(wrapped_draft_note: ev, with: damus_state)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,6 +376,8 @@ class HomeModel: ContactsDelegate {
|
||||
boost_ev_id = inner_ev.id
|
||||
|
||||
Task {
|
||||
// NOTE (jb55): remove this after nostrdb update, since nostrdb
|
||||
// processess reposts when note is ingested
|
||||
guard validate_event(ev: inner_ev) == .ok else {
|
||||
return
|
||||
}
|
||||
@@ -385,7 +397,7 @@ class HomeModel: ContactsDelegate {
|
||||
switch self.damus_state.boosts.add_event(ev, target: e) {
|
||||
case .already_counted:
|
||||
break
|
||||
case .success(let n):
|
||||
case .success(_):
|
||||
notify(.update_stats(note_id: e))
|
||||
}
|
||||
}
|
||||
@@ -394,7 +406,7 @@ class HomeModel: ContactsDelegate {
|
||||
switch damus_state.quote_reposts.add_event(ev, target: target) {
|
||||
case .already_counted:
|
||||
break
|
||||
case .success(let n):
|
||||
case .success(_):
|
||||
notify(.update_stats(note_id: target))
|
||||
}
|
||||
}
|
||||
@@ -723,6 +735,16 @@ class HomeModel: ContactsDelegate {
|
||||
handle_quote_repost_event(ev, target: quoted_event.note_id)
|
||||
}
|
||||
|
||||
// don't add duplicate reposts to home
|
||||
if ev.known_kind == .boost, let target = ev.get_inner_event()?.id {
|
||||
if already_reposted.contains(target) {
|
||||
Log.info("Skipping duplicate repost for event %s", for: .timeline, target.hex())
|
||||
return
|
||||
} else {
|
||||
already_reposted.insert(target)
|
||||
}
|
||||
}
|
||||
|
||||
if sub_id == home_subid {
|
||||
insert_home_event(ev)
|
||||
} else if sub_id == notifications_subid {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// LongformEvent.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel Nogueira on 2023-11-24.
|
||||
// Created by Daniel D'Aquino on 2023-11-24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@@ -122,6 +122,10 @@ enum MentionRef: TagKeys, TagConvertible, Equatable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
protocol URLEncodable {
|
||||
func url() -> URL?
|
||||
}
|
||||
|
||||
struct Mention<T: Equatable>: Equatable {
|
||||
let index: Int?
|
||||
let ref: T
|
||||
|
||||
@@ -22,8 +22,10 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
var seen_event: Set<NoteId> = Set()
|
||||
var sub_id = UUID().description
|
||||
var prof_subid = UUID().description
|
||||
var conversations_subid = UUID().description
|
||||
var findRelay_subid = UUID().description
|
||||
|
||||
var conversation_events: Set<NoteId> = Set()
|
||||
|
||||
init(pubkey: Pubkey, damus: DamusState) {
|
||||
self.pubkey = pubkey
|
||||
self.damus = damus
|
||||
@@ -59,6 +61,9 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
print("unsubscribing from profile \(pubkey) with sub_id \(sub_id)")
|
||||
damus.pool.unsubscribe(sub_id: sub_id)
|
||||
damus.pool.unsubscribe(sub_id: prof_subid)
|
||||
if pubkey != damus.pubkey {
|
||||
damus.pool.unsubscribe(sub_id: conversations_subid)
|
||||
}
|
||||
}
|
||||
|
||||
func subscribe() {
|
||||
@@ -69,13 +74,29 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
|
||||
text_filter.authors = [pubkey]
|
||||
text_filter.limit = 500
|
||||
|
||||
print("subscribing to profile \(pubkey) with sub_id \(sub_id)")
|
||||
|
||||
print("subscribing to textlike events from profile \(pubkey) with sub_id \(sub_id)")
|
||||
//print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]])
|
||||
damus.pool.subscribe(sub_id: sub_id, filters: [text_filter], handler: handle_event)
|
||||
damus.pool.subscribe(sub_id: prof_subid, filters: [profile_filter], handler: handle_event)
|
||||
|
||||
subscribe_to_conversations()
|
||||
}
|
||||
|
||||
|
||||
private func subscribe_to_conversations() {
|
||||
// Only subscribe to conversation events if the profile is not us.
|
||||
guard pubkey != damus.pubkey else {
|
||||
return
|
||||
}
|
||||
|
||||
let conversation_kinds: [NostrKind] = [.text, .longform, .highlight]
|
||||
let limit: UInt32 = 500
|
||||
let conversations_filter_them = NostrFilter(kinds: conversation_kinds, pubkeys: [damus.pubkey], limit: limit, authors: [pubkey])
|
||||
let conversations_filter_us = NostrFilter(kinds: conversation_kinds, pubkeys: [pubkey], limit: limit, authors: [damus.pubkey])
|
||||
print("subscribing to conversation events from and to profile \(pubkey) with sub_id \(conversations_subid)")
|
||||
damus.pool.subscribe(sub_id: conversations_subid, filters: [conversations_filter_them, conversations_filter_us], handler: handle_event)
|
||||
}
|
||||
|
||||
func handle_profile_contact_event(_ ev: NostrEvent) {
|
||||
process_contact_event(state: damus, ev: ev)
|
||||
|
||||
@@ -90,15 +111,8 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
self.following = count_pubkeys(ev.tags)
|
||||
self.relays = decode_json_relays(ev.content)
|
||||
}
|
||||
|
||||
func add_event(_ ev: NostrEvent) {
|
||||
guard ev.should_show_event else {
|
||||
return
|
||||
}
|
||||
|
||||
if seen_event.contains(ev.id) {
|
||||
return
|
||||
}
|
||||
private func add_event(_ ev: NostrEvent) {
|
||||
if ev.is_textlike || ev.known_kind == .boost {
|
||||
if self.events.insert(ev) {
|
||||
self.objectWillChange.send()
|
||||
@@ -109,24 +123,57 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
seen_event.insert(ev.id)
|
||||
}
|
||||
|
||||
// Ensure the event public key matches the public key(s) we are querying.
|
||||
// This is done to protect against a relay not properly filtering events by the pubkey
|
||||
// See https://github.com/damus-io/damus/issues/1846 for more information
|
||||
private func relay_filtered_correctly(_ ev: NostrEvent, subid: String?) -> Bool {
|
||||
if subid == self.conversations_subid {
|
||||
switch ev.pubkey {
|
||||
case self.pubkey:
|
||||
return ev.referenced_pubkeys.contains(damus.pubkey)
|
||||
case damus.pubkey:
|
||||
return ev.referenced_pubkeys.contains(self.pubkey)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return self.pubkey == ev.pubkey
|
||||
}
|
||||
|
||||
private func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
||||
switch ev {
|
||||
case .ws_event:
|
||||
return
|
||||
case .nostr_event(let resp):
|
||||
guard resp.subid == self.sub_id || resp.subid == self.prof_subid else {
|
||||
guard resp.subid == self.sub_id || resp.subid == self.prof_subid || resp.subid == self.conversations_subid else {
|
||||
return
|
||||
}
|
||||
switch resp {
|
||||
case .ok:
|
||||
break
|
||||
case .event(_, let ev):
|
||||
// Ensure the event public key matches this profiles public key
|
||||
// This is done to protect against a relay not properly filtering events by the pubkey
|
||||
// See https://github.com/damus-io/damus/issues/1846 for more information
|
||||
guard self.pubkey == ev.pubkey else { break }
|
||||
guard ev.should_show_event else {
|
||||
break
|
||||
}
|
||||
|
||||
add_event(ev)
|
||||
if !seen_event.contains(ev.id) {
|
||||
guard relay_filtered_correctly(ev, subid: resp.subid) else {
|
||||
break
|
||||
}
|
||||
|
||||
add_event(ev)
|
||||
|
||||
if resp.subid == self.conversations_subid {
|
||||
conversation_events.insert(ev.id)
|
||||
}
|
||||
} else if resp.subid == self.conversations_subid && !conversation_events.contains(ev.id) {
|
||||
guard relay_filtered_correctly(ev, subid: resp.subid) else {
|
||||
break
|
||||
}
|
||||
|
||||
conversation_events.insert(ev.id)
|
||||
}
|
||||
case .notice:
|
||||
break
|
||||
//notify(.notice, notice)
|
||||
|
||||
@@ -363,6 +363,42 @@ class DamusPurple: StoreObserverDelegate {
|
||||
return freshly_completed_checkouts
|
||||
}
|
||||
|
||||
/// Handles a Purple URL
|
||||
/// - Parameter purple_url: The Purple URL being opened
|
||||
/// - Returns: A view to be shown in the UI
|
||||
@MainActor
|
||||
func handle(purple_url: DamusPurpleURL) async -> ContentView.ViewOpenAction {
|
||||
if case let .welcome(checkout_id) = purple_url.variant {
|
||||
// If this is a welcome link, do the following before showing the onboarding screen:
|
||||
// 1. Check if this is legitimate and good to go.
|
||||
// 2. Mark as complete if this is good to go.
|
||||
let is_good_to_go = try? await check_and_mark_ln_checkout_is_good_to_go(checkout_id: checkout_id)
|
||||
switch is_good_to_go {
|
||||
case .some(let is_good_to_go):
|
||||
if is_good_to_go {
|
||||
return .sheet(.purple(purple_url)) // ALL GOOD, SHOW WELCOME SHEET
|
||||
}
|
||||
else {
|
||||
return .sheet(.error(.init(
|
||||
user_visible_description: NSLocalizedString("You clicked on a Purple welcome link, but it does not look like the checkout is completed yet. This is likely a bug.", comment: "Error label upon continuing in the app from a Damus Purple purchase"),
|
||||
tip: NSLocalizedString("Please double-check the checkout web page, or go to the Side Menu → \"Purple\" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue.", comment: "User-facing tips on what to do if a Purple welcome link doesn't work"),
|
||||
technical_info: "Handling Purple URL \"\(purple_url)\" failed, the `is_good_to_go` result was `\(is_good_to_go)`"
|
||||
)))
|
||||
}
|
||||
case .none:
|
||||
return .sheet(.error(.init(
|
||||
user_visible_description: NSLocalizedString("You clicked on a Purple welcome link, but we could not find your checkout. This is likely a bug.", comment: "Error label upon continuing in the app from a Damus Purple purchase"),
|
||||
tip: NSLocalizedString("Please double-check the checkout web page, or go to the Side Menu → \"Purple\" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue.", comment: "User-facing tips on what to do if a Purple welcome link doesn't work"),
|
||||
technical_info: "Handling Purple URL \"\(purple_url)\" failed, the `is_good_to_go` result was `\(String(describing: is_good_to_go))`"
|
||||
)))
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Show the purple url contents
|
||||
return .sheet(.purple(purple_url))
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// This function checks the status of a specific checkout id with the server
|
||||
/// You should use this result immediately, since it will internally be marked as handled
|
||||
@@ -382,6 +418,13 @@ class DamusPurple: StoreObserverDelegate {
|
||||
let expiry: Date
|
||||
let subscriber_number: Int
|
||||
let active: Bool
|
||||
let attributes: PurpleAccountAttributes
|
||||
|
||||
struct PurpleAccountAttributes: OptionSet {
|
||||
let rawValue: Int
|
||||
|
||||
static let memberForMoreThanOneYear = PurpleAccountAttributes(rawValue: 1 << 0)
|
||||
}
|
||||
|
||||
func ordinal() -> String? {
|
||||
let number = Int(self.subscriber_number)
|
||||
@@ -402,7 +445,8 @@ class DamusPurple: StoreObserverDelegate {
|
||||
created_at: Date.init(timeIntervalSince1970: TimeInterval(payload.created_at)),
|
||||
expiry: Date.init(timeIntervalSince1970: TimeInterval(payload.expiry)),
|
||||
subscriber_number: Int(payload.subscriber_number),
|
||||
active: payload.active
|
||||
active: payload.active,
|
||||
attributes: (payload.attributes?.member_for_more_than_one_year ?? false) ? [.memberForMoreThanOneYear] : []
|
||||
)
|
||||
}
|
||||
|
||||
@@ -412,6 +456,11 @@ class DamusPurple: StoreObserverDelegate {
|
||||
let expiry: UInt64 // Unix timestamp
|
||||
let subscriber_number: UInt
|
||||
let active: Bool
|
||||
let attributes: Attributes?
|
||||
|
||||
struct Attributes: Codable {
|
||||
let member_for_more_than_one_year: Bool
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// DamusPurpleURL.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel Nogueira on 2024-01-13.
|
||||
// Created by Daniel D'Aquino on 2024-01-13.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@@ -7,42 +7,75 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// manages the lifetime of a thread
|
||||
/// manages the lifetime of a thread in a thread view such as `ChatroomThreadView`
|
||||
/// Makes a subscription to the relay pool to get events related to the thread
|
||||
/// It also keeps track of a selected event in the thread, and can pinpoint all of its parents and reply chain
|
||||
@MainActor
|
||||
class ThreadModel: ObservableObject {
|
||||
@Published var event: NostrEvent
|
||||
/// The original event where this thread was loaded from
|
||||
/// We use this to know the starting point from which we try to load the rest of the thread
|
||||
/// This is immutable because this is our starting point of the thread, and we don't expect this to ever change during the lifetime of a thread view
|
||||
let original_event: NostrEvent
|
||||
let highlight: String?
|
||||
var event_map: Set<NostrEvent>
|
||||
/// A map of events, the reply chain, etc
|
||||
/// This can be read by the view, but it can only be updated internally, because it is this classes' responsibility to ensure we load the proper events
|
||||
@Published private(set) var event_map: ThreadEventMap
|
||||
/// The currently selected event
|
||||
/// Can only be directly changed internally. Views should set this via the `select` methods
|
||||
@Published private(set) var selected_event: NostrEvent
|
||||
|
||||
init(event: NostrEvent, damus_state: DamusState, highlight: String? = nil) {
|
||||
self.damus_state = damus_state
|
||||
self.event_map = Set()
|
||||
self.event = event
|
||||
self.original_event = event
|
||||
self.highlight = highlight
|
||||
add_event(event, keypair: damus_state.keypair)
|
||||
/// All of the parent events of `selected_event` in the thread, sorted from the highest level in the thread (The root of the thread), down to the direct parent
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// This is a computed property because we then don't need to worry about keeping things in sync
|
||||
var parent_events: [NostrEvent] {
|
||||
return event_map.parent_events(of: selected_event)
|
||||
}
|
||||
|
||||
func events() -> [NostrEvent] {
|
||||
return Array(event_map).sorted(by: { a, b in
|
||||
return a.created_at < b.created_at
|
||||
/// All of the direct and indirect replies of `selected_event` in the thread. sorted chronologically
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// This is a computed property because we then don't need to worry about keeping things in sync
|
||||
var sorted_child_events: [NostrEvent] {
|
||||
event_map.sorted_recursive_child_events(of: selected_event).filter({
|
||||
should_show_event(event: $0, damus_state: damus_state) // Hide muted events from chatroom conversation
|
||||
})
|
||||
}
|
||||
|
||||
var is_original: Bool {
|
||||
return original_event.id == event.id
|
||||
}
|
||||
|
||||
/// The damus state, needed to access the relay pool and load the thread events
|
||||
let damus_state: DamusState
|
||||
|
||||
let profiles_subid = UUID().description
|
||||
let base_subid = UUID().description
|
||||
let meta_subid = UUID().description
|
||||
|
||||
var subids: [String] {
|
||||
private let profiles_subid = UUID().description
|
||||
private let base_subid = UUID().description
|
||||
private let meta_subid = UUID().description
|
||||
private var subids: [String] {
|
||||
return [profiles_subid, base_subid, meta_subid]
|
||||
}
|
||||
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Initialize this model
|
||||
///
|
||||
/// You should also call `subscribe()` to start loading thread events from the relay pool.
|
||||
/// This is done manually to ensure we only load stuff when needed (e.g. when a view appears)
|
||||
init(event: NostrEvent, damus_state: DamusState) {
|
||||
self.damus_state = damus_state
|
||||
self.event_map = ThreadEventMap()
|
||||
self.original_event = event
|
||||
self.selected_event = event
|
||||
add_event(event, keypair: damus_state.keypair)
|
||||
}
|
||||
|
||||
/// All events in the thread, sorted in chronological order
|
||||
var events: [NostrEvent] {
|
||||
return event_map.sorted_events
|
||||
}
|
||||
|
||||
|
||||
// MARK: Relay pool subscription management
|
||||
|
||||
/// Unsubscribe from events in the relay pool. Call this when unloading the view
|
||||
func unsubscribe() {
|
||||
self.damus_state.pool.remove_handler(sub_id: base_subid)
|
||||
self.damus_state.pool.remove_handler(sub_id: meta_subid)
|
||||
@@ -50,33 +83,25 @@ class ThreadModel: ObservableObject {
|
||||
self.damus_state.pool.unsubscribe(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)")
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func set_active_event(_ ev: NostrEvent, keypair: Keypair) -> Bool {
|
||||
self.event = ev
|
||||
add_event(ev, keypair: keypair)
|
||||
|
||||
//self.objectWillChange.send()
|
||||
return false
|
||||
Log.info("unsubscribing to thread %s with sub_id %s", for: .render, original_event.id.hex(), base_subid)
|
||||
}
|
||||
|
||||
/// Subscribe to events in this thread. Call this when loading the view.
|
||||
func subscribe() {
|
||||
var meta_events = NostrFilter()
|
||||
var quote_events = NostrFilter()
|
||||
var event_filter = NostrFilter()
|
||||
var ref_events = NostrFilter()
|
||||
|
||||
let thread_id = event.thread_id()
|
||||
let thread_id = original_event.thread_id()
|
||||
|
||||
ref_events.referenced_ids = [thread_id, event.id]
|
||||
ref_events.referenced_ids = [thread_id, original_event.id]
|
||||
ref_events.kinds = [.text]
|
||||
ref_events.limit = 1000
|
||||
|
||||
event_filter.ids = [thread_id, event.id]
|
||||
event_filter.ids = [thread_id, original_event.id]
|
||||
|
||||
meta_events.referenced_ids = [event.id]
|
||||
meta_events.referenced_ids = [original_event.id]
|
||||
|
||||
var kinds: [NostrKind] = [.zap, .text, .boost]
|
||||
if !damus_state.settings.onlyzaps_mode {
|
||||
@@ -86,33 +111,40 @@ class ThreadModel: ObservableObject {
|
||||
meta_events.limit = 1000
|
||||
|
||||
quote_events.kinds = [.text]
|
||||
quote_events.quotes = [event.id]
|
||||
quote_events.quotes = [original_event.id]
|
||||
quote_events.limit = 1000
|
||||
|
||||
let base_filters = [event_filter, ref_events]
|
||||
let meta_filters = [meta_events, quote_events]
|
||||
|
||||
print("subscribing to thread \(event.id) with sub_id \(base_subid)")
|
||||
Log.info("subscribing to thread %s with sub_id %s", for: .render, original_event.id.hex(), base_subid)
|
||||
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)
|
||||
}
|
||||
|
||||
/// Adds an event to this thread.
|
||||
/// Normally this does not need to be called externally because it is the responsibility of this class to load the events, not the view's.
|
||||
/// However, this can be called externally for testing purposes (e.g. injecting events for testing)
|
||||
func add_event(_ ev: NostrEvent, keypair: Keypair) {
|
||||
if event_map.contains(ev) {
|
||||
if event_map.contains(id: ev.id) {
|
||||
return
|
||||
}
|
||||
|
||||
damus_state.events.upsert(ev)
|
||||
_ = damus_state.events.upsert(ev)
|
||||
damus_state.replies.count_replies(ev, keypair: keypair)
|
||||
damus_state.events.add_replies(ev: ev, keypair: keypair)
|
||||
|
||||
event_map.insert(ev)
|
||||
event_map.add(event: ev)
|
||||
|
||||
// Publish changes
|
||||
objectWillChange.send()
|
||||
}
|
||||
|
||||
|
||||
/// Handles an incoming event from a relay pool
|
||||
///
|
||||
/// Marked as private because it is this class' responsibility to load events, not the view's. Simplify the interface
|
||||
@MainActor
|
||||
func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
||||
|
||||
private func handle_event(relay_id: RelayURL, ev: NostrConnectionEvent) {
|
||||
let (sub_id, done) = handle_subid_event(pool: damus_state.pool, relay_id: relay_id, ev: ev) { sid, ev in
|
||||
guard subids.contains(sid) else {
|
||||
return
|
||||
@@ -125,7 +157,7 @@ class ThreadModel: ObservableObject {
|
||||
} else if ev.is_textlike {
|
||||
// handle thread quote reposts, we just count them instead of
|
||||
// adding them to the thread
|
||||
if let target = ev.is_quote_repost, target == self.event.id {
|
||||
if let target = ev.is_quote_repost, target == self.selected_event.id {
|
||||
//let _ = self.damus_state.quote_reposts.add_event(ev, target: target)
|
||||
} else {
|
||||
self.add_event(ev, keypair: damus_state.keypair)
|
||||
@@ -139,10 +171,167 @@ class ThreadModel: ObservableObject {
|
||||
|
||||
if sub_id == self.base_subid {
|
||||
guard let txn = NdbTxn(ndb: damus_state.ndb) else { return }
|
||||
load_profiles(context: "thread", profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(Array(event_map)), damus_state: damus_state, txn: txn)
|
||||
load_profiles(context: "thread", profiles_subid: self.profiles_subid, relay_id: relay_id, load: .from_events(Array(event_map.events)), damus_state: damus_state, txn: txn)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: External control interface
|
||||
// Control methods created for the thread view
|
||||
|
||||
/// Change the currently selected event
|
||||
///
|
||||
/// - Parameter event: Event to select
|
||||
func select(event: NostrEvent) {
|
||||
self.selected_event = event
|
||||
add_event(event, keypair: damus_state.keypair)
|
||||
}
|
||||
}
|
||||
|
||||
/// A thread event map, a model that holds events, indexes them, and can efficiently answer questions about a thread.
|
||||
///
|
||||
/// Add events that are part of a thread to this model, and use one of its many convenience functions to get answers about the hierarchy of the thread.
|
||||
///
|
||||
/// This does NOT perform any event loading, networking, or storage operations. This is simply a convenient/efficient way to keep and query about a thread
|
||||
struct ThreadEventMap {
|
||||
/// A map for keeping nostr events, and efficiently querying them by note id
|
||||
///
|
||||
/// Marked as `private` because:
|
||||
/// - We want to hide this complexity from the user of this struct
|
||||
/// - It is this struct's responsibility to keep this in sync with `event_reply_index`
|
||||
private var event_map: [NoteId: NostrEvent] = [:]
|
||||
/// An index of the reply hierarchy, which allows replies to be found in O(1) efficiency
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// Marked as `private` because:
|
||||
/// - We want to hide this complexity from the user of this struct
|
||||
/// - It is this struct's responsibility to keep this in sync with `event_map`
|
||||
///
|
||||
/// We only store note ids to save space, as we can easily get them from `event_map`
|
||||
private var event_reply_index: [NoteId: Set<NoteId>] = [:]
|
||||
|
||||
|
||||
// MARK: External interface
|
||||
|
||||
/// Events in the thread, in no particular order
|
||||
/// Use this when the order does not matter
|
||||
var events: Set<NostrEvent> {
|
||||
return Set(event_map.values)
|
||||
}
|
||||
|
||||
/// Events in the thread, sorted chronologically. Use this when the order matters.
|
||||
/// Use `.events` when the order doesn't matter, as it is more computationally efficient.
|
||||
var sorted_events: [NostrEvent] {
|
||||
return events.sorted(by: { a, b in
|
||||
return a.created_at < b.created_at
|
||||
})
|
||||
}
|
||||
|
||||
/// Add an event to this map
|
||||
///
|
||||
/// Efficiency: O(1)
|
||||
///
|
||||
/// - Parameter event: The event to be added
|
||||
mutating func add(event: NostrEvent) {
|
||||
self.event_map[event.id] = event
|
||||
|
||||
// Update our efficient reply index
|
||||
if let note_id_replied_to = event.direct_replies() {
|
||||
if event_reply_index[note_id_replied_to] == nil {
|
||||
event_reply_index[note_id_replied_to] = [event.id]
|
||||
}
|
||||
else {
|
||||
event_reply_index[note_id_replied_to]?.insert(event.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the thread map contains a given note, referenced by ID
|
||||
///
|
||||
/// Efficiency: O(1)
|
||||
///
|
||||
/// - Parameter id: The ID to look for
|
||||
/// - Returns: True if it does, false otherwise
|
||||
func contains(id: NoteId) -> Bool {
|
||||
return self.event_map[id] != nil
|
||||
}
|
||||
|
||||
/// Gets a note from the thread by its id
|
||||
///
|
||||
/// Efficiency: O(1)
|
||||
///
|
||||
/// - Parameter id: The note id
|
||||
/// - Returns: The note, if it exists in the thread map.
|
||||
func get(id: NoteId) -> NostrEvent? {
|
||||
return self.event_map[id]
|
||||
}
|
||||
|
||||
|
||||
/// Returns all the parent events in a thread, relative to a given event
|
||||
///
|
||||
/// Efficiency: O(N) in the worst case
|
||||
///
|
||||
/// - Parameter query_event: The event for which to find the parents for
|
||||
/// - Returns: An array of parent events, sorted from the highest level in the thread (The root of the thread), down to the direct parent of the query event. If query event is not found, this will return an empty array
|
||||
func parent_events(of query_event: NostrEvent) -> [NostrEvent] {
|
||||
var parents: [NostrEvent] = []
|
||||
var event = query_event
|
||||
while true {
|
||||
guard let direct_reply = event.direct_replies(),
|
||||
let parent_event = self.get(id: direct_reply), parent_event != event
|
||||
else {
|
||||
break
|
||||
}
|
||||
|
||||
parents.append(parent_event)
|
||||
event = parent_event
|
||||
}
|
||||
|
||||
return parents.reversed()
|
||||
}
|
||||
|
||||
|
||||
/// All of the replies in a thread for a given event, including indirect replies (reply of a reply), sorted in chronological order
|
||||
///
|
||||
/// Efficiency: O(Nlog(N)) in the worst case scenario, coming from Swift's built-in sorting algorithm "Timsort"
|
||||
///
|
||||
/// - Parameter query_event: The event for which to find the children for
|
||||
/// - Returns: All of the direct and indirect replies for an event, sorted in chronological order. If query event is not present, this will be an empty array.
|
||||
func sorted_recursive_child_events(of query_event: NostrEvent) -> [NostrEvent] {
|
||||
let all_recursive_child_events = self.recursive_child_events(of: query_event)
|
||||
return all_recursive_child_events.sorted(by: { a, b in
|
||||
return a.created_at < b.created_at
|
||||
})
|
||||
}
|
||||
|
||||
/// All of the replies in a thread for a given event, including indirect replies (reply of a reply), in any order
|
||||
///
|
||||
/// Use this when the order does not matter, as it is more efficient
|
||||
///
|
||||
/// Efficiency: O(N) in the worst case scenario.
|
||||
///
|
||||
/// - Parameter query_event: The event for which to find the children for
|
||||
/// - Returns: All of the direct and indirect replies for an event, sorted in chronological order. If query event is not present, this will be an empty array.
|
||||
func recursive_child_events(of query_event: NostrEvent) -> Set<NostrEvent> {
|
||||
let immediate_children_ids = self.event_reply_index[query_event.id] ?? []
|
||||
var immediate_children: Set<NostrEvent> = []
|
||||
for immediate_child_id in immediate_children_ids {
|
||||
guard let immediate_child = self.event_map[immediate_child_id] else {
|
||||
// This is an internal inconsistency.
|
||||
// Crash the app in debug mode to increase awareness, but let it go in production mode (not mission critical)
|
||||
assertionFailure("Desync between `event_map` and `event_reply_index` should never happen in `ThreadEventMap`!")
|
||||
continue
|
||||
}
|
||||
immediate_children.insert(immediate_child)
|
||||
}
|
||||
|
||||
var indirect_children: Set<NdbNote> = []
|
||||
for immediate_child in immediate_children {
|
||||
let recursive_children = self.recursive_child_events(of: immediate_child)
|
||||
indirect_children = indirect_children.union(recursive_children)
|
||||
}
|
||||
return immediate_children.union(indirect_children)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
107
damus/Models/URLHandler.swift
Normal file
107
damus/Models/URLHandler.swift
Normal file
@@ -0,0 +1,107 @@
|
||||
//
|
||||
// URLHandler.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2024-09-06.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Parses URLs into actions within the app.
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - This exists so that we can separate the logic of parsing the URL and the actual action within the app. That makes the code more readable, testable, and extensible
|
||||
struct DamusURLHandler {
|
||||
/// Parses a URL, handles any needed actions within damus state, and returns the view to be opened in the app
|
||||
///
|
||||
/// Side effects: May mutate `damus_state` in some circumstances
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - damus_state: The Damus state. May be mutated as part of this function
|
||||
/// - url: The URL to be opened
|
||||
/// - Returns: A view to be shown to the user
|
||||
static func handle_opening_url_and_compute_view_action(damus_state: DamusState, url: URL) async -> ContentView.ViewOpenAction {
|
||||
let parsed_url_info = parse_url(url: url)
|
||||
|
||||
switch parsed_url_info {
|
||||
case .profile(let pubkey):
|
||||
return .route(.ProfileByKey(pubkey: pubkey))
|
||||
case .filter(let nostrFilter):
|
||||
let search = SearchModel(state: damus_state, search: nostrFilter)
|
||||
return .route(.Search(search: search))
|
||||
case .event(let nostrEvent):
|
||||
let thread = await ThreadModel(event: nostrEvent, damus_state: damus_state)
|
||||
return .route(.Thread(thread: thread))
|
||||
case .event_reference(let event_reference):
|
||||
return .route(.LoadableNostrEvent(note_reference: event_reference))
|
||||
case .wallet_connect(let walletConnectURL):
|
||||
damus_state.wallet.new(walletConnectURL)
|
||||
return .route(.Wallet(wallet: damus_state.wallet))
|
||||
case .script(let data):
|
||||
let model = ScriptModel(data: data, state: .not_loaded)
|
||||
return .route(.Script(script: model))
|
||||
case .purple(let purple_url):
|
||||
return await damus_state.purple.handle(purple_url: purple_url)
|
||||
case nil:
|
||||
break
|
||||
}
|
||||
return .sheet(.error(ErrorView.UserPresentableError(
|
||||
user_visible_description: NSLocalizedString("Could not parse the URL you are trying to open.", comment: "User visible error description"),
|
||||
tip: NSLocalizedString("Please try again, check the URL for typos, or contact support for further help.", comment: "User visible error tips"),
|
||||
technical_info: "Could not find a suitable open action. User tried to open this URL: \(url.absoluteString)"
|
||||
)))
|
||||
}
|
||||
|
||||
/// Parses a URL into a structured information object.
|
||||
///
|
||||
/// This function does not cause any mutations on the app, or any side-effects.
|
||||
///
|
||||
/// - Parameter url: The URL to be parsed
|
||||
/// - Returns: Structured information about the contents inside the URL. Returns `nil` if URL is not compatible, invalid, or could not be parsed for some reason.
|
||||
static func parse_url(url: URL) -> ParsedURLInfo? {
|
||||
if let purple_url = DamusPurpleURL(url: url) {
|
||||
return .purple(purple_url)
|
||||
}
|
||||
|
||||
if let nwc = WalletConnectURL(str: url.absoluteString) {
|
||||
return .wallet_connect(nwc)
|
||||
}
|
||||
|
||||
guard let link = decode_nostr_uri(url.absoluteString) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch link {
|
||||
case .ref(let ref):
|
||||
switch ref {
|
||||
case .pubkey(let pk):
|
||||
return .profile(pk)
|
||||
case .event(let noteid):
|
||||
return .event_reference(.note_id(noteid))
|
||||
case .hashtag(let ht):
|
||||
return .filter(.filter_hashtag([ht.hashtag]))
|
||||
case .param, .quote, .reference:
|
||||
// doesn't really make sense here
|
||||
break
|
||||
case .naddr(let naddr):
|
||||
return .event_reference(.naddr(naddr))
|
||||
}
|
||||
case .filter(let filt):
|
||||
return .filter(filt)
|
||||
case .script(let script):
|
||||
return .script(script)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
enum ParsedURLInfo {
|
||||
case profile(Pubkey)
|
||||
case filter(NostrFilter)
|
||||
case event(NostrEvent)
|
||||
case event_reference(LoadableNostrEventViewModel.NoteReference)
|
||||
case wallet_connect(WalletConnectURL)
|
||||
case script([UInt8])
|
||||
case purple(DamusPurpleURL)
|
||||
}
|
||||
}
|
||||
@@ -185,9 +185,6 @@ class UserSettingsStore: ObservableObject {
|
||||
|
||||
@Setting(key: "show_music_statuses", default_value: true)
|
||||
var show_music_statuses: Bool
|
||||
|
||||
@Setting(key: "show_only_preferred_languages", default_value: false)
|
||||
var show_only_preferred_languages: Bool
|
||||
|
||||
@Setting(key: "multiple_events_per_pubkey", default_value: false)
|
||||
var multiple_events_per_pubkey: Bool
|
||||
@@ -204,6 +201,10 @@ class UserSettingsStore: ObservableObject {
|
||||
@Setting(key: "developer_mode", default_value: false)
|
||||
var developer_mode: Bool
|
||||
|
||||
/// Makes all post content gibberish and blurhashes images, to avoid distractions when developers are working.
|
||||
@Setting(key: "undistract_mode", default_value: false)
|
||||
var undistractMode: Bool
|
||||
|
||||
@Setting(key: "always_show_onboarding_suggestions", default_value: false)
|
||||
var always_show_onboarding_suggestions: Bool
|
||||
|
||||
@@ -327,9 +328,13 @@ class UserSettingsStore: ObservableObject {
|
||||
|
||||
// MARK: Internal, hidden settings
|
||||
|
||||
// TODO: Get rid of this once we have NostrDB query capabilities integrated
|
||||
@Setting(key: "latest_contact_event_id", default_value: nil)
|
||||
var latest_contact_event_id_hex: String?
|
||||
|
||||
// TODO: Get rid of this once we have NostrDB query capabilities integrated
|
||||
@Setting(key: "draft_event_ids", default_value: nil)
|
||||
var draft_event_ids: [String]?
|
||||
|
||||
// MARK: Helper types
|
||||
|
||||
|
||||
119
damus/NIP37/NIP37Draft.swift
Normal file
119
damus/NIP37/NIP37Draft.swift
Normal file
@@ -0,0 +1,119 @@
|
||||
//
|
||||
// NIP37Draft.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-01-20.
|
||||
//
|
||||
import Foundation
|
||||
|
||||
/// This models a NIP-37 draft.
|
||||
///
|
||||
/// It is an immutable data structure that automatically makes both sides of a NIP-37 draft available: Its unwrapped form and wrapped form.
|
||||
///
|
||||
/// This is useful for keeping it or passing it around to other functions when both sides will be used, or it is not known which side of it will be used.
|
||||
///
|
||||
/// Just initialize it, and read its properties.
|
||||
struct NIP37Draft {
|
||||
// MARK: Properties
|
||||
// Implementation note: Must be immutable to maintain integrity of the structure.
|
||||
|
||||
/// The wrapped version of the draft. That is, a NIP-37 note with draft contents encrypted.
|
||||
let wrapped_note: NdbNote
|
||||
/// The unwrapped version of the draft. That is, the actual note that was being drafted.
|
||||
let unwrapped_note: NdbNote
|
||||
/// The unique ID of the draft, as per NIP-37
|
||||
var id: String? {
|
||||
return self.wrapped_note.referenced_params.first?.param.string()
|
||||
}
|
||||
|
||||
|
||||
// MARK: Initialization
|
||||
|
||||
/// Basic initializer
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - Using this externally defeats the whole purpose of using this struct, so this is kept private.
|
||||
private init(wrapped_note: NdbNote, unwrapped_note: NdbNote) {
|
||||
self.wrapped_note = wrapped_note
|
||||
self.unwrapped_note = unwrapped_note
|
||||
}
|
||||
|
||||
/// Initializes object with a wrapped NIP-37 note, if the keys can decrypt it.
|
||||
/// - Parameters:
|
||||
/// - wrapped_note: NIP-37 note
|
||||
/// - keypair: The keys to decrypt
|
||||
init?(wrapped_note: NdbNote, keypair: FullKeypair) throws {
|
||||
self.wrapped_note = wrapped_note
|
||||
guard let unwrapped_note = try Self.unwrap(note: wrapped_note, keypair: keypair) else { return nil }
|
||||
self.unwrapped_note = unwrapped_note
|
||||
}
|
||||
|
||||
/// Initializes object with an event to be wrapped into a NIP-37 draft
|
||||
/// - Parameters:
|
||||
/// - unwrapped_note: a note to be wrapped
|
||||
/// - draft_id: the unique ID of this draft, as per NIP-37
|
||||
/// - keypair: the keys to use for encrypting
|
||||
init?(unwrapped_note: NdbNote, draft_id: String, keypair: FullKeypair) throws {
|
||||
self.unwrapped_note = unwrapped_note
|
||||
guard let wrapped_note = try Self.wrap(note: unwrapped_note, draft_id: draft_id, keypair: keypair) else { return nil }
|
||||
self.wrapped_note = wrapped_note
|
||||
}
|
||||
|
||||
|
||||
// MARK: Static functions
|
||||
// Use these when you just need to wrap/unwrap once
|
||||
|
||||
|
||||
/// A function that wraps a note into NIP-37 draft event
|
||||
/// - Parameters:
|
||||
/// - note: the note that needs to be wrapped
|
||||
/// - draft_id: the unique ID of the draft, as per NIP-37
|
||||
/// - keypair: the keys to use for encrypting
|
||||
/// - Returns: A NIP-37 draft, if it succeeds.
|
||||
static func wrap(note: NdbNote, draft_id: String, keypair: FullKeypair) throws -> NdbNote? {
|
||||
let note_json_data = try JSONEncoder().encode(note)
|
||||
guard let note_json_string = String(data: note_json_data, encoding: .utf8) else {
|
||||
throw NIP37DraftEventError.encoding_error
|
||||
}
|
||||
guard let contents = try? NIP44v2Encryption.encrypt(plaintext: note_json_string, privateKeyA: keypair.privkey, publicKeyB: keypair.pubkey) else {
|
||||
return nil
|
||||
}
|
||||
var tags = [
|
||||
["d", draft_id],
|
||||
["k", String(note.kind)],
|
||||
]
|
||||
|
||||
if let replied_to_note = note.direct_replies() {
|
||||
tags.append(["e", replied_to_note.hex()])
|
||||
}
|
||||
guard let wrapped_event = NostrEvent(
|
||||
content: contents,
|
||||
keypair: keypair.to_keypair(),
|
||||
kind: NostrKind.draft.rawValue,
|
||||
tags: tags
|
||||
) else { return nil }
|
||||
return wrapped_event
|
||||
}
|
||||
|
||||
/// A function that unwraps and decrypts a NIP-37 draft
|
||||
/// - Parameters:
|
||||
/// - note: NIP-37 note to be unwrapped
|
||||
/// - keypair: The keys to use for decrypting
|
||||
/// - Returns: The unwrapped note, if it can be decrypted/unwrapped.
|
||||
static func unwrap(note: NdbNote, keypair: FullKeypair) throws -> NdbNote? {
|
||||
let wrapped_note = note
|
||||
guard wrapped_note.known_kind == .draft else { return nil }
|
||||
guard let draft_event_json = try? NIP44v2Encryption.decrypt(
|
||||
payload: wrapped_note.content,
|
||||
privateKeyA: keypair.privkey,
|
||||
publicKeyB: keypair.pubkey
|
||||
) else { return nil }
|
||||
return NdbNote.owned_from_json(json: draft_event_json)
|
||||
}
|
||||
|
||||
enum NIP37DraftEventError: Error {
|
||||
case invalid_keypair
|
||||
case encoding_error
|
||||
}
|
||||
}
|
||||
357
damus/NIP44/NIP44.swift
Normal file
357
damus/NIP44/NIP44.swift
Normal file
@@ -0,0 +1,357 @@
|
||||
//
|
||||
// NIP44.swift
|
||||
// damus
|
||||
//
|
||||
// Based on NIP44v2Encrypting.swift created by Terry Yiu on 3/16/24, from https://github.com/nostr-sdk/nostr-sdk-ios, which is MIT licensed.
|
||||
//
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2023 Nostr SDK
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
//
|
||||
// Adapted by Daniel D’Aquino on 2025-02-10.
|
||||
//
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
import CryptoSwift
|
||||
import secp256k1
|
||||
|
||||
struct NIP44v2Encryption {
|
||||
|
||||
/// Produces a `String` containing `plaintext` that has been encrypted using the `privateKey` of user A and the `publicKey` of user B.
|
||||
///
|
||||
/// The result is non-deterministic because a cryptographically secure pseudorandom generated nonce is used each time,
|
||||
/// but can be decrypted deterministically with a call to ``NIP44v2Encryption/decrypt(payload:privateKeyA:publicKeyB:)``,
|
||||
/// where user A and user B are interchangeable.
|
||||
///
|
||||
/// This function can `throw` an error from ``EncryptionError`` if it fails to encrypt the plaintext.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - plaintext: The plaintext to encrypt.
|
||||
/// - privateKeyA: The private key of user A.
|
||||
/// - publicKeyB: The public key of user B.
|
||||
/// - Returns: The encrypted ciphertext.
|
||||
static func encrypt(plaintext: String, privateKeyA: Privkey, publicKeyB: Pubkey) throws -> String {
|
||||
let conversationKey = try conversationKey(privateKeyA: privateKeyA, publicKeyB: publicKeyB)
|
||||
|
||||
return try encrypt(plaintext: plaintext, conversationKey: conversationKey)
|
||||
}
|
||||
|
||||
/// Produces a `String` containing `payload` that has been decrypted using the `privateKey` of user A and the `publicKey` of user B,
|
||||
/// and the result is identical to if the `privateKey` of user B and `publicKey` of user A were used to decrypt `payload` instead.
|
||||
///
|
||||
/// Any ciphertext returned from the call to ``NIP44v2Encryption/encrypt(plaintext:privateKeyA:publicKeyB:)``
|
||||
/// can be decrypted, where user A and B are interchangeable.
|
||||
///
|
||||
/// This function can `throw` an error from ``EncryptionError`` if it fails to decrypt the payload.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - payload: The payload to decrypt.
|
||||
/// - privateKeyA: The private key of user A.
|
||||
/// - publicKeyB: The public key of user B.
|
||||
/// - Returns: The decrypted plaintext message.
|
||||
static func decrypt(payload: String, privateKeyA: Privkey, publicKeyB: Pubkey) throws -> String {
|
||||
let conversationKey = try conversationKey(privateKeyA: privateKeyA, publicKeyB: publicKeyB)
|
||||
|
||||
return try decrypt(payload: payload, conversationKey: conversationKey)
|
||||
}
|
||||
|
||||
/// Calculates length of the padded byte array.
|
||||
static func calculatePaddedLength(_ unpaddedLength: Int) throws -> Int {
|
||||
guard unpaddedLength > 0 else {
|
||||
throw EncryptionError.unpaddedLengthInvalid(unpaddedLength)
|
||||
}
|
||||
if unpaddedLength <= 32 {
|
||||
return 32
|
||||
}
|
||||
|
||||
let nextPower = 1 << (Int(floor(log2(Double(unpaddedLength) - 1))) + 1)
|
||||
let chunk: Int
|
||||
|
||||
if nextPower <= 256 {
|
||||
chunk = 32
|
||||
} else {
|
||||
chunk = nextPower / 8
|
||||
}
|
||||
|
||||
return chunk * (Int(floor((Double(unpaddedLength) - 1) / Double(chunk))) + 1)
|
||||
}
|
||||
|
||||
/// Converts unpadded plaintext to padded bytes.
|
||||
static func pad(_ plaintext: String) throws -> Data {
|
||||
guard let unpadded = plaintext.data(using: .utf8) else {
|
||||
throw EncryptionError.utf8EncodingFailed
|
||||
}
|
||||
|
||||
let unpaddedLength = unpadded.count
|
||||
|
||||
guard 1...65535 ~= unpaddedLength else {
|
||||
throw EncryptionError.plaintextLengthInvalid(unpaddedLength)
|
||||
}
|
||||
|
||||
var prefix = Data(count: 2)
|
||||
prefix.withUnsafeMutableBytes { (ptr: UnsafeMutableRawBufferPointer) in
|
||||
ptr.storeBytes(of: UInt16(unpaddedLength).bigEndian, as: UInt16.self)
|
||||
}
|
||||
|
||||
let suffix = Data(count: try calculatePaddedLength(unpaddedLength) - unpaddedLength)
|
||||
|
||||
return prefix + unpadded + suffix
|
||||
}
|
||||
|
||||
/// Converts padded bytes to unpadded plaintext.
|
||||
static func unpad(_ padded: Data) throws -> String {
|
||||
guard padded.count >= 2 else {
|
||||
throw EncryptionError.paddingInvalid
|
||||
}
|
||||
|
||||
let unpaddedLength = (Int(padded[0]) << 8) | Int(padded[1])
|
||||
|
||||
guard 2+unpaddedLength <= padded.count else {
|
||||
throw EncryptionError.paddingInvalid
|
||||
}
|
||||
|
||||
let unpadded = toBytes(from: padded)[2..<2+unpaddedLength]
|
||||
let paddedLength = try calculatePaddedLength(unpaddedLength)
|
||||
|
||||
guard unpaddedLength > 0,
|
||||
unpadded.count == unpaddedLength,
|
||||
padded.count == 2 + paddedLength,
|
||||
let result = String(data: Data(unpadded), encoding: .utf8) else {
|
||||
throw EncryptionError.paddingInvalid
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
static func decodePayload(_ payload: String) throws -> DecodedPayload {
|
||||
let payloadLength = payload.count
|
||||
|
||||
guard payloadLength > 0 && payload.first != "#" else {
|
||||
throw EncryptionError.unknownVersion()
|
||||
}
|
||||
guard 132...87472 ~= payloadLength else {
|
||||
throw EncryptionError.payloadSizeInvalid(payloadLength)
|
||||
}
|
||||
|
||||
guard let data = Data(base64Encoded: payload) else {
|
||||
throw EncryptionError.base64EncodingFailed
|
||||
}
|
||||
|
||||
let dataLength = data.count
|
||||
|
||||
guard 99...65603 ~= dataLength else {
|
||||
throw EncryptionError.dataSizeInvalid(dataLength)
|
||||
}
|
||||
|
||||
guard let version = data.first else {
|
||||
throw EncryptionError.unknownVersion()
|
||||
}
|
||||
|
||||
guard version == 2 else {
|
||||
throw EncryptionError.unknownVersion(Int(version))
|
||||
}
|
||||
|
||||
let nonce = data[data.index(data.startIndex, offsetBy: 1)..<data.index(data.startIndex, offsetBy: 33)]
|
||||
let ciphertext = data[data.index(data.startIndex, offsetBy: 33)..<data.index(data.startIndex, offsetBy: dataLength - 32)]
|
||||
let mac = data[data.index(data.startIndex, offsetBy: dataLength - 32)..<data.index(data.startIndex, offsetBy: dataLength)]
|
||||
|
||||
return DecodedPayload(nonce: nonce, ciphertext: ciphertext, mac: mac)
|
||||
}
|
||||
|
||||
static func hmacAad(key: Data, message: Data, aad: Data) throws -> Data {
|
||||
guard aad.count == 32 else {
|
||||
throw EncryptionError.aadLengthInvalid(aad.count)
|
||||
}
|
||||
|
||||
let combined = aad + message
|
||||
|
||||
return Data(CryptoKit.HMAC<CryptoKit.SHA256>.authenticationCode(for: combined, using: SymmetricKey(data: key)))
|
||||
}
|
||||
|
||||
static func toBytes(from data: Data) -> [UInt8] {
|
||||
data.withUnsafeBytes { bytesPointer in Array(bytesPointer) }
|
||||
}
|
||||
|
||||
static func preparePublicKeyBytes(from publicKey: Pubkey) throws -> [UInt8] {
|
||||
let publicKeyBytes = publicKey.bytes
|
||||
|
||||
let prefix = Data([2])
|
||||
let prefixBytes = toBytes(from: prefix)
|
||||
|
||||
return prefixBytes + publicKeyBytes
|
||||
}
|
||||
|
||||
static func parsePublicKey(from bytes: [UInt8]) throws -> secp256k1_pubkey {
|
||||
var publicKey = secp256k1_pubkey()
|
||||
guard secp256k1_ec_pubkey_parse(secp256k1.Context.raw, &publicKey, bytes, bytes.count) == 1 else {
|
||||
throw EncryptionError.publicKeyInvalid
|
||||
}
|
||||
return publicKey
|
||||
}
|
||||
|
||||
static func computeSharedSecret(using publicKey: secp256k1_pubkey, and privateKeyBytes: [UInt8]) throws -> [UInt8] {
|
||||
var sharedSecret = [UInt8](repeating: 0, count: 32)
|
||||
var mutablePublicKey = publicKey
|
||||
|
||||
// Multiplication of point B by scalar a (a ⋅ B), defined in [BIP340](https://github.com/bitcoin/bips/blob/e918b50731397872ad2922a1b08a5a4cd1d6d546/bip-0340.mediawiki).
|
||||
// The operation produces a shared point, and we encode the shared point's 32-byte x coordinate, using method bytes(P) from BIP340.
|
||||
// Private and public keys must be validated as per BIP340: pubkey must be a valid, on-curve point, and private key must be a scalar in range [1, secp256k1_order - 1]
|
||||
guard secp256k1_ecdh(secp256k1.Context.raw, &sharedSecret, &mutablePublicKey, privateKeyBytes, { (output, x32, _, _) in
|
||||
memcpy(output, x32, 32)
|
||||
return 1
|
||||
}, nil) != 0 else {
|
||||
throw EncryptionError.sharedSecretComputationFailed
|
||||
}
|
||||
return sharedSecret
|
||||
}
|
||||
|
||||
/// Calculates long-term key between users A and B.
|
||||
/// The conversation key of A's private key and B's public key is equal to the conversation key of B's private key and A's public key.
|
||||
static func conversationKey(privateKeyA: Privkey, publicKeyB: Pubkey) throws -> ContiguousBytes {
|
||||
let privateKeyABytes = privateKeyA.bytes
|
||||
let publicKeyBBytes = try preparePublicKeyBytes(from: publicKeyB)
|
||||
let parsedPublicKeyB = try parsePublicKey(from: publicKeyBBytes)
|
||||
let sharedSecret = try computeSharedSecret(using: parsedPublicKeyB, and: privateKeyABytes)
|
||||
|
||||
return CryptoKit.HKDF<CryptoKit.SHA256>.extract(inputKeyMaterial: SymmetricKey(data: sharedSecret), salt: Data("nip44-v2".utf8))
|
||||
}
|
||||
|
||||
/// Calculates unique per-message key.
|
||||
static func messageKeys(conversationKey: ContiguousBytes, nonce: Data) throws -> MessageKeys {
|
||||
let conversationKeyByteCount = conversationKey.bytes.count
|
||||
guard conversationKeyByteCount == 32 else {
|
||||
throw EncryptionError.conversationKeyLengthInvalid(conversationKeyByteCount)
|
||||
}
|
||||
|
||||
guard nonce.count == 32 else {
|
||||
throw EncryptionError.nonceLengthInvalid(nonce.count)
|
||||
}
|
||||
|
||||
let keys = CryptoKit.HKDF<CryptoKit.SHA256>.expand(pseudoRandomKey: conversationKey, info: nonce, outputByteCount: 76)
|
||||
let keysBytes = keys.bytes
|
||||
|
||||
let chaChaKey = Data(keysBytes[0..<32])
|
||||
let chaChaNonce = Data(keysBytes[32..<44])
|
||||
let hmacKey = Data(keysBytes[44..<76])
|
||||
|
||||
return MessageKeys(chaChaKey: chaChaKey, chaChaNonce: chaChaNonce, hmacKey: hmacKey)
|
||||
}
|
||||
|
||||
static func encrypt(plaintext: String, conversationKey: ContiguousBytes, nonce: Data? = nil) throws -> String {
|
||||
let nonceData: Data
|
||||
if let nonce {
|
||||
nonceData = nonce
|
||||
} else {
|
||||
// Fetches randomness from CSPRNG.
|
||||
nonceData = Data.secureRandomBytes(count: 32)
|
||||
}
|
||||
|
||||
let messageKeys = try messageKeys(conversationKey: conversationKey, nonce: nonceData)
|
||||
let padded = try pad(plaintext)
|
||||
let paddedBytes = toBytes(from: padded)
|
||||
|
||||
let chaChaKey = toBytes(from: messageKeys.chaChaKey)
|
||||
let chaChaNonce = toBytes(from: messageKeys.chaChaNonce)
|
||||
|
||||
let ciphertext = try ChaCha20(key: chaChaKey, iv: chaChaNonce).encrypt(paddedBytes)
|
||||
let ciphertextData = Data(ciphertext)
|
||||
|
||||
let mac = try hmacAad(key: messageKeys.hmacKey, message: ciphertextData, aad: nonceData)
|
||||
|
||||
let data = Data([2]) + nonceData + ciphertextData + mac
|
||||
return data.base64EncodedString()
|
||||
}
|
||||
|
||||
static func decrypt(payload: String, conversationKey: ContiguousBytes) throws -> String {
|
||||
let decodedPayload = try decodePayload(payload)
|
||||
let nonce = decodedPayload.nonce
|
||||
let ciphertext = decodedPayload.ciphertext
|
||||
let ciphertextBytes = toBytes(from: ciphertext)
|
||||
let mac = decodedPayload.mac
|
||||
|
||||
let messageKeys = try messageKeys(conversationKey: conversationKey, nonce: nonce)
|
||||
|
||||
let calculatedMac = try hmacAad(key: messageKeys.hmacKey, message: ciphertext, aad: nonce)
|
||||
|
||||
guard calculatedMac == mac else {
|
||||
throw EncryptionError.macInvalid
|
||||
}
|
||||
|
||||
let chaChaNonce = toBytes(from: messageKeys.chaChaNonce)
|
||||
let chaChaKey = toBytes(from: messageKeys.chaChaKey)
|
||||
|
||||
let paddedPlaintext = try ChaCha20(key: chaChaKey, iv: chaChaNonce).decrypt(ciphertextBytes)
|
||||
let paddedPlaintextData = Data(paddedPlaintext.bytes)
|
||||
|
||||
return try unpad(paddedPlaintextData)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Helper structures and extensions
|
||||
|
||||
extension Data {
|
||||
/// Random data of a given size, from CSPRNG
|
||||
/// - Parameter count: The size of the data, in bytes
|
||||
/// - Returns: Bytes randomly generated from CSPRNG
|
||||
static func secureRandomBytes(count: Int) -> Data {
|
||||
var bytes = [Int8](repeating: 0, count: count)
|
||||
guard SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) == errSecSuccess else {
|
||||
fatalError("can't copy secure random data")
|
||||
}
|
||||
return Data(bytes: bytes, count: count)
|
||||
}
|
||||
}
|
||||
|
||||
extension NIP44v2Encryption {
|
||||
struct DecodedPayload {
|
||||
let nonce: Data
|
||||
let ciphertext: Data
|
||||
let mac: Data
|
||||
}
|
||||
|
||||
struct MessageKeys {
|
||||
let chaChaKey: Data
|
||||
let chaChaNonce: Data
|
||||
let hmacKey: Data
|
||||
}
|
||||
|
||||
public enum EncryptionError: Error {
|
||||
case aadLengthInvalid(Int)
|
||||
case base64EncodingFailed
|
||||
case chaCha20DecryptionFailed
|
||||
case chaCha20EncryptionFailed
|
||||
case conversationKeyLengthInvalid(Int)
|
||||
case dataSizeInvalid(Int)
|
||||
case macInvalid
|
||||
case nonceLengthInvalid(Int)
|
||||
case paddingInvalid
|
||||
case payloadSizeInvalid(Int)
|
||||
case plaintextLengthInvalid(Int)
|
||||
case privateKeyInvalid
|
||||
case publicKeyInvalid
|
||||
case sharedSecretComputationFailed
|
||||
case unknownVersion(Int? = nil)
|
||||
case unpaddedLengthInvalid(Int)
|
||||
case utf8EncodingFailed
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,8 @@ class ProfileRecord {
|
||||
}
|
||||
|
||||
guard let profile = data.profile,
|
||||
let addr = profile.lud16 ?? profile.lud06 else {
|
||||
let addr = (profile.lud16 ?? profile.lud06)?.trimmingCharacters(in: .whitespaces)
|
||||
else {
|
||||
return nil;
|
||||
}
|
||||
|
||||
@@ -301,7 +302,7 @@ class Profile: Codable {
|
||||
*/
|
||||
|
||||
func make_test_profile() -> Profile {
|
||||
return Profile(name: "jb55", display_name: "Will", about: "Its a me", picture: "https://cdn.jb55.com/img/red-me.jpg", banner: "https://pbs.twimg.com/profile_banners/9918032/1531711830/600x200", website: "jb55.com", lud06: "jb55@jb55.com", lud16: nil, nip05: "jb55@jb55.com", damus_donation: 1)
|
||||
return Profile(name: "jb55", display_name: "Will", about: "Its a me", picture: "https://cdn.jb55.com/img/red-me.jpg", banner: "https://pbs.twimg.com/profile_banners/9918032/1531711830/600x200", website: "jb55.com", lud06: nil, lud16: "jb55@jb55.com", nip05: "jb55@jb55.com", damus_donation: 1)
|
||||
}
|
||||
|
||||
func make_ln_url(_ str: String?) -> URL? {
|
||||
|
||||
@@ -19,6 +19,7 @@ enum NostrKind: UInt32, Codable {
|
||||
case chat = 42
|
||||
case mute_list = 10000
|
||||
case list_deprecated = 30000
|
||||
case draft = 31234
|
||||
case longform = 30023
|
||||
case zap = 9735
|
||||
case zap_request = 9734
|
||||
|
||||
@@ -7,19 +7,11 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct LocalNotificationNotify: Notify {
|
||||
typealias Payload = LossyLocalNotification
|
||||
var payload: Payload
|
||||
}
|
||||
|
||||
extension NotifyHandler {
|
||||
static var local_notification: NotifyHandler<LocalNotificationNotify> {
|
||||
.init()
|
||||
}
|
||||
}
|
||||
|
||||
extension Notifications {
|
||||
static func local_notification(_ payload: LossyLocalNotification) -> Notifications<LocalNotificationNotify> {
|
||||
.init(.init(payload: payload))
|
||||
}
|
||||
extension QueueableNotify<LossyLocalNotification> {
|
||||
/// A shared singleton for opening local and push user notifications
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - The queue can only hold one element. This is done because if the user hypothetically opened 10 push notifications and there was a lag, we wouldn't want the app to suddenly open 10 different things.
|
||||
static let shared = QueueableNotify(maxQueueItems: 1)
|
||||
}
|
||||
|
||||
90
damus/Notify/QueueableNotify.swift
Normal file
90
damus/Notify/QueueableNotify.swift
Normal file
@@ -0,0 +1,90 @@
|
||||
//
|
||||
// QueueableNotify.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-02-14.
|
||||
//
|
||||
|
||||
/// This notifies another object about some payload,
|
||||
/// with automatic "queueing" of messages if there are no listeners.
|
||||
///
|
||||
/// When used as a singleton, this can be used to easily send notifications to be handled at the app-level.
|
||||
///
|
||||
/// This serves the same purpose as `Notify`, except this implements the queueing of messages,
|
||||
/// which means that messages can be handled even if the listener is not instantiated yet.
|
||||
///
|
||||
/// **Example:** The app delegate can send some events that need handling from `ContentView` — but some can occur before `ContentView` is even instantiated.
|
||||
///
|
||||
///
|
||||
/// ## Usage notes
|
||||
///
|
||||
/// - This code was mainly written to have one listener at a time. Have more than one listener may be possible, but this class has not been tested/optimized for that purpose.
|
||||
///
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - This makes heavy use of `AsyncStream` and continuations, because that allows complexities here to be handled elegantly with a simple "for-in" loop
|
||||
/// - Without this, it would take a couple of callbacks and manual handling of queued items to achieve the same effect
|
||||
/// - Modeled as an `actor` for extra thread-safety
|
||||
actor QueueableNotify<T: Sendable> {
|
||||
/// The continuation, which allows us to publish new items to the listener
|
||||
/// If `nil`, that means there is no listeners to the stream, which is used for determining whether to queue new incoming items.
|
||||
private var continuation: AsyncStream<T>.Continuation?
|
||||
/// Holds queue items
|
||||
private var queue: [T] = []
|
||||
/// The maximum amount of items allowed in the queue. Older items will be discarded from the queue after it is full
|
||||
var maxQueueItems: Int
|
||||
|
||||
/// Initializes the object
|
||||
/// - Parameter maxQueueItems: The maximum amount of items allowed in the queue. Older items will be discarded from the queue after it is full
|
||||
init(maxQueueItems: Int) {
|
||||
self.maxQueueItems = maxQueueItems
|
||||
}
|
||||
|
||||
/// The async stream, used for listening for notifications
|
||||
///
|
||||
/// This will first stream the queued "inbox" items that the listener may have missed, and then it will do a real-time stream of new items as they come in.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// ```swift
|
||||
/// for await notification in queueableNotify.stream {
|
||||
/// // Do something with the notification
|
||||
/// }
|
||||
/// ```
|
||||
var stream: AsyncStream<T> {
|
||||
return AsyncStream { continuation in
|
||||
// Stream queued "inbox" items that the listener may have missed
|
||||
for item in queue {
|
||||
continuation.yield(item)
|
||||
}
|
||||
|
||||
// Clean up if the stream closes
|
||||
continuation.onTermination = { continuation in
|
||||
Task { await self.cleanup() }
|
||||
}
|
||||
|
||||
// Point to this stream, so that it can receive new updates
|
||||
self.continuation = continuation
|
||||
}
|
||||
}
|
||||
|
||||
/// Cleans up after a stream is closed by the listener
|
||||
private func cleanup() {
|
||||
self.continuation = nil // This will cause new items to be queued for when another listener is attached
|
||||
}
|
||||
|
||||
/// Adds a new notification item to be handled by a listener.
|
||||
///
|
||||
/// This will automatically stream the new item to the listener, or queue the item if no one is listening
|
||||
func add(item: T) {
|
||||
while queue.count >= maxQueueItems { queue.removeFirst() } // Ensures queue stays within the desired size
|
||||
guard let continuation else {
|
||||
// No one is listening, queue it (send it to an inbox for later handling)
|
||||
queue.append(item)
|
||||
return
|
||||
}
|
||||
// Send directly to the active listener stream
|
||||
continuation.yield(item)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,10 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
/// General app-wide constants
|
||||
///
|
||||
/// ## Implementation notes:
|
||||
/// - Force unwrapping in this class is generally ok, because the contents are static, and so we can easily provide guarantees that they will not crash the app.
|
||||
class Constants {
|
||||
//static let EXAMPLE_DEMOS: DamusState = .empty
|
||||
static let DAMUS_APP_GROUP_IDENTIFIER: String = "group.com.damus"
|
||||
@@ -32,6 +36,9 @@ class Constants {
|
||||
static let DAMUS_WEBSITE_STAGING_URL: URL = URL(string: "https://staging.damus.io")!
|
||||
static let DAMUS_WEBSITE_PRODUCTION_URL: URL = URL(string: "https://damus.io")!
|
||||
|
||||
// MARK: Damus Company Info
|
||||
static let SUPPORT_PUBKEY: Pubkey = Pubkey(hex: "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681")!
|
||||
|
||||
// MARK: General constants
|
||||
static let GIF_IMAGE_TYPE: String = "com.compuserve.gif"
|
||||
}
|
||||
|
||||
@@ -259,11 +259,10 @@ func should_translate(event: NostrEvent, our_keypair: Keypair, note_lang: String
|
||||
}
|
||||
|
||||
if let note_lang {
|
||||
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
|
||||
let currentLanguage = localeToLanguage(Locale.current.identifier)
|
||||
|
||||
// Don't translate if its in our preferred languages
|
||||
guard !preferredLanguages.contains(note_lang) else {
|
||||
// if its the same, give up and don't retry
|
||||
// Don't translate if the note is in our current language
|
||||
guard currentLanguage != note_lang else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,17 @@ let ANON_PUBKEY = Pubkey(Data([
|
||||
struct FullKeypair: Equatable {
|
||||
let pubkey: Pubkey
|
||||
let privkey: Privkey
|
||||
|
||||
init(pubkey: Pubkey, privkey: Privkey) {
|
||||
self.pubkey = pubkey
|
||||
self.privkey = privkey
|
||||
}
|
||||
|
||||
init?(privkey: Privkey) {
|
||||
self.privkey = privkey
|
||||
guard let pubkey = privkey_to_pubkey_raw(sec: privkey.bytes) else { return nil }
|
||||
self.pubkey = pubkey
|
||||
}
|
||||
|
||||
func to_keypair() -> Keypair {
|
||||
return Keypair(pubkey: pubkey, privkey: privkey)
|
||||
|
||||
@@ -14,6 +14,7 @@ enum LogCategory: String {
|
||||
case render
|
||||
case storage
|
||||
case networking
|
||||
case timeline
|
||||
case push_notifications
|
||||
case damus_purple
|
||||
case image_uploading
|
||||
|
||||
@@ -32,6 +32,7 @@ enum Route: Hashable {
|
||||
case DeveloperSettings(settings: UserSettingsStore)
|
||||
case FirstAidSettings(settings: UserSettingsStore)
|
||||
case Thread(thread: ThreadModel)
|
||||
case LoadableNostrEvent(note_reference: LoadableNostrEventViewModel.NoteReference)
|
||||
case Reposts(reposts: EventsModel)
|
||||
case QuoteReposts(quotes: EventsModel)
|
||||
case Reactions(reactions: EventsModel)
|
||||
@@ -96,6 +97,8 @@ enum Route: Hashable {
|
||||
case .Thread(let thread):
|
||||
ChatroomThreadView(damus: damusState, thread: thread)
|
||||
//ThreadView(state: damusState, thread: thread)
|
||||
case .LoadableNostrEvent(let note_reference):
|
||||
LoadableNostrEventView(state: damusState, note_reference: note_reference)
|
||||
case .Reposts(let reposts):
|
||||
RepostsView(damus_state: damusState, model: reposts)
|
||||
case .QuoteReposts(let quote_reposts):
|
||||
@@ -186,7 +189,10 @@ enum Route: Hashable {
|
||||
hasher.combine("firstAidSettings")
|
||||
case .Thread(let threadModel):
|
||||
hasher.combine("thread")
|
||||
hasher.combine(threadModel.event.id)
|
||||
hasher.combine(threadModel.original_event.id)
|
||||
case .LoadableNostrEvent(note_reference: let note_reference):
|
||||
hasher.combine("loadable_nostr_event")
|
||||
hasher.combine(note_reference)
|
||||
case .Reposts(let reposts):
|
||||
hasher.combine("reposts")
|
||||
hasher.combine(reposts.target)
|
||||
|
||||
30
damus/Util/Undistractor.swift
Normal file
30
damus/Util/Undistractor.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// Undistractor.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-02-19.
|
||||
//
|
||||
|
||||
/// Keeping the minds of developers safe from the occupational hazard of social media distractions when testing Damus since 2025
|
||||
struct Undistractor {
|
||||
static func makeGibberish(text: String) -> String {
|
||||
let lowercaseLetters = "abcdefghijklmnopqrstuvwxyz"
|
||||
let uppercaseLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
var transformedText = ""
|
||||
|
||||
for char in text {
|
||||
if lowercaseLetters.contains(char) {
|
||||
if let randomLetter = lowercaseLetters.randomElement() {
|
||||
transformedText.append(randomLetter)
|
||||
}
|
||||
} else if uppercaseLetters.contains(char) {
|
||||
if let randomLetter = uppercaseLetters.randomElement() {
|
||||
transformedText.append(randomLetter)
|
||||
}
|
||||
} else {
|
||||
transformedText.append(char)
|
||||
}
|
||||
}
|
||||
return transformedText
|
||||
}
|
||||
}
|
||||
@@ -298,7 +298,7 @@ struct ChatEventView: View {
|
||||
}
|
||||
.swipeSpacing(-20)
|
||||
.swipeActionsStyle(.mask)
|
||||
.swipeMinimumDistance(20)
|
||||
.swipeMinimumDistance(40)
|
||||
.swipeDragGesturePriority(.normal)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,47 +13,24 @@ struct ChatroomThreadView: View {
|
||||
@State var once: Bool = false
|
||||
let damus: DamusState
|
||||
@ObservedObject var thread: ThreadModel
|
||||
@State var selected_note_id: NoteId? = nil
|
||||
@State var highlighted_note_id: NoteId? = nil
|
||||
@State var user_just_posted_flag: Bool = false
|
||||
@Namespace private var animation
|
||||
|
||||
@State var parent_events: [NostrEvent] = []
|
||||
@State var sorted_child_events: [NostrEvent] = []
|
||||
|
||||
func compute_events(selected_event: NostrEvent? = nil) {
|
||||
let selected_event = selected_event ?? thread.event
|
||||
self.parent_events = damus.events.parent_events(event: selected_event, keypair: damus.keypair)
|
||||
let all_recursive_child_events = self.recursive_child_events(event: selected_event)
|
||||
self.sorted_child_events = all_recursive_child_events.filter({
|
||||
should_show_event(event: $0, damus_state: damus) // Hide muted events from chatroom conversation
|
||||
}).sorted(by: { a, b in
|
||||
return a.created_at < b.created_at
|
||||
})
|
||||
}
|
||||
|
||||
func recursive_child_events(event: NdbNote) -> [NdbNote] {
|
||||
let immediate_children = damus.events.child_events(event: event)
|
||||
var indirect_children: [NdbNote] = []
|
||||
for immediate_child in immediate_children {
|
||||
indirect_children.append(contentsOf: self.recursive_child_events(event: immediate_child))
|
||||
}
|
||||
return immediate_children + indirect_children
|
||||
}
|
||||
|
||||
func go_to_event(scroller: ScrollViewProxy, note_id: NoteId) {
|
||||
scroll_to_event(scroller: scroller, id: note_id, delay: 0, animate: true, anchor: .top)
|
||||
selected_note_id = note_id
|
||||
highlighted_note_id = note_id
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
|
||||
withAnimation {
|
||||
selected_note_id = nil
|
||||
highlighted_note_id = nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func set_active_event(scroller: ScrollViewProxy, ev: NdbNote) {
|
||||
withAnimation {
|
||||
self.compute_events(selected_event: ev)
|
||||
thread.set_active_event(ev, keypair: self.damus.keypair)
|
||||
self.thread.select(event: ev)
|
||||
self.go_to_event(scroller: scroller, note_id: ev.id)
|
||||
}
|
||||
}
|
||||
@@ -63,7 +40,7 @@ struct ChatroomThreadView: View {
|
||||
ScrollView(.vertical) {
|
||||
LazyVStack(alignment: .leading, spacing: 8) {
|
||||
// MARK: - Parents events view
|
||||
ForEach(parent_events, id: \.id) { parent_event in
|
||||
ForEach(thread.parent_events, id: \.id) { parent_event in
|
||||
EventMutingContainerView(damus_state: damus, event: parent_event) {
|
||||
EventView(damus: damus, event: parent_event)
|
||||
.matchedGeometryEffect(id: parent_event.id.hex(), in: animation, anchor: .center)
|
||||
@@ -93,7 +70,7 @@ struct ChatroomThreadView: View {
|
||||
// MARK: - Actual event view
|
||||
EventMutingContainerView(
|
||||
damus_state: damus,
|
||||
event: self.thread.event,
|
||||
event: self.thread.selected_event,
|
||||
muteBox: { event_shown, muted_reason in
|
||||
AnyView(
|
||||
EventMutedBoxView(shown: event_shown, reason: muted_reason)
|
||||
@@ -101,19 +78,19 @@ struct ChatroomThreadView: View {
|
||||
)
|
||||
}
|
||||
) {
|
||||
SelectedEventView(damus: damus, event: self.thread.event, size: .selected)
|
||||
.matchedGeometryEffect(id: self.thread.event.id.hex(), in: animation, anchor: .center)
|
||||
SelectedEventView(damus: damus, event: self.thread.selected_event, size: .selected)
|
||||
.matchedGeometryEffect(id: self.thread.selected_event.id.hex(), in: animation, anchor: .center)
|
||||
}
|
||||
.id(self.thread.event.id)
|
||||
.id(self.thread.selected_event.id)
|
||||
|
||||
|
||||
// MARK: - Children view
|
||||
let events = sorted_child_events
|
||||
let events = thread.sorted_child_events
|
||||
let count = events.count
|
||||
SwipeViewGroup {
|
||||
ForEach(Array(zip(events, events.indices)), id: \.0.id) { (ev, ind) in
|
||||
ChatEventView(event: events[ind],
|
||||
selected_event: self.thread.event,
|
||||
selected_event: self.thread.selected_event,
|
||||
prev_ev: ind > 0 ? events[ind-1] : nil,
|
||||
next_ev: ind == count-1 ? nil : events[ind+1],
|
||||
damus_state: damus,
|
||||
@@ -124,7 +101,7 @@ struct ChatroomThreadView: View {
|
||||
focus_event: {
|
||||
self.set_active_event(scroller: scroller, ev: ev)
|
||||
},
|
||||
highlight_bubble: selected_note_id == ev.id,
|
||||
highlight_bubble: highlighted_note_id == ev.id,
|
||||
bar: make_actionbar_model(ev: ev.id, damus: damus)
|
||||
)
|
||||
.padding(.horizontal)
|
||||
@@ -148,16 +125,14 @@ struct ChatroomThreadView: View {
|
||||
}
|
||||
})
|
||||
.onReceive(thread.objectWillChange) {
|
||||
self.compute_events()
|
||||
if let last_event = thread.events().last, last_event.pubkey == damus.pubkey, user_just_posted_flag {
|
||||
if let last_event = thread.events.last, last_event.pubkey == damus.pubkey, user_just_posted_flag {
|
||||
self.go_to_event(scroller: scroller, note_id: last_event.id)
|
||||
user_just_posted_flag = false
|
||||
}
|
||||
}
|
||||
.onAppear() {
|
||||
thread.subscribe()
|
||||
self.compute_events()
|
||||
scroll_to_event(scroller: scroller, id: thread.event.id, delay: 0.1, animate: false)
|
||||
scroll_to_event(scroller: scroller, id: thread.selected_event.id, delay: 0.1, animate: false)
|
||||
}
|
||||
.onDisappear() {
|
||||
thread.unsubscribe()
|
||||
@@ -193,6 +168,7 @@ struct ChatroomView_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func scroll_after_load(thread: ThreadModel, proxy: ScrollViewProxy) {
|
||||
scroll_to_event(scroller: proxy, id: thread.event.id, delay: 0.1, animate: false)
|
||||
scroll_to_event(scroller: proxy, id: thread.selected_event.id, delay: 0.1, animate: false)
|
||||
}
|
||||
|
||||
119
damus/Views/ErrorHandling/ErrorView.swift
Normal file
119
damus/Views/ErrorHandling/ErrorView.swift
Normal file
@@ -0,0 +1,119 @@
|
||||
//
|
||||
// ErrorView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D'Aquino on 2025-01-08.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// A generic user-presentable error view
|
||||
///
|
||||
/// Use this to handle and display errors to the user when it does not make sense to create a custom error view.
|
||||
/// This includes good error handling UX practices, such as:
|
||||
/// - Clear description
|
||||
/// - Actionable advice for the user on what to do next.
|
||||
/// - One-click support contact options
|
||||
struct ErrorView: View {
|
||||
let damus_state: DamusState?
|
||||
let error: UserPresentableError
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 6) {
|
||||
Image(systemName: "exclamationmark.circle")
|
||||
.resizable()
|
||||
.frame(width: 30, height: 30)
|
||||
.foregroundStyle(.red)
|
||||
.accessibilityHidden(true)
|
||||
Text("Oops!", comment: "Heading for an error screen")
|
||||
.font(.title)
|
||||
.bold()
|
||||
.padding(.bottom, 10)
|
||||
.accessibilityHeading(.h1)
|
||||
Text(error.user_visible_description)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(spacing: 5) {
|
||||
Image(systemName: "sparkles")
|
||||
.accessibilityHidden(true)
|
||||
Text("Advice", comment: "Heading for some advice text to help the user with an error")
|
||||
.font(.headline)
|
||||
.accessibilityHeading(.h3)
|
||||
}
|
||||
Text(error.tip)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.secondary.opacity(0.2))
|
||||
.cornerRadius(10)
|
||||
.padding(.vertical, 30)
|
||||
|
||||
Spacer()
|
||||
|
||||
if let damus_state, damus_state.is_privkey_user {
|
||||
Button(action: {
|
||||
damus_state.nav.push(route: .DMChat(dms: .init(our_pubkey: damus_state.keypair.pubkey, pubkey: Constants.SUPPORT_PUBKEY)))
|
||||
dismiss()
|
||||
}, label: {
|
||||
Text("Contact support via DMs", comment: "Button label to contact support from an error screen")
|
||||
})
|
||||
.padding(.vertical, 20)
|
||||
}
|
||||
Text("Contact support via email at [support@damus.io](mailto:support@damus.io)", comment: "Text to contact support via email")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(20)
|
||||
.padding(.top, 20)
|
||||
}
|
||||
|
||||
/// An error that is displayed to the user, and can be sent to the Developers as well.
|
||||
struct UserPresentableError {
|
||||
/// The description of the error to be shown to the user
|
||||
///
|
||||
/// **Requirements:**
|
||||
/// - This should not be technical. It should use accessible language
|
||||
/// - Should be localized
|
||||
/// - It should try to explain the user what happened, and — if possible — why.
|
||||
let user_visible_description: String
|
||||
|
||||
/// Helpful tip/advice to the user, to help them overcome the error
|
||||
///
|
||||
/// **Requirements:**
|
||||
/// - Should provide actionable advice to the user
|
||||
/// - This should not be overly technical
|
||||
/// - Should be localized
|
||||
/// - Should NOT include support contact (The view that will display this will already include support contact options)
|
||||
///
|
||||
/// **Implementation notes:**
|
||||
/// - This is NOT an optional value, because part of good UX is making sure error messages are actionable, which is something that is often forgotten. It's not uncommon for error messages to be written in vague, technical, and/or unactionable terms, but this is when the user needs help the most. And so this field is made mandatory to force developers to write actionable content to the user
|
||||
let tip: String
|
||||
|
||||
/// Technical information about the error, which will be sendable to developers
|
||||
///
|
||||
/// Note: This is still unutilized, but this will be used in the future.
|
||||
///
|
||||
/// **Requirements**
|
||||
/// - Should never include any sensitive info
|
||||
/// - Should be in English. The developers are the main audience.
|
||||
/// - Should include helping info, such as context in which the error happens.
|
||||
/// - Should be technical
|
||||
let technical_info: String?
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#Preview {
|
||||
ErrorView(
|
||||
damus_state: test_damus_state,
|
||||
error: .init(
|
||||
user_visible_description: "We are still too early",
|
||||
tip: "Stay humble, keep building, stack sats",
|
||||
technical_info: nil
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -57,6 +57,10 @@ struct EventView: View {
|
||||
|
||||
// blame the porn bots for this code
|
||||
func should_blur_images(settings: UserSettingsStore, contacts: Contacts, ev: NostrEvent, our_pubkey: Pubkey, booster_pubkey: Pubkey? = nil) -> Bool {
|
||||
if settings.undistractMode {
|
||||
return true
|
||||
}
|
||||
|
||||
if !settings.blur_images {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ struct EventBody: View {
|
||||
HighlightBodyView(state: damus_state, ev: event, options: options)
|
||||
.onTapGesture {
|
||||
if let highlighted_note = event.highlighted_note_id().flatMap({ damus_state.events.lookup($0) }) {
|
||||
let thread = ThreadModel(event: highlighted_note, damus_state: damus_state, highlight: event.content)
|
||||
let thread = ThreadModel(event: highlighted_note, damus_state: damus_state)
|
||||
damus_state.nav.push(route: Route.Thread(thread: thread))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ struct MenuItems: View {
|
||||
MuteDurationMenu { duration in
|
||||
notify(.mute(.user(target_pubkey, duration?.date_from_now)))
|
||||
} label: {
|
||||
Label(NSLocalizedString("Mute user", comment: "Context menu option for muting users."), image: "mute")
|
||||
Label(NSLocalizedString("Mute/Block user", comment: "Context menu option for muting/blocking users."), image: "mute")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
275
damus/Views/LoadableNostrEventView.swift
Normal file
275
damus/Views/LoadableNostrEventView.swift
Normal file
@@ -0,0 +1,275 @@
|
||||
//
|
||||
// LoadableNostrEventView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D'Aquino on 2025-01-08.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
||||
/// A view model for `LoadableNostrEventView`
|
||||
///
|
||||
/// This takes a nostr event reference, automatically tries to load it, and updates itself to reflect its current state
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - This is on the main actor because `ObservableObjects` with `Published` properties should be on the main actor for thread-safety.
|
||||
///
|
||||
@MainActor
|
||||
class LoadableNostrEventViewModel: ObservableObject {
|
||||
let damus_state: DamusState
|
||||
let note_reference: NoteReference
|
||||
@Published var state: ThreadModelLoadingState = .loading
|
||||
/// The time period after which it will give up loading the view.
|
||||
/// Written in nanoseconds
|
||||
let TIMEOUT: UInt64 = 10 * 1_000_000_000 // 10 seconds
|
||||
|
||||
init(damus_state: DamusState, note_reference: NoteReference) {
|
||||
self.damus_state = damus_state
|
||||
self.note_reference = note_reference
|
||||
Task { await self.load() }
|
||||
}
|
||||
|
||||
func load() async {
|
||||
// Start the loading process in a separate task to manage the timeout independently.
|
||||
let loadTask = Task { @MainActor in
|
||||
self.state = await executeLoadingLogic(note_reference: self.note_reference)
|
||||
}
|
||||
|
||||
// Setup a timer to cancel the load after the timeout period
|
||||
let timeoutTask = Task { @MainActor in
|
||||
try await Task.sleep(nanoseconds: TIMEOUT)
|
||||
loadTask.cancel() // This sends a cancellation signal to the load task.
|
||||
self.state = .not_found
|
||||
}
|
||||
|
||||
await loadTask.value
|
||||
timeoutTask.cancel() // Cancel the timeout task if loading finishes earlier.
|
||||
}
|
||||
|
||||
/// Asynchronously find an event from NostrDB or from the network (if not available on NostrDB)
|
||||
private func loadEvent(noteId: NoteId) async -> NostrEvent? {
|
||||
let res = await find_event(state: damus_state, query: .event(evid: noteId))
|
||||
guard let res, case .event(let ev) = res else { return nil }
|
||||
return ev
|
||||
}
|
||||
|
||||
/// Gets the note reference and tries to load it, outputting a new state for this view model.
|
||||
private func executeLoadingLogic(note_reference: NoteReference) async -> ThreadModelLoadingState {
|
||||
switch note_reference {
|
||||
case .note_id(let note_id):
|
||||
guard let ev = await self.loadEvent(noteId: note_id) else { return .not_found }
|
||||
guard let known_kind = ev.known_kind else { return .unknown_or_unsupported_kind }
|
||||
switch known_kind {
|
||||
case .text, .highlight:
|
||||
return .loaded(route: Route.Thread(thread: ThreadModel(event: ev, damus_state: damus_state)))
|
||||
case .dm:
|
||||
let dm_model = damus_state.dms.lookup_or_create(ev.pubkey)
|
||||
return .loaded(route: Route.DMChat(dms: dm_model))
|
||||
case .like:
|
||||
// Load the event that this reaction refers to.
|
||||
guard let first_referenced_note_id = ev.referenced_ids.first else { return .not_found }
|
||||
return await self.executeLoadingLogic(note_reference: .note_id(first_referenced_note_id))
|
||||
case .zap, .zap_request:
|
||||
guard let zap = await get_zap(from: ev, state: damus_state) else { return .not_found }
|
||||
return .loaded(route: Route.Zaps(target: zap.target))
|
||||
case .contacts, .metadata, .delete, .boost, .chat, .mute_list, .list_deprecated, .draft, .longform, .zap, .zap_request, .nwc_request, .nwc_response, .http_auth, .status:
|
||||
return .unknown_or_unsupported_kind
|
||||
}
|
||||
case .naddr(let naddr):
|
||||
guard let event = await naddrLookup(damus_state: damus_state, naddr: naddr) else { return .not_found }
|
||||
return .loaded(route: Route.Thread(thread: ThreadModel(event: event, damus_state: damus_state)))
|
||||
}
|
||||
}
|
||||
|
||||
enum ThreadModelLoadingState {
|
||||
case loading
|
||||
case loaded(route: Route)
|
||||
case not_found
|
||||
case unknown_or_unsupported_kind
|
||||
}
|
||||
|
||||
enum NoteReference: Hashable {
|
||||
case note_id(NoteId)
|
||||
case naddr(NAddr)
|
||||
}
|
||||
}
|
||||
|
||||
/// A view for a Nostr event that has not been loaded yet.
|
||||
/// This takes a Nostr event reference and loads it, while providing nice loading UX and graceful error handling.
|
||||
struct LoadableNostrEventView: View {
|
||||
let state: DamusState
|
||||
@StateObject var loadableModel: LoadableNostrEventViewModel
|
||||
var loading: Bool {
|
||||
switch loadableModel.state {
|
||||
case .loading:
|
||||
return true
|
||||
case .loaded, .not_found, .unknown_or_unsupported_kind:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
init(state: DamusState, note_reference: LoadableNostrEventViewModel.NoteReference) {
|
||||
self.state = state
|
||||
self._loadableModel = StateObject.init(wrappedValue: LoadableNostrEventViewModel(damus_state: state, note_reference: note_reference))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
switch self.loadableModel.state {
|
||||
case .loading:
|
||||
ScrollView(.vertical) {
|
||||
self.skeleton
|
||||
.redacted(reason: loading ? .placeholder : [])
|
||||
.shimmer(loading)
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(NSLocalizedString("Loading thread", comment: "Accessibility label for the thread view when it is loading"))
|
||||
}
|
||||
case .loaded(route: let route):
|
||||
route.view(navigationCoordinator: state.nav, damusState: state)
|
||||
case .not_found:
|
||||
self.not_found
|
||||
case .unknown_or_unsupported_kind:
|
||||
self.unknown_or_unsupported_kind
|
||||
}
|
||||
}
|
||||
|
||||
var not_found: some View {
|
||||
SomethingWrong(
|
||||
imageSystemName: "questionmark.app",
|
||||
heading: NSLocalizedString("Note not found", comment: "Heading for the thread view in a not found error state."),
|
||||
description: NSLocalizedString("We were unable to find the note you were looking for.", comment: "Text for the thread view when it is unable to find the note the user is looking for"),
|
||||
advice: NSLocalizedString("Try checking the link again, your internet connection, or contact the person who provided you the link for help.", comment: "Tips on what to do if a note cannot be found.")
|
||||
)
|
||||
}
|
||||
|
||||
var unknown_or_unsupported_kind: some View {
|
||||
SomethingWrong(
|
||||
imageSystemName: "questionmark.app",
|
||||
heading: NSLocalizedString("Can’t display note", comment: "User-visible heading for an error message indicating a note has an unknown kind or is unsupported for viewing."),
|
||||
description: NSLocalizedString("We do not yet support viewing this type of content.", comment: "User-visible description of an error indicating a note has an unknown kind or is unsupported for viewing."),
|
||||
advice: NSLocalizedString("Please try opening this content on another Nostr app that supports this type of content.", comment: "User-visible advice on what to do if they see the error indicating a note has an unknown kind or is unsupported for viewing.")
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: Skeleton views
|
||||
// Implementation notes
|
||||
// - No localization is needed because the text will be redacted
|
||||
// - No accessibility label is needed because these will be summarized into a single accessibility label at the top-level view. See `body` in this struct
|
||||
|
||||
var skeleton: some View {
|
||||
VStack(alignment: .leading, spacing: 40) {
|
||||
Self.skeleton_selected_event
|
||||
Self.skeleton_chat_event(message: "Nice! Have you tried Damus?", right: false)
|
||||
Self.skeleton_chat_event(message: "Yes, it's awesome.", right: true)
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
static func skeleton_chat_event(message: String, right: Bool) -> some View {
|
||||
HStack(alignment: .center) {
|
||||
if !right {
|
||||
self.skeleton_chat_user_avatar
|
||||
}
|
||||
else {
|
||||
Spacer()
|
||||
}
|
||||
ChatBubble(
|
||||
direction: right ? .right : .left,
|
||||
stroke_content: Color.accentColor.opacity(0),
|
||||
stroke_style: .init(lineWidth: 4),
|
||||
background_style: Color.secondary.opacity(0.5),
|
||||
content: {
|
||||
Text(verbatim: message)
|
||||
.padding()
|
||||
}
|
||||
)
|
||||
if right {
|
||||
self.skeleton_chat_user_avatar
|
||||
}
|
||||
else {
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static var skeleton_selected_event: some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack {
|
||||
Circle()
|
||||
.frame(width: 50, height: 50)
|
||||
.foregroundStyle(.secondary.opacity(0.5))
|
||||
Text(verbatim: "Satoshi Nakamoto")
|
||||
.bold()
|
||||
}
|
||||
Text(verbatim: "Nostr is the super app. Because it’s actually an ecosystem of apps, all of which make each other better. People haven’t grasped that yet. They will when it’s more accessible and onboarding is more straightforward and intuitive.")
|
||||
HStack {
|
||||
self.skeleton_action_item
|
||||
Spacer()
|
||||
self.skeleton_action_item
|
||||
Spacer()
|
||||
self.skeleton_action_item
|
||||
Spacer()
|
||||
self.skeleton_action_item
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static var skeleton_chat_user_avatar: some View {
|
||||
Circle()
|
||||
.fill(.secondary.opacity(0.5))
|
||||
.frame(width: 35, height: 35)
|
||||
.padding(.bottom, -21)
|
||||
}
|
||||
|
||||
static var skeleton_action_item: some View {
|
||||
Circle()
|
||||
.fill(Color.secondary.opacity(0.5))
|
||||
.frame(width: 25, height: 25)
|
||||
}
|
||||
}
|
||||
|
||||
extension LoadableNostrEventView {
|
||||
struct SomethingWrong: View {
|
||||
let imageSystemName: String
|
||||
let heading: String
|
||||
let description: String
|
||||
let advice: String
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 6) {
|
||||
Image(systemName: imageSystemName)
|
||||
.resizable()
|
||||
.frame(width: 30, height: 30)
|
||||
.accessibilityHidden(true)
|
||||
Text(heading)
|
||||
.font(.title)
|
||||
.bold()
|
||||
.padding(.bottom, 10)
|
||||
Text(description)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(spacing: 5) {
|
||||
Image(systemName: "sparkles")
|
||||
.accessibilityHidden(true)
|
||||
Text("Advice", comment: "Heading for some advice text to help the user with an error")
|
||||
.font(.headline)
|
||||
}
|
||||
Text(advice)
|
||||
}
|
||||
.padding()
|
||||
.background(Color.secondary.opacity(0.2))
|
||||
.cornerRadius(10)
|
||||
.padding(.vertical, 30)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Loadable") {
|
||||
LoadableNostrEventView(state: test_damus_state, note_reference: .note_id(test_thread_note_1.id))
|
||||
}
|
||||
@@ -40,6 +40,9 @@ struct NoteContentView: View {
|
||||
@ObservedObject var settings: UserSettingsStore
|
||||
|
||||
var note_artifacts: NoteArtifacts {
|
||||
if damus_state.settings.undistractMode {
|
||||
return .separated(.just_content(Undistractor.makeGibberish(text: event.get_content(damus_state.keypair))))
|
||||
}
|
||||
return self.artifacts_model.state.artifacts ?? .separated(.just_content(event.get_content(damus_state.keypair)))
|
||||
}
|
||||
|
||||
|
||||
@@ -51,21 +51,32 @@ enum PostAction {
|
||||
}
|
||||
|
||||
struct PostView: View {
|
||||
|
||||
@State var post: NSMutableAttributedString = NSMutableAttributedString()
|
||||
@State var uploadedMedias: [UploadedMedia] = []
|
||||
@State var references: [RefId] = []
|
||||
/// Pubkeys that should be filtered out from the references
|
||||
///
|
||||
/// For example, when replying to an event, the user can select which pubkey mentions they want to keep, and which ones to remove.
|
||||
@State var filtered_pubkeys: Set<Pubkey> = []
|
||||
|
||||
@FocusState var focus: Bool
|
||||
@State var attach_media: Bool = false
|
||||
@State var attach_camera: Bool = false
|
||||
@State var error: String? = nil
|
||||
@State var uploadedMedias: [UploadedMedia] = []
|
||||
@State var image_upload_confirm: Bool = false
|
||||
@State var imagePastedFromPasteboard: PreUploadedMedia? = nil
|
||||
@State var imageUploadConfirmPasteboard: Bool = false
|
||||
@State var references: [RefId] = []
|
||||
@State var imageUploadConfirmDamusShare: Bool = false
|
||||
@State var filtered_pubkeys: Set<Pubkey> = []
|
||||
@State var focusWordAttributes: (String?, NSRange?) = (nil, nil)
|
||||
@State var newCursorIndex: Int?
|
||||
@State var textHeight: CGFloat? = nil
|
||||
/// Manages the auto-save logic for drafts.
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - This intentionally does _not_ use `@ObservedObject` or `@StateObject` because observing changes causes unwanted automatic scrolling to the text cursor on each save state update.
|
||||
var autoSaveModel: AutoSaveIndicatorView.AutoSaveViewModel
|
||||
|
||||
@State var preUploadedMedia: [PreUploadedMedia] = []
|
||||
|
||||
@@ -93,6 +104,7 @@ struct PostView: View {
|
||||
self.prompt_view = prompt_view
|
||||
self.placeholder_messages = placeholder_messages ?? [POST_PLACEHOLDER]
|
||||
self.initial_text_suffix = initial_text_suffix
|
||||
self.autoSaveModel = AutoSaveIndicatorView.AutoSaveViewModel(save: { damus_state.drafts.save(damus_state: damus_state) })
|
||||
}
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@@ -109,24 +121,7 @@ struct PostView: View {
|
||||
}
|
||||
|
||||
func send_post() {
|
||||
// don't add duplicate pubkeys but retain order
|
||||
var pkset = Set<Pubkey>()
|
||||
|
||||
// we only want pubkeys really
|
||||
let pks = references.reduce(into: Array<Pubkey>()) { acc, ref in
|
||||
guard case .pubkey(let pk) = ref else {
|
||||
return
|
||||
}
|
||||
|
||||
if pkset.contains(pk) || filtered_pubkeys.contains(pk) {
|
||||
return
|
||||
}
|
||||
|
||||
pkset.insert(pk)
|
||||
acc.append(pk)
|
||||
}
|
||||
|
||||
let new_post = build_post(state: damus_state, post: self.post, action: action, uploadedMedias: uploadedMedias, pubkeys: pks)
|
||||
let new_post = build_post(state: self.damus_state, post: self.post, action: action, uploadedMedias: uploadedMedias, references: self.references, filtered_pubkeys: filtered_pubkeys)
|
||||
|
||||
notify(.post(.post(new_post)))
|
||||
|
||||
@@ -186,6 +181,8 @@ struct PostView: View {
|
||||
HStack(alignment: .center, spacing: 15) {
|
||||
ImageButton
|
||||
CameraButton
|
||||
Spacer()
|
||||
AutoSaveIndicatorView(saveViewModel: self.autoSaveModel)
|
||||
}
|
||||
.disabled(uploading_disabled)
|
||||
}
|
||||
@@ -222,40 +219,49 @@ struct PostView: View {
|
||||
func clear_draft() {
|
||||
switch action {
|
||||
case .replying_to(let replying_to):
|
||||
damus_state.drafts.replies.removeValue(forKey: replying_to)
|
||||
damus_state.drafts.replies.removeValue(forKey: replying_to.id)
|
||||
case .quoting(let quoting):
|
||||
damus_state.drafts.quotes.removeValue(forKey: quoting)
|
||||
damus_state.drafts.quotes.removeValue(forKey: quoting.id)
|
||||
case .posting:
|
||||
damus_state.drafts.post = nil
|
||||
case .highlighting(let draft):
|
||||
damus_state.drafts.highlights.removeValue(forKey: draft.source)
|
||||
damus_state.drafts.highlights.removeValue(forKey: draft)
|
||||
case .sharing(_):
|
||||
damus_state.drafts.post = nil
|
||||
}
|
||||
|
||||
damus_state.drafts.save(damus_state: damus_state)
|
||||
}
|
||||
|
||||
func load_draft() -> Bool {
|
||||
guard let draft = load_draft_for_post(drafts: self.damus_state.drafts, action: self.action) else {
|
||||
self.post = NSMutableAttributedString("")
|
||||
self.uploadedMedias = []
|
||||
|
||||
self.autoSaveModel.markNothingToSave() // We should not save empty drafts.
|
||||
return false
|
||||
}
|
||||
|
||||
self.uploadedMedias = draft.media
|
||||
self.post = draft.content
|
||||
self.autoSaveModel.markSaved() // The draft we just loaded is saved to memory. Mark it as such.
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
/// Use this to signal that the post contents have changed. This will do two things:
|
||||
///
|
||||
/// 1. Save the new contents into our in-memory drafts
|
||||
/// 2. Signal that we need to save drafts persistently, which will happen after a certain wait period
|
||||
func post_changed(post: NSMutableAttributedString, media: [UploadedMedia]) {
|
||||
if let draft = load_draft_for_post(drafts: damus_state.drafts, action: action) {
|
||||
draft.content = post
|
||||
draft.media = media
|
||||
draft.media = uploadedMedias
|
||||
draft.references = references
|
||||
draft.filtered_pubkeys = filtered_pubkeys
|
||||
} else {
|
||||
let artifacts = DraftArtifacts(content: post, media: media)
|
||||
let artifacts = DraftArtifacts(content: post, media: uploadedMedias, references: references, id: UUID().uuidString)
|
||||
set_draft_for_post(drafts: damus_state.drafts, action: action, artifacts: artifacts)
|
||||
}
|
||||
self.autoSaveModel.needsSaving()
|
||||
}
|
||||
|
||||
var TextEntry: some View {
|
||||
@@ -356,7 +362,7 @@ struct PostView: View {
|
||||
}
|
||||
let blurhash = await blurhash
|
||||
let meta = blurhash.map { bh in calculate_image_metadata(url: url, img: img, blurhash: bh) }
|
||||
let uploadedMedia = UploadedMedia(localURL: media.localURL, uploadedURL: url, representingImage: img, metadata: meta)
|
||||
let uploadedMedia = UploadedMedia(localURL: media.localURL, uploadedURL: url, metadata: meta)
|
||||
uploadedMedias.append(uploadedMedia)
|
||||
return true
|
||||
|
||||
@@ -628,7 +634,7 @@ struct PVImageCarouselView: View {
|
||||
.cornerRadius(10)
|
||||
.padding()
|
||||
.contextMenu { contextMenuContent(for: media[index]) }
|
||||
} else if is_animated_image(url: media[index].uploadedURL) {
|
||||
} else {
|
||||
KFAnimatedImage(media[index].uploadedURL)
|
||||
.imageContext(.note, disable_animation: false)
|
||||
.configure { view in
|
||||
@@ -638,14 +644,6 @@ struct PVImageCarouselView: View {
|
||||
.cornerRadius(10)
|
||||
.padding()
|
||||
.contextMenu { contextMenuContent(for: media[index]) }
|
||||
} else {
|
||||
Image(uiImage: media[index].representingImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: media.count == 1 ? deviceWidth * 0.8 : 250, height: media.count == 1 ? 400 : 250)
|
||||
.cornerRadius(10)
|
||||
.padding()
|
||||
.contextMenu { contextMenuContent(for: media[index]) }
|
||||
}
|
||||
|
||||
VStack { // Set spacing to 0 to remove the gap between items
|
||||
@@ -732,7 +730,6 @@ fileprivate func getImage(media: MediaUpload) -> UIImage {
|
||||
struct UploadedMedia: Equatable {
|
||||
let localURL: URL
|
||||
let uploadedURL: URL
|
||||
let representingImage: UIImage
|
||||
let metadata: ImageMetadata?
|
||||
}
|
||||
|
||||
@@ -740,13 +737,13 @@ struct UploadedMedia: Equatable {
|
||||
func set_draft_for_post(drafts: Drafts, action: PostAction, artifacts: DraftArtifacts) {
|
||||
switch action {
|
||||
case .replying_to(let ev):
|
||||
drafts.replies[ev] = artifacts
|
||||
drafts.replies[ev.id] = artifacts
|
||||
case .quoting(let ev):
|
||||
drafts.quotes[ev] = artifacts
|
||||
drafts.quotes[ev.id] = artifacts
|
||||
case .posting:
|
||||
drafts.post = artifacts
|
||||
case .highlighting(let draft):
|
||||
drafts.highlights[draft.source] = artifacts
|
||||
drafts.highlights[draft] = artifacts
|
||||
case .sharing(_):
|
||||
drafts.post = artifacts
|
||||
}
|
||||
@@ -755,13 +752,21 @@ func set_draft_for_post(drafts: Drafts, action: PostAction, artifacts: DraftArti
|
||||
func load_draft_for_post(drafts: Drafts, action: PostAction) -> DraftArtifacts? {
|
||||
switch action {
|
||||
case .replying_to(let ev):
|
||||
return drafts.replies[ev]
|
||||
return drafts.replies[ev.id]
|
||||
case .quoting(let ev):
|
||||
return drafts.quotes[ev]
|
||||
return drafts.quotes[ev.id]
|
||||
case .posting:
|
||||
return drafts.post
|
||||
case .highlighting(let draft):
|
||||
return drafts.highlights[draft.source]
|
||||
case .highlighting(let highlight):
|
||||
if let exact_match = drafts.highlights[highlight] {
|
||||
return exact_match // Always prefer to return the draft for that exact same highlight
|
||||
}
|
||||
// If there are no exact matches to the highlight, try to load a draft for the same highlight source
|
||||
// We do this to improve UX, because we don't want to leave the post view blank if they only selected a slightly different piece of text from before.
|
||||
var other_matches = drafts.highlights
|
||||
.filter { $0.key.source == highlight.source }
|
||||
// It's not an exact match, so there is no way of telling which one is the preferred draft. So just load the first one we found.
|
||||
return other_matches.first?.value
|
||||
case .sharing(_):
|
||||
return drafts.post
|
||||
}
|
||||
@@ -788,7 +793,53 @@ func nip10_reply_tags(replying_to: NostrEvent, keypair: Keypair) -> [[String]] {
|
||||
return tags
|
||||
}
|
||||
|
||||
func build_post(state: DamusState, post: NSMutableAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], pubkeys: [Pubkey]) -> NostrPost {
|
||||
func build_post(state: DamusState, action: PostAction, draft: DraftArtifacts) -> NostrPost {
|
||||
return build_post(
|
||||
state: state,
|
||||
post: draft.content,
|
||||
action: action,
|
||||
uploadedMedias: draft.media,
|
||||
references: draft.references,
|
||||
filtered_pubkeys: draft.filtered_pubkeys
|
||||
)
|
||||
}
|
||||
|
||||
func build_post(state: DamusState, post: NSAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], references: [RefId], filtered_pubkeys: Set<Pubkey>) -> NostrPost {
|
||||
// don't add duplicate pubkeys but retain order
|
||||
var pkset = Set<Pubkey>()
|
||||
|
||||
// we only want pubkeys really
|
||||
let pks = references.reduce(into: Array<Pubkey>()) { acc, ref in
|
||||
guard case .pubkey(let pk) = ref else {
|
||||
return
|
||||
}
|
||||
|
||||
if pkset.contains(pk) || filtered_pubkeys.contains(pk) {
|
||||
return
|
||||
}
|
||||
|
||||
pkset.insert(pk)
|
||||
acc.append(pk)
|
||||
}
|
||||
|
||||
return build_post(state: state, post: post, action: action, uploadedMedias: uploadedMedias, pubkeys: pks)
|
||||
}
|
||||
|
||||
/// This builds a Nostr post from draft data from `PostView` or other draft-related classes
|
||||
///
|
||||
/// ## Implementation notes
|
||||
///
|
||||
/// - This function _likely_ causes no side-effects, and _should not_ cause side-effects to any of the inputs.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - state: The damus state, needed to fetch more Nostr data to form this event
|
||||
/// - post: The text content from `PostView`.
|
||||
/// - action: The intended action of the post (highlighting? replying?)
|
||||
/// - uploadedMedias: The medias attached to this post
|
||||
/// - pubkeys: The referenced pubkeys
|
||||
/// - Returns: A NostrPost, which can then be signed into an event.
|
||||
func build_post(state: DamusState, post: NSAttributedString, action: PostAction, uploadedMedias: [UploadedMedia], pubkeys: [Pubkey]) -> NostrPost {
|
||||
let post = NSMutableAttributedString(attributedString: post)
|
||||
post.enumerateAttributes(in: NSRange(location: 0, length: post.length), options: []) { attributes, range, stop in
|
||||
if let link = attributes[.link] as? String {
|
||||
let nextCharIndex = range.upperBound
|
||||
@@ -876,3 +927,11 @@ func isSupportedVideo(url: URL?) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isSupportedImage(url: URL) -> Bool {
|
||||
let fileExtension = url.pathExtension.lowercased()
|
||||
// It would be better to pull this programmatically from Apple's APIs, but there seems to be no such call
|
||||
let supportedTypes = ["jpg", "png", "gif"]
|
||||
return supportedTypes.contains(fileExtension)
|
||||
}
|
||||
|
||||
|
||||
136
damus/Views/Posting/AutoSaveIndicatorView.swift
Normal file
136
damus/Views/Posting/AutoSaveIndicatorView.swift
Normal file
@@ -0,0 +1,136 @@
|
||||
//
|
||||
// AutoSaveIndicatorView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-02-12.
|
||||
//
|
||||
import SwiftUI
|
||||
|
||||
/// A small indicator view to indicate whether an item has been saved or not.
|
||||
///
|
||||
/// This view uses and observes an `AutoSaveViewModel`.
|
||||
struct AutoSaveIndicatorView: View {
|
||||
@ObservedObject var saveViewModel: AutoSaveViewModel
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
switch saveViewModel.savedState {
|
||||
case .needsSaving, .nothingToSave:
|
||||
EmptyView()
|
||||
.accessibilityHidden(true) // Probably no need to show this to users with visual impairment, might be too noisy.
|
||||
case .saving:
|
||||
ProgressView()
|
||||
.accessibilityHidden(true) // Probably no need to show this to users with visual impairment, might be too noisy.
|
||||
case .saved:
|
||||
Image(systemName: "checkmark")
|
||||
.accessibilityHidden(true)
|
||||
Text("Saved", comment: "Small label indicating that the user's draft has been saved to storage.")
|
||||
.accessibilityLabel(NSLocalizedString("Your draft has been saved to storage.", comment: "Accessibility label indicating that a user's post draft has been saved, meant to be read by screen reading technology."))
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
.padding(6)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension AutoSaveIndicatorView {
|
||||
/// A simple data structure to model the saving state of an item that can be auto-saved every few seconds.
|
||||
enum SaveState: Equatable {
|
||||
/// There is nothing to save (e.g. A new empty item was just created, an item was just loaded)
|
||||
case nothingToSave
|
||||
/// The item has been modified and needs saving.
|
||||
/// Saving should occur in N seconds.
|
||||
case needsSaving(secondsRemaining: Int)
|
||||
/// A saving operation is in progress.
|
||||
case saving
|
||||
/// The item has been saved to disk.
|
||||
case saved
|
||||
}
|
||||
|
||||
/// Models an auto-save mechanism, which automatically saves an item after N seconds.
|
||||
///
|
||||
/// # Implementation notes
|
||||
///
|
||||
/// - This runs on the main actor because running this on other actors causes issues with published properties.
|
||||
/// - Running on one actor helps ensure thread safety.
|
||||
@MainActor
|
||||
class AutoSaveViewModel: ObservableObject {
|
||||
/// The delay between the time something is marked as needing to save, and the actual saving operation.
|
||||
///
|
||||
/// Should be low enough that the user does not lose significant progress, and should be high enough to avoid unnecessary disk writes and jittery, stress-inducing behavior
|
||||
let saveDelay: Int
|
||||
/// The current state of this model
|
||||
@Published private(set) var savedState: SaveState
|
||||
/// A timer which counts down the time to save the item
|
||||
private var timer: Timer?
|
||||
/// The function that performs the actual save operation
|
||||
var save: () async -> Void
|
||||
|
||||
|
||||
// MARK: Init/de-init
|
||||
|
||||
/// Initializes a new auto-save model
|
||||
/// - Parameters:
|
||||
/// - save: The function that performs the save operation
|
||||
/// - initialState: Optional initial state
|
||||
/// - saveDelay: The time delay between the item is marked as needing to be saved, and the actual save operation — denoted in seconds.
|
||||
init(save: @escaping () async -> Void, initialState: SaveState = .nothingToSave, saveDelay: Int = 3) {
|
||||
self.saveDelay = saveDelay
|
||||
self.savedState = initialState
|
||||
self.save = save
|
||||
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true, block: { timer in
|
||||
Task { await self.tick() } // Task { await ... } ensures the function is properly run on the main actor and avoids thread-safety issues
|
||||
})
|
||||
self.timer = timer
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let timer = self.timer {
|
||||
timer.isValid ? timer.invalidate() : ()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Internal logic
|
||||
|
||||
/// Runs internal countdown-to-save logic
|
||||
private func tick() async {
|
||||
switch self.savedState {
|
||||
case .needsSaving(secondsRemaining: let secondsRemaining):
|
||||
if secondsRemaining <= 0 {
|
||||
self.savedState = .saving
|
||||
await save()
|
||||
self.savedState = .saved
|
||||
}
|
||||
else {
|
||||
self.savedState = .needsSaving(secondsRemaining: secondsRemaining - 1)
|
||||
}
|
||||
case .saving, .saved, .nothingToSave:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: External interface
|
||||
|
||||
/// Marks item as needing to be saved.
|
||||
/// Call this whenever your item is modified.
|
||||
func needsSaving() {
|
||||
self.savedState = .needsSaving(secondsRemaining: self.saveDelay)
|
||||
}
|
||||
|
||||
/// Marks item as saved.
|
||||
/// Call this when you know the item is already saved (e.g. when loading a saved item from memory).
|
||||
func markSaved() {
|
||||
self.savedState = .saved
|
||||
}
|
||||
|
||||
/// Tells the auto-save logic that there is nothing to be saved.
|
||||
/// Call this when there is nothing to be saved (e.g. when opening a new empty item).
|
||||
func markNothingToSave() {
|
||||
self.savedState = .nothingToSave
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,6 +170,9 @@ struct EditMetadataView: View {
|
||||
TextField(NSLocalizedString("Lightning Address or LNURL", comment: "Placeholder text for entry of Lightning Address or LNURL."), text: $ln)
|
||||
.autocorrectionDisabled(true)
|
||||
.textInputAutocapitalization(.never)
|
||||
.onReceive(Just(ln)) { newValue in
|
||||
self.ln = newValue.trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
}
|
||||
|
||||
Section(content: {
|
||||
|
||||
@@ -122,6 +122,9 @@ struct ProfileView: View {
|
||||
func content_filter(_ fstate: FilterState) -> ((NostrEvent) -> Bool) {
|
||||
var filters = ContentFilters.defaults(damus_state: damus_state)
|
||||
filters.append(fstate.filter)
|
||||
if fstate == .conversations {
|
||||
filters.append({ profile.conversation_events.contains($0.id) } )
|
||||
}
|
||||
return ContentFilters(filters: filters).filter
|
||||
}
|
||||
|
||||
@@ -429,6 +432,17 @@ struct ProfileView: View {
|
||||
.padding(.horizontal)
|
||||
}
|
||||
|
||||
var tabs: [(String, FilterState)] {
|
||||
var tabs = [
|
||||
(NSLocalizedString("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies)."), FilterState.posts),
|
||||
(NSLocalizedString("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes)."), FilterState.posts_and_replies)
|
||||
]
|
||||
if profile.pubkey != damus_state.pubkey && !profile.conversation_events.isEmpty {
|
||||
tabs.append((NSLocalizedString("Conversations", comment: "Label for filter for seeing notes and replies that involve conversations between the signed in user and the current profile."), FilterState.conversations))
|
||||
}
|
||||
return tabs
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
ScrollView(.vertical) {
|
||||
@@ -440,10 +454,7 @@ struct ProfileView: View {
|
||||
aboutSection
|
||||
|
||||
VStack(spacing: 0) {
|
||||
CustomPicker(tabs: [
|
||||
(NSLocalizedString("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies)."), FilterState.posts),
|
||||
(NSLocalizedString("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes)."), FilterState.posts_and_replies)
|
||||
], selection: $filter_state)
|
||||
CustomPicker(tabs: tabs, selection: $filter_state)
|
||||
Divider()
|
||||
.frame(height: 1)
|
||||
}
|
||||
@@ -455,6 +466,9 @@ struct ProfileView: View {
|
||||
if filter_state == FilterState.posts_and_replies {
|
||||
InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.posts_and_replies))
|
||||
}
|
||||
if filter_state == FilterState.conversations && !profile.conversation_events.isEmpty {
|
||||
InnerTimelineView(events: profile.events, damus: damus_state, filter: content_filter(FilterState.conversations))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, Theme.safeAreaInsets?.left)
|
||||
.zIndex(-yOffset > navbarHeight ? 0 : 1)
|
||||
|
||||
@@ -136,7 +136,8 @@ struct DamusPurpleAccountView: View {
|
||||
created_at: Date.now,
|
||||
expiry: Date.init(timeIntervalSinceNow: 60 * 60 * 24 * 30),
|
||||
subscriber_number: 7,
|
||||
active: true
|
||||
active: true,
|
||||
attributes: []
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -149,7 +150,8 @@ struct DamusPurpleAccountView: View {
|
||||
created_at: Date.init(timeIntervalSinceNow: -60 * 60 * 24 * 37),
|
||||
expiry: Date.init(timeIntervalSinceNow: -60 * 60 * 24 * 7),
|
||||
subscriber_number: 7,
|
||||
active: false
|
||||
active: false,
|
||||
attributes: []
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ struct RepostedEvent: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
NavigationLink(value: Route.ProfileByKey(pubkey: event.pubkey)) {
|
||||
Reposted(damus: damus, pubkey: event.pubkey)
|
||||
Reposted(damus: damus, pubkey: event.pubkey, target: inner_ev.id)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
|
||||
@@ -20,8 +20,6 @@ struct SearchHomeView: View {
|
||||
return ContentFilters(filters: filters).filter
|
||||
}
|
||||
|
||||
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
|
||||
|
||||
var SearchInput: some View {
|
||||
HStack {
|
||||
HStack{
|
||||
@@ -64,17 +62,7 @@ struct SearchHomeView: View {
|
||||
return false
|
||||
}
|
||||
|
||||
if damus_state.settings.show_only_preferred_languages == false {
|
||||
return true
|
||||
}
|
||||
|
||||
// If we can't determine the note's language with 50%+ confidence, lean on the side of caution and show it anyway.
|
||||
let note_lang = damus_state.events.get_cache_data(ev.id).translations_model.note_language
|
||||
guard let note_lang else {
|
||||
return true
|
||||
}
|
||||
|
||||
return preferredLanguages.contains(note_lang)
|
||||
return true
|
||||
},
|
||||
content: {
|
||||
AnyView(VStack {
|
||||
|
||||
@@ -17,6 +17,7 @@ struct DeveloperSettingsView: View {
|
||||
Toggle(NSLocalizedString("Developer Mode", comment: "Setting to enable developer mode"), isOn: $settings.developer_mode)
|
||||
.toggleStyle(.switch)
|
||||
if settings.developer_mode {
|
||||
Toggle(NSLocalizedString("Undistract mode", comment: "Developer mode setting to scramble text and images to avoid distractions during development."), isOn: $settings.undistractMode)
|
||||
Toggle(NSLocalizedString("Always show onboarding", comment: "Developer mode setting to always show onboarding suggestions."), isOn: $settings.always_show_onboarding_suggestions)
|
||||
Picker(NSLocalizedString("Push notification environment", comment: "Prompt selection of the Push notification environment (Developer feature to switch between real/production mode to test modes)."),
|
||||
selection: Binding(
|
||||
|
||||
@@ -16,9 +16,6 @@ struct TranslationSettingsView: View {
|
||||
var body: some View {
|
||||
Form {
|
||||
Section(NSLocalizedString("Translations", comment: "Section title for selecting the translation service.")) {
|
||||
Toggle(NSLocalizedString("Show only preferred languages on Universe feed", comment: "Toggle to show notes that are only in the device's preferred languages on the Universe feed and hide notes that are in other languages."), isOn: $settings.show_only_preferred_languages)
|
||||
.toggleStyle(.switch)
|
||||
|
||||
Picker(NSLocalizedString("Service", comment: "Prompt selection of translation service provider."), selection: $settings.translation_service) {
|
||||
ForEach(TranslationService.allCases.filter({ damus_state.purple.enable_purple ? true : $0 != .purple }), id: \.self) { server in
|
||||
Text(server.model.displayName)
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
//
|
||||
// ThreadV2View.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Thomas Tastet on 25/12/2022.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ThreadView: View {
|
||||
let state: DamusState
|
||||
|
||||
@ObservedObject var thread: ThreadModel
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
var parent_events: [NostrEvent] {
|
||||
state.events.parent_events(event: thread.event, keypair: state.keypair)
|
||||
}
|
||||
|
||||
var sorted_child_events: [NostrEvent] {
|
||||
state.events.child_events(event: thread.event).sorted(by: { a, b in
|
||||
let a_is_muted = !should_show_event(event: a, damus_state: state)
|
||||
let b_is_muted = !should_show_event(event: b, damus_state: state)
|
||||
|
||||
if a_is_muted == b_is_muted {
|
||||
// If both are muted or unmuted, sort them based on their creation date.
|
||||
return a.created_at < b.created_at
|
||||
}
|
||||
else {
|
||||
// Muting status is different
|
||||
// Prioritize the replies that are not muted
|
||||
return !a_is_muted && b_is_muted
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
//let top_zap = get_top_zap(events: state.events, evid: thread.event.id)
|
||||
ScrollViewReader { reader in
|
||||
ScrollView {
|
||||
LazyVStack {
|
||||
// MARK: - Parents events view
|
||||
ForEach(parent_events, id: \.id) { parent_event in
|
||||
EventMutingContainerView(damus_state: state, event: parent_event) {
|
||||
EventView(damus: state, event: parent_event)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.onTapGesture {
|
||||
thread.set_active_event(parent_event, keypair: self.state.keypair)
|
||||
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: 40, y: 40)
|
||||
})
|
||||
|
||||
// MARK: - Actual event view
|
||||
EventMutingContainerView(
|
||||
damus_state: state,
|
||||
event: self.thread.event,
|
||||
muteBox: { event_shown, muted_reason in
|
||||
AnyView(
|
||||
EventMutedBoxView(shown: event_shown, reason: muted_reason)
|
||||
.padding(5)
|
||||
)
|
||||
}
|
||||
) {
|
||||
SelectedEventView(damus: state, event: self.thread.event, size: .selected)
|
||||
}
|
||||
.id(self.thread.event.id)
|
||||
|
||||
/*
|
||||
if let top_zap {
|
||||
ZapEvent(damus: state, zap: top_zap, is_top_zap: true)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
*/
|
||||
|
||||
ForEach(sorted_child_events, id: \.id) { child_event in
|
||||
EventMutingContainerView(
|
||||
damus_state: state,
|
||||
event: child_event
|
||||
) {
|
||||
EventView(damus: state, event: child_event)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.onTapGesture {
|
||||
thread.set_active_event(child_event, keypair: state.keypair)
|
||||
scroll_to_event(scroller: reader, id: child_event.id, delay: 0.1, animate: false)
|
||||
}
|
||||
|
||||
Divider()
|
||||
.padding([.top], 4)
|
||||
}
|
||||
}
|
||||
}.navigationBarTitle(NSLocalizedString("Thread", comment: "Navigation bar title for note thread."))
|
||||
.onAppear {
|
||||
thread.subscribe()
|
||||
let anchor: UnitPoint = self.thread.event.known_kind == .longform ? .top : .bottom
|
||||
scroll_to_event(scroller: reader, id: self.thread.event.id, delay: 0.0, animate: false, anchor: anchor)
|
||||
}
|
||||
.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_note, damus_state: state)
|
||||
ThreadView(state: state, thread: thread)
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,8 @@ struct ProfileZapLinkView<Content: View>: View {
|
||||
self.label = label
|
||||
self.action = action
|
||||
self.reactions_enabled = reactions_enabled
|
||||
self.lud16 = lud16
|
||||
self.lnurl = lnurl
|
||||
self.lud16 = lud16?.trimmingCharacters(in: .whitespaces)
|
||||
self.lnurl = lnurl?.trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
|
||||
init(damus_state: DamusState, pubkey: Pubkey, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) {
|
||||
@@ -36,8 +36,8 @@ struct ProfileZapLinkView<Content: View>: View {
|
||||
let profile_txn = damus_state.profiles.lookup_with_timestamp(pubkey)
|
||||
let record = profile_txn?.unsafeUnownedValue
|
||||
self.reactions_enabled = record?.profile?.reactions ?? true
|
||||
self.lud16 = record?.profile?.lud06
|
||||
self.lnurl = record?.lnurl
|
||||
self.lud16 = record?.profile?.lud06?.trimmingCharacters(in: .whitespaces)
|
||||
self.lnurl = record?.lnurl?.trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
|
||||
init(unownedProfileRecord: ProfileRecord?, profileModel: ProfileModel, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) {
|
||||
@@ -46,8 +46,8 @@ struct ProfileZapLinkView<Content: View>: View {
|
||||
self.action = action
|
||||
|
||||
self.reactions_enabled = unownedProfileRecord?.profile?.reactions ?? true
|
||||
self.lud16 = unownedProfileRecord?.profile?.lud16
|
||||
self.lnurl = unownedProfileRecord?.lnurl
|
||||
self.lud16 = unownedProfileRecord?.profile?.lud16?.trimmingCharacters(in: .whitespaces)
|
||||
self.lnurl = unownedProfileRecord?.lnurl?.trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -81,12 +81,13 @@ struct ProfileZapLinkView<Content: View>: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.disabled(lnurl == nil)
|
||||
.disabled(lnurl == nil && lud16 == nil)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ProfileZapLinkView(pubkey: test_pubkey, reactions_enabled: true, lud16: make_test_profile().lud16, lnurl: "test@sendzaps.lol", label: { reactions_enabled, lud16, lnurl in
|
||||
let profile = make_test_profile()
|
||||
ProfileZapLinkView(pubkey: test_pubkey, reactions_enabled: true, lud16: profile.lud16, lnurl: profile.lud06, label: { reactions_enabled, lud16, lnurl in
|
||||
Image("zap.fill")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -86,11 +86,14 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
|
||||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
Log.info("App delegate is handling a push notification", for: .push_notifications)
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
guard let notification = LossyLocalNotification.from_user_info(user_info: userInfo) else {
|
||||
Log.error("App delegate could not decode notification information", for: .push_notifications)
|
||||
return
|
||||
}
|
||||
notify(.local_notification(notification))
|
||||
Log.info("App delegate notifying the app about the received push notification", for: .push_notifications)
|
||||
Task { await QueueableNotify<LossyLocalNotification>.shared.add(item: notification) }
|
||||
completionHandler()
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -31,8 +31,8 @@
|
||||
<note>Privacy - Face ID Usage Description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve">
|
||||
<source>Damus needs access to your microphone for creating video recording posts</source>
|
||||
<target>Damus needs access to your microphone for creating video recording posts</target>
|
||||
<source>Damus needs access to your microphone to allow you to create video recordings that you can choose to post publicly on the network</source>
|
||||
<target>Damus needs access to your microphone to allow you to create video recordings that you can choose to post publicly on the network</target>
|
||||
<note>Privacy - Microphone Usage Description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve">
|
||||
@@ -252,6 +252,11 @@ Title text to indicate user to an add a relay.</note>
|
||||
<target>Additional information</target>
|
||||
<note>Header text to prompt user to optionally provide additional information when reporting a user or note.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Advice" xml:space="preserve">
|
||||
<source>Advice</source>
|
||||
<target>Advice</target>
|
||||
<note>Heading for some advice text to help the user with an error</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All" xml:space="preserve">
|
||||
<source>All</source>
|
||||
<target>All</target>
|
||||
@@ -533,10 +538,10 @@ Text for button to conect to Nostr Wallet Connect lightning wallet.</note>
|
||||
<target>Connect to Alby Wallet</target>
|
||||
<note>Button to attach an Alby Wallet, a service that provides a Lightning wallet for zapping sats. Alby is the name of the service and should not be translated.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect to Mutiny Wallet" xml:space="preserve">
|
||||
<source>Connect to Mutiny Wallet</source>
|
||||
<target>Connect to Mutiny Wallet</target>
|
||||
<note>Button to attach an Mutiny Wallet, a service that provides a Lightning wallet for zapping sats. Mutiny is the name of the service and should not be translated.</note>
|
||||
<trans-unit id="Connect to Coinos" xml:space="preserve">
|
||||
<source>Connect to Coinos</source>
|
||||
<target>Connect to Coinos</target>
|
||||
<note>Button to attach a Coinos Wallet, a service that provides a Lightning wallet for zapping sats. Coinos is the name of the service and should not be translated.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connecting" xml:space="preserve">
|
||||
<source>Connecting</source>
|
||||
@@ -553,6 +558,16 @@ Text for button to conect to Nostr Wallet Connect lightning wallet.</note>
|
||||
<target>Contact list has been reset</target>
|
||||
<note>Message indicating that the contact list was successfully reset.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact support via DMs" xml:space="preserve">
|
||||
<source>Contact support via DMs</source>
|
||||
<target>Contact support via DMs</target>
|
||||
<note>Button label to contact support from an error screen</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact support via email at [support@damus.io](mailto:support@damus.io)" xml:space="preserve">
|
||||
<source>Contact support via email at [support@damus.io](mailto:support@damus.io)</source>
|
||||
<target>Contact support via email at [support@damus.io](mailto:support@damus.io)</target>
|
||||
<note>Text to contact support via email</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Content filters" xml:space="preserve">
|
||||
<source>Content filters</source>
|
||||
<target>Content filters</target>
|
||||
@@ -654,6 +669,11 @@ Context menu option for copying the version of damus.</note>
|
||||
<target>Could not find user to mute...</target>
|
||||
<note>Alert message to indicate that the muted user could not be found.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Could not parse the URL you are trying to open." xml:space="preserve">
|
||||
<source>Could not parse the URL you are trying to open.</source>
|
||||
<target>Could not parse the URL you are trying to open.</target>
|
||||
<note>User visible error description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create Account" xml:space="preserve">
|
||||
<source>Create Account</source>
|
||||
<target>Create Account</target>
|
||||
@@ -779,7 +799,8 @@ Button to disconnect from the relay.</note>
|
||||
<trans-unit id="Dismiss" xml:space="preserve">
|
||||
<source>Dismiss</source>
|
||||
<target>Dismiss</target>
|
||||
<note>Button to dismiss alert</note>
|
||||
<note>Button to dismiss alert
|
||||
Button to dismiss error</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Done" xml:space="preserve">
|
||||
<source>Done</source>
|
||||
@@ -815,11 +836,21 @@ The duration in which to mute the given item.</note>
|
||||
Button to enter edit mode for modifying the list of relays.
|
||||
Edit Button for editing profile</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Edit Image" xml:space="preserve">
|
||||
<source>Edit Image</source>
|
||||
<target>Edit Image</target>
|
||||
<note>Accessibility label for a button that edits an image</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Edit banner image" xml:space="preserve">
|
||||
<source>Edit banner image</source>
|
||||
<target>Edit banner image</target>
|
||||
<note>Accessibility label for edit banner image button</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Edit profile picture" xml:space="preserve">
|
||||
<source>Edit profile picture</source>
|
||||
<target>Edit profile picture</target>
|
||||
<note>Accessibility label for a button that edits a profile picture</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enable Purple auto-translations" xml:space="preserve">
|
||||
<source>Enable Purple auto-translations</source>
|
||||
<target>Enable Purple auto-translations</target>
|
||||
@@ -878,6 +909,16 @@ Title indicating that an error has occurred.</note>
|
||||
<target>Error syncing up push notifications preferences with the server: %@</target>
|
||||
<note>Error label shown when system tries to sync up notification preferences to the push notification server but something fails</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error uploading profile image. Please check your internet connection and try again. If error persists, please contact Damus support (support@damus.io)." xml:space="preserve">
|
||||
<source>Error uploading profile image. Please check your internet connection and try again. If error persists, please contact Damus support (support@damus.io).</source>
|
||||
<target>Error uploading profile image. Please check your internet connection and try again. If error persists, please contact Damus support (support@damus.io).</target>
|
||||
<note>Error label when uploading profile image</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error while cropping image" xml:space="preserve">
|
||||
<source>Error while cropping image</source>
|
||||
<target>Error while cropping image</target>
|
||||
<note>Heading on cropping error page</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error, please try again" xml:space="preserve">
|
||||
<source>Error, please try again</source>
|
||||
<target>Error, please try again</target>
|
||||
@@ -913,6 +954,11 @@ Title indicating that an error has occurred.</note>
|
||||
<target>Expiry date</target>
|
||||
<note>Label for Purple subscription expiry date</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Failed to generate media for upload. Please try again. If error persists, please contact Damus support at support@damus.io" xml:space="preserve">
|
||||
<source>Failed to generate media for upload. Please try again. If error persists, please contact Damus support at support@damus.io</source>
|
||||
<target>Failed to generate media for upload. Please try again. If error persists, please contact Damus support at support@damus.io</target>
|
||||
<note>Error label forming media for upload after user crops the image.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Failed to get push notification preferences from the server" xml:space="preserve">
|
||||
<source>Failed to get push notification preferences from the server</source>
|
||||
<target>Failed to get push notification preferences from the server</target>
|
||||
@@ -1148,7 +1194,13 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
|
||||
<trans-unit id="Image URL" xml:space="preserve">
|
||||
<source>Image URL</source>
|
||||
<target>Image URL</target>
|
||||
<note>Option to enter a url</note>
|
||||
<note>Label for image url text field
|
||||
Option to enter a url</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Image is setup" xml:space="preserve">
|
||||
<source>Image is setup</source>
|
||||
<target>Image is setup</target>
|
||||
<note>Accessibility value on image control</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Image uploader" xml:space="preserve">
|
||||
<source>Image uploader</source>
|
||||
@@ -1190,6 +1242,11 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
|
||||
<target>Invalid Tip Address</target>
|
||||
<note>Title of alerting as invalid tip address.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Invalid URL" xml:space="preserve">
|
||||
<source>Invalid URL</source>
|
||||
<target>Invalid URL</target>
|
||||
<note>Error label when user enters an invalid URL</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Invalid key" xml:space="preserve">
|
||||
<source>Invalid key</source>
|
||||
<target>Invalid key</target>
|
||||
@@ -1256,6 +1313,11 @@ Settings section for managing keys</note>
|
||||
<target>Load media</target>
|
||||
<note>Button to show media in note.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Loading thread" xml:space="preserve">
|
||||
<source>Loading thread</source>
|
||||
<target>Loading thread</target>
|
||||
<note>Accessibility label for the thread view when it is loading</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Local" xml:space="preserve">
|
||||
<source>Local</source>
|
||||
<target>Local</target>
|
||||
@@ -1363,20 +1425,20 @@ Title for confirmation dialog to mute a profile.</note>
|
||||
<target>Mute Hashtag</target>
|
||||
<note>Label represnting a button that the user can tap to mute a given hashtag so they don't see it in their feed anymore.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Mute User" xml:space="preserve">
|
||||
<source>Mute User</source>
|
||||
<target>Mute User</target>
|
||||
<note>Title of alert for muting a user.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Mute conversation" xml:space="preserve">
|
||||
<source>Mute conversation</source>
|
||||
<target>Mute conversation</target>
|
||||
<note>Context menu option for muting a conversation.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Mute user" xml:space="preserve">
|
||||
<source>Mute user</source>
|
||||
<target>Mute user</target>
|
||||
<note>Context menu option for muting users.</note>
|
||||
<trans-unit id="Mute/Block User" xml:space="preserve">
|
||||
<source>Mute/Block User</source>
|
||||
<target>Mute/Block User</target>
|
||||
<note>Title of alert for muting/blocking a user.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Mute/Block user" xml:space="preserve">
|
||||
<source>Mute/Block user</source>
|
||||
<target>Mute/Block user</target>
|
||||
<note>Context menu option for muting/blocking users.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Muted" xml:space="preserve">
|
||||
<source>Muted</source>
|
||||
@@ -1448,6 +1510,11 @@ User confirm No</note>
|
||||
<target>No contact list was found. You might experience issues using the app. If you suspect you have permanently lost your contact list (or if you never had one), you can fix this by resetting it</target>
|
||||
<note>Section footer for Contact list first aid tools</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No image is currently setup" xml:space="preserve">
|
||||
<source>No image is currently setup</source>
|
||||
<target>No image is currently setup</target>
|
||||
<note>Accessibility value on image control</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No logs to display" xml:space="preserve">
|
||||
<source>No logs to display</source>
|
||||
<target>No logs to display</target>
|
||||
@@ -1463,6 +1530,11 @@ User confirm No</note>
|
||||
<target>No one will see that you zapped</target>
|
||||
<note>Description of anonymous zap type where the zap is sent anonymously and does not identify the user who sent it.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No profile picture is currently setup" xml:space="preserve">
|
||||
<source>No profile picture is currently setup</source>
|
||||
<target>No profile picture is currently setup</target>
|
||||
<note>Accessibility value on profile picture image control</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No results" xml:space="preserve">
|
||||
<source>No results</source>
|
||||
<target>No results</target>
|
||||
@@ -1499,6 +1571,11 @@ Picker option to indicate that sats should be sent to the user's wallet as a reg
|
||||
<target>Nostr Address</target>
|
||||
<note>Label for the Nostr Address section of user profile form.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Nostr is the super app. Because it’s actually an ecosystem of apps, all of which make each other better. People haven’t grasped that yet. They will when it’s more accessible and onboarding is more straightforward and intuitive." xml:space="preserve">
|
||||
<source>Nostr is the super app. Because it’s actually an ecosystem of apps, all of which make each other better. People haven’t grasped that yet. They will when it’s more accessible and onboarding is more straightforward and intuitive.</source>
|
||||
<target>Nostr is the super app. Because it’s actually an ecosystem of apps, all of which make each other better. People haven’t grasped that yet. They will when it’s more accessible and onboarding is more straightforward and intuitive.</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="NostrScript" xml:space="preserve">
|
||||
<source>NostrScript</source>
|
||||
<target>NostrScript</target>
|
||||
@@ -1524,6 +1601,11 @@ Picker option to indicate that sats should be sent to the user's wallet as a reg
|
||||
<target>Note from a %@ you've muted</target>
|
||||
<note>Text to indicate that what is being shown is a note which has been muted.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Note not found" xml:space="preserve">
|
||||
<source>Note not found</source>
|
||||
<target>Note not found</target>
|
||||
<note>Heading for the thread view in a not found error state</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Note you've muted" xml:space="preserve">
|
||||
<source>Note you've muted</source>
|
||||
<target>Note you've muted</target>
|
||||
@@ -1613,6 +1695,11 @@ Button label to dismiss an error dialog</note>
|
||||
<target>OnlyZaps mode</target>
|
||||
<note>Setting toggle to hide reactions.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Oops!" xml:space="preserve">
|
||||
<source>Oops!</source>
|
||||
<target>Oops!</target>
|
||||
<note>Heading for an error screen</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open in browser" xml:space="preserve">
|
||||
<source>Open in browser</source>
|
||||
<target>Open in browser</target>
|
||||
@@ -1684,6 +1771,16 @@ Section title for deleting the user</note>
|
||||
<target>Please choose relays from the list below to filter the current feed:</target>
|
||||
<note>Instructions on how to filter a specific timeline feed by choosing relay servers to filter on.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please double-check the checkout web page, or go to the Side Menu → "Purple" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue." xml:space="preserve">
|
||||
<source>Please double-check the checkout web page, or go to the Side Menu → "Purple" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue.</source>
|
||||
<target>Please double-check the checkout web page, or go to the Side Menu → "Purple" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue.</target>
|
||||
<note>User-facing tips on what to do if a Purple welcome link doesn't work</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please try again, check the URL for typos, or contact support for further help." xml:space="preserve">
|
||||
<source>Please try again, check the URL for typos, or contact support for further help.</source>
|
||||
<target>Please try again, check the URL for typos, or contact support for further help.</target>
|
||||
<note>User visible error tips</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Point your camera to a QR code…" xml:space="preserve">
|
||||
<source>Point your camera to a QR code…</source>
|
||||
<target>Point your camera to a QR code…</target>
|
||||
@@ -1747,6 +1844,11 @@ Label indicating the production environment for Push notification functionality<
|
||||
<target>Profile action sheets allow you to follow, zap, or DM profiles more quickly without having to view their full profile</target>
|
||||
<note>Section footer clarifying what the profile action sheet feature does</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Profile picture is setup" xml:space="preserve">
|
||||
<source>Profile picture is setup</source>
|
||||
<target>Profile picture is setup</target>
|
||||
<note>Accessibility value on profile picture image control</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Profiles" xml:space="preserve">
|
||||
<source>Profiles</source>
|
||||
<target>Profiles</target>
|
||||
@@ -2034,6 +2136,11 @@ Button to save key, complete account creation, and start using the app.</note>
|
||||
<target>Save your login info?</target>
|
||||
<note>Ask user if they want to save their account information.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Saved" xml:space="preserve">
|
||||
<source>Saved</source>
|
||||
<target>Saved</target>
|
||||
<note>Small label indicating that the user's draft has been saved to storage</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Scan Code" xml:space="preserve">
|
||||
<source>Scan Code</source>
|
||||
<target>Scan Code</target>
|
||||
@@ -2202,11 +2309,6 @@ Button to show more of a long profile description.</note>
|
||||
<target>Show only from users you follow</target>
|
||||
<note>Setting to Show notifications only associated to users your follow</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show only preferred languages on Universe feed" xml:space="preserve">
|
||||
<source>Show only preferred languages on Universe feed</source>
|
||||
<target>Show only preferred languages on Universe feed</target>
|
||||
<note>Toggle to show notes that are only in the device's preferred languages on the Universe feed and hide notes that are in other languages.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show profile action sheets" xml:space="preserve">
|
||||
<source>Show profile action sheets</source>
|
||||
<target>Show profile action sheets</target>
|
||||
@@ -2217,6 +2319,11 @@ Button to show more of a long profile description.</note>
|
||||
<target>Show wallet selector</target>
|
||||
<note>Toggle to show or hide selection of wallet.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Shows options to edit the image" xml:space="preserve">
|
||||
<source>Shows options to edit the image</source>
|
||||
<target>Shows options to edit the image</target>
|
||||
<note>Accessibility hint for a button that edits an image</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Side menu" xml:space="preserve">
|
||||
<source>Side menu</source>
|
||||
<target>Side menu</target>
|
||||
@@ -2267,6 +2374,11 @@ Button to show more of a long profile description.</note>
|
||||
<target>Someone zapped you ⚡️</target>
|
||||
<note>Title label for a push notification where someone zapped the user</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sorry, but for some reason there has been an issue while trying to crop this image. Please try again later. If the error persists, please contact [Damus support](mailto:support@damus.io)" xml:space="preserve">
|
||||
<source>Sorry, but for some reason there has been an issue while trying to crop this image. Please try again later. If the error persists, please contact [Damus support](mailto:support@damus.io)</source>
|
||||
<target>Sorry, but for some reason there has been an issue while trying to crop this image. Please try again later. If the error persists, please contact [Damus support](mailto:support@damus.io)</target>
|
||||
<note>Cropping error message</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sorry, this QR code looks incompatible with Damus. Please try another one." xml:space="preserve">
|
||||
<source>Sorry, this QR code looks incompatible with Damus. Please try another one.</source>
|
||||
<target>Sorry, this QR code looks incompatible with Damus. Please try another one.</target>
|
||||
@@ -2453,11 +2565,6 @@ Nice to meet you all! #introductions #plebchain </target>
|
||||
<target>This user cannot be zapped because they have not configured zaps on their account yet. Time to orange-pill?</target>
|
||||
<note>Comment explaining why a user cannot be zapped.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Thread" xml:space="preserve">
|
||||
<source>Thread</source>
|
||||
<target>Thread</target>
|
||||
<note>Navigation bar title for note thread.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Threads" xml:space="preserve">
|
||||
<source>Threads</source>
|
||||
<target>Threads</target>
|
||||
@@ -2519,6 +2626,11 @@ Section header for text and appearance settings</note>
|
||||
<target>Truncate timeline text</target>
|
||||
<note>Setting to truncate text in timeline</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Try checking the link again, your internet connection, whether you need to connect to a specific relay to access this content." xml:space="preserve">
|
||||
<source>Try checking the link again, your internet connection, whether you need to connect to a specific relay to access this content.</source>
|
||||
<target>Try checking the link again, your internet connection, whether you need to connect to a specific relay to access this content.</target>
|
||||
<note>Tips on what to do if a note cannot be found.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Type %@ to delete" xml:space="preserve">
|
||||
<source>Type %@ to delete</source>
|
||||
<target>Type %@ to delete</target>
|
||||
@@ -2715,6 +2827,11 @@ Title for section in zap settings that controls the Lightning wallet selection.<
|
||||
<target>We did not detect any issues that we can automatically fix for you. If you are having issues, please contact Damus support: [support@damus.io](mailto:support@damus.io)</target>
|
||||
<note>Message indicating that no First Aid actions are available.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="We were unable to find the note you were looking for." xml:space="preserve">
|
||||
<source>We were unable to find the note you were looking for.</source>
|
||||
<target>We were unable to find the note you were looking for.</target>
|
||||
<note>Text for the thread view when it is unable to find the note the user is looking for</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="We'll save your account key, so you won't need to enter it manually next time you log in." xml:space="preserve">
|
||||
<source>We'll save your account key, so you won't need to enter it manually next time you log in.</source>
|
||||
<target>We'll save your account key, so you won't need to enter it manually next time you log in.</target>
|
||||
@@ -2796,6 +2913,16 @@ User confirm Yes</note>
|
||||
<target>You cannot post a highlight because you have selected no text on the page! Please close this, select some text, and try again.</target>
|
||||
<note>Label explaining a highlight cannot be made because there was no selected text, and some instructions on how to resolve the issue</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You clicked on a Purple welcome link, but it does not look like the checkout is completed yet. This is likely a bug." xml:space="preserve">
|
||||
<source>You clicked on a Purple welcome link, but it does not look like the checkout is completed yet. This is likely a bug.</source>
|
||||
<target>You clicked on a Purple welcome link, but it does not look like the checkout is completed yet. This is likely a bug.</target>
|
||||
<note>Error label upon continuing in the app from a Damus Purple purchase</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You clicked on a Purple welcome link, but we could not find your checkout. This is likely a bug." xml:space="preserve">
|
||||
<source>You clicked on a Purple welcome link, but we could not find your checkout. This is likely a bug.</source>
|
||||
<target>You clicked on a Purple welcome link, but we could not find your checkout. This is likely a bug.</target>
|
||||
<note>Error label upon continuing in the app from a Damus Purple purchase</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You have no bookmarks yet, add them in the context menu" xml:space="preserve">
|
||||
<source>You have no bookmarks yet, add them in the context menu</source>
|
||||
<target>You have no bookmarks yet, add them in the context menu</target>
|
||||
@@ -2826,6 +2953,11 @@ User confirm Yes</note>
|
||||
<target>Your Purple subscription has expired. Renew?</target>
|
||||
<note>A notification message explaining to the user that their Damus Purple Subscription has expired, prompting them to renew.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your draft has been saved to storage" xml:space="preserve">
|
||||
<source>Your draft has been saved to storage</source>
|
||||
<target>Your draft has been saved to storage</target>
|
||||
<note>Accessibility label indicating that a user's post draft has been saved, meant only for visually impaired users</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your highlight is being broadcasted to the network. Please wait." xml:space="preserve">
|
||||
<source>Your highlight is being broadcasted to the network. Please wait.</source>
|
||||
<target>Your highlight is being broadcasted to the network. Please wait.</target>
|
||||
@@ -3727,6 +3859,11 @@ Title text to indicate user to an add a relay.</note>
|
||||
<target state="new">Additional information</target>
|
||||
<note>Header text to prompt user to optionally provide additional information when reporting a user or note.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Advice" xml:space="preserve">
|
||||
<source>Advice</source>
|
||||
<target state="new">Advice</target>
|
||||
<note>Heading for some advice text to help the user with an error</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All" xml:space="preserve">
|
||||
<source>All</source>
|
||||
<target state="new">All</target>
|
||||
@@ -4011,10 +4148,10 @@ Text for button to conect to Nostr Wallet Connect lightning wallet.</note>
|
||||
<target state="new">Connect to Alby Wallet</target>
|
||||
<note>Button to attach an Alby Wallet, a service that provides a Lightning wallet for zapping sats. Alby is the name of the service and should not be translated.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect to Mutiny Wallet" xml:space="preserve">
|
||||
<source>Connect to Mutiny Wallet</source>
|
||||
<target state="new">Connect to Mutiny Wallet</target>
|
||||
<note>Button to attach an Mutiny Wallet, a service that provides a Lightning wallet for zapping sats. Mutiny is the name of the service and should not be translated.</note>
|
||||
<trans-unit id="Connect to Coinos" xml:space="preserve">
|
||||
<source>Connect to Coinos</source>
|
||||
<target state="new">Connect to Coinos</target>
|
||||
<note>Button to attach a Coinos Wallet, a service that provides a Lightning wallet for zapping sats. Coinos is the name of the service and should not be translated.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connecting" xml:space="preserve">
|
||||
<source>Connecting</source>
|
||||
@@ -4031,6 +4168,16 @@ Text for button to conect to Nostr Wallet Connect lightning wallet.</note>
|
||||
<target state="new">Contact list has been reset</target>
|
||||
<note>Message indicating that the contact list was successfully reset.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact support via DMs" xml:space="preserve">
|
||||
<source>Contact support via DMs</source>
|
||||
<target state="new">Contact support via DMs</target>
|
||||
<note>Button label to contact support from an error screen</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Contact support via email at [support@damus.io](mailto:support@damus.io)" xml:space="preserve">
|
||||
<source>Contact support via email at [support@damus.io](mailto:support@damus.io)</source>
|
||||
<target state="new">Contact support via email at [support@damus.io](mailto:support@damus.io)</target>
|
||||
<note>Text to contact support via email</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Content filters" xml:space="preserve">
|
||||
<source>Content filters</source>
|
||||
<target state="new">Content filters</target>
|
||||
@@ -4132,6 +4279,11 @@ Context menu option for copying the version of damus.</note>
|
||||
<target state="new">Could not find user to mute...</target>
|
||||
<note>Alert message to indicate that the muted user could not be found.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Could not parse the URL you are trying to open." xml:space="preserve">
|
||||
<source>Could not parse the URL you are trying to open.</source>
|
||||
<target state="new">Could not parse the URL you are trying to open.</target>
|
||||
<note>User visible error description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Create Account" xml:space="preserve">
|
||||
<source>Create Account</source>
|
||||
<target state="new">Create Account</target>
|
||||
@@ -4257,7 +4409,8 @@ Button to disconnect from the relay.</note>
|
||||
<trans-unit id="Dismiss" xml:space="preserve">
|
||||
<source>Dismiss</source>
|
||||
<target state="new">Dismiss</target>
|
||||
<note>Button to dismiss alert</note>
|
||||
<note>Button to dismiss alert
|
||||
Button to dismiss error</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Done" xml:space="preserve">
|
||||
<source>Done</source>
|
||||
@@ -4293,11 +4446,21 @@ The duration in which to mute the given item.</note>
|
||||
Button to enter edit mode for modifying the list of relays.
|
||||
Edit Button for editing profile</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Edit Image" xml:space="preserve">
|
||||
<source>Edit Image</source>
|
||||
<target state="new">Edit Image</target>
|
||||
<note>Accessibility label for a button that edits an image</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Edit banner image" xml:space="preserve">
|
||||
<source>Edit banner image</source>
|
||||
<target state="new">Edit banner image</target>
|
||||
<note>Accessibility label for edit banner image button</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Edit profile picture" xml:space="preserve">
|
||||
<source>Edit profile picture</source>
|
||||
<target state="new">Edit profile picture</target>
|
||||
<note>Accessibility label for a button that edits a profile picture</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enable Purple auto-translations" xml:space="preserve">
|
||||
<source>Enable Purple auto-translations</source>
|
||||
<target state="new">Enable Purple auto-translations</target>
|
||||
@@ -4356,6 +4519,16 @@ Title indicating that an error has occurred.</note>
|
||||
<target state="new">Error syncing up push notifications preferences with the server: %@</target>
|
||||
<note>Error label shown when system tries to sync up notification preferences to the push notification server but something fails</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error uploading profile image. Please check your internet connection and try again. If error persists, please contact Damus support (support@damus.io)." xml:space="preserve">
|
||||
<source>Error uploading profile image. Please check your internet connection and try again. If error persists, please contact Damus support (support@damus.io).</source>
|
||||
<target state="new">Error uploading profile image. Please check your internet connection and try again. If error persists, please contact Damus support (support@damus.io).</target>
|
||||
<note>Error label when uploading profile image</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error while cropping image" xml:space="preserve">
|
||||
<source>Error while cropping image</source>
|
||||
<target state="new">Error while cropping image</target>
|
||||
<note>Heading on cropping error page</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Error, please try again" xml:space="preserve">
|
||||
<source>Error, please try again</source>
|
||||
<target state="new">Error, please try again</target>
|
||||
@@ -4391,6 +4564,11 @@ Title indicating that an error has occurred.</note>
|
||||
<target state="new">Expiry date</target>
|
||||
<note>Label for Purple subscription expiry date</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Failed to generate media for upload. Please try again. If error persists, please contact Damus support at support@damus.io" xml:space="preserve">
|
||||
<source>Failed to generate media for upload. Please try again. If error persists, please contact Damus support at support@damus.io</source>
|
||||
<target state="new">Failed to generate media for upload. Please try again. If error persists, please contact Damus support at support@damus.io</target>
|
||||
<note>Error label forming media for upload after user crops the image.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Failed to get push notification preferences from the server" xml:space="preserve">
|
||||
<source>Failed to get push notification preferences from the server</source>
|
||||
<target state="new">Failed to get push notification preferences from the server</target>
|
||||
@@ -4626,7 +4804,13 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
|
||||
<trans-unit id="Image URL" xml:space="preserve">
|
||||
<source>Image URL</source>
|
||||
<target state="new">Image URL</target>
|
||||
<note>Option to enter a url</note>
|
||||
<note>Label for image url text field
|
||||
Option to enter a url</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Image is setup" xml:space="preserve">
|
||||
<source>Image is setup</source>
|
||||
<target state="new">Image is setup</target>
|
||||
<note>Accessibility value on image control</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Image uploader" xml:space="preserve">
|
||||
<source>Image uploader</source>
|
||||
@@ -4668,6 +4852,11 @@ Hope to meet folks who are on their own journeys to a peaceful and free life!</t
|
||||
<target state="new">Invalid Tip Address</target>
|
||||
<note>Title of alerting as invalid tip address.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Invalid URL" xml:space="preserve">
|
||||
<source>Invalid URL</source>
|
||||
<target state="new">Invalid URL</target>
|
||||
<note>Error label when user enters an invalid URL</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Invalid key" xml:space="preserve">
|
||||
<source>Invalid key</source>
|
||||
<target state="new">Invalid key</target>
|
||||
@@ -4734,6 +4923,11 @@ Settings section for managing keys</note>
|
||||
<target state="new">Load media</target>
|
||||
<note>Button to show media in note.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Loading thread" xml:space="preserve">
|
||||
<source>Loading thread</source>
|
||||
<target state="new">Loading thread</target>
|
||||
<note>Accessibility label for the thread view when it is loading</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Local" xml:space="preserve">
|
||||
<source>Local</source>
|
||||
<target state="new">Local</target>
|
||||
@@ -4841,20 +5035,20 @@ Title for confirmation dialog to mute a profile.</note>
|
||||
<target state="new">Mute Hashtag</target>
|
||||
<note>Label represnting a button that the user can tap to mute a given hashtag so they don't see it in their feed anymore.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Mute User" xml:space="preserve">
|
||||
<source>Mute User</source>
|
||||
<target state="new">Mute User</target>
|
||||
<note>Title of alert for muting a user.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Mute conversation" xml:space="preserve">
|
||||
<source>Mute conversation</source>
|
||||
<target state="new">Mute conversation</target>
|
||||
<note>Context menu option for muting a conversation.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Mute user" xml:space="preserve">
|
||||
<source>Mute user</source>
|
||||
<target state="new">Mute user</target>
|
||||
<note>Context menu option for muting users.</note>
|
||||
<trans-unit id="Mute/Block User" xml:space="preserve">
|
||||
<source>Mute/Block User</source>
|
||||
<target state="new">Mute/Block User</target>
|
||||
<note>Title of alert for muting/blocking a user.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Mute/Block user" xml:space="preserve">
|
||||
<source>Mute/Block user</source>
|
||||
<target state="new">Mute/Block user</target>
|
||||
<note>Context menu option for muting/blocking users.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Muted" xml:space="preserve">
|
||||
<source>Muted</source>
|
||||
@@ -4926,6 +5120,11 @@ User confirm No</note>
|
||||
<target state="new">No content available to share</target>
|
||||
<note>Title indicating that there was no available content to share</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No image is currently setup" xml:space="preserve">
|
||||
<source>No image is currently setup</source>
|
||||
<target state="new">No image is currently setup</target>
|
||||
<note>Accessibility value on image control</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No logs to display" xml:space="preserve">
|
||||
<source>No logs to display</source>
|
||||
<target state="new">No logs to display</target>
|
||||
@@ -4941,6 +5140,11 @@ User confirm No</note>
|
||||
<target state="new">No one will see that you zapped</target>
|
||||
<note>Description of anonymous zap type where the zap is sent anonymously and does not identify the user who sent it.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No profile picture is currently setup" xml:space="preserve">
|
||||
<source>No profile picture is currently setup</source>
|
||||
<target state="new">No profile picture is currently setup</target>
|
||||
<note>Accessibility value on profile picture image control</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No results" xml:space="preserve">
|
||||
<source>No results</source>
|
||||
<target state="new">No results</target>
|
||||
@@ -4972,6 +5176,11 @@ Picker option to indicate that sats should be sent to the user's wallet as a reg
|
||||
<target state="new">Nostr Address</target>
|
||||
<note>Label for the Nostr Address section of user profile form.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Nostr is the super app. Because it’s actually an ecosystem of apps, all of which make each other better. People haven’t grasped that yet. They will when it’s more accessible and onboarding is more straightforward and intuitive." xml:space="preserve">
|
||||
<source>Nostr is the super app. Because it’s actually an ecosystem of apps, all of which make each other better. People haven’t grasped that yet. They will when it’s more accessible and onboarding is more straightforward and intuitive.</source>
|
||||
<target state="new">Nostr is the super app. Because it’s actually an ecosystem of apps, all of which make each other better. People haven’t grasped that yet. They will when it’s more accessible and onboarding is more straightforward and intuitive.</target>
|
||||
<note/>
|
||||
</trans-unit>
|
||||
<trans-unit id="NostrScript" xml:space="preserve">
|
||||
<source>NostrScript</source>
|
||||
<target state="new">NostrScript</target>
|
||||
@@ -4997,6 +5206,11 @@ Picker option to indicate that sats should be sent to the user's wallet as a reg
|
||||
<target state="new">Note from a %@ you've muted</target>
|
||||
<note>Text to indicate that what is being shown is a note which has been muted.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Note not found" xml:space="preserve">
|
||||
<source>Note not found</source>
|
||||
<target state="new">Note not found</target>
|
||||
<note>Heading for the thread view in a not found error state</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Note you've muted" xml:space="preserve">
|
||||
<source>Note you've muted</source>
|
||||
<target state="new">Note you've muted</target>
|
||||
@@ -5086,6 +5300,11 @@ Button label to dismiss an error dialog</note>
|
||||
<target state="new">OnlyZaps mode</target>
|
||||
<note>Setting toggle to hide reactions.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Oops!" xml:space="preserve">
|
||||
<source>Oops!</source>
|
||||
<target state="new">Oops!</target>
|
||||
<note>Heading for an error screen</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open in browser" xml:space="preserve">
|
||||
<source>Open in browser</source>
|
||||
<target state="new">Open in browser</target>
|
||||
@@ -5157,6 +5376,16 @@ Section title for deleting the user</note>
|
||||
<target state="new">Please choose relays from the list below to filter the current feed:</target>
|
||||
<note>Instructions on how to filter a specific timeline feed by choosing relay servers to filter on.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please double-check the checkout web page, or go to the Side Menu → "Purple" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue." xml:space="preserve">
|
||||
<source>Please double-check the checkout web page, or go to the Side Menu → "Purple" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue.</source>
|
||||
<target state="new">Please double-check the checkout web page, or go to the Side Menu → "Purple" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue.</target>
|
||||
<note>User-facing tips on what to do if a Purple welcome link doesn't work</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Please try again, check the URL for typos, or contact support for further help." xml:space="preserve">
|
||||
<source>Please try again, check the URL for typos, or contact support for further help.</source>
|
||||
<target state="new">Please try again, check the URL for typos, or contact support for further help.</target>
|
||||
<note>User visible error tips</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Point your camera to a QR code…" xml:space="preserve">
|
||||
<source>Point your camera to a QR code…</source>
|
||||
<target state="new">Point your camera to a QR code…</target>
|
||||
@@ -5210,6 +5439,11 @@ Label indicating the production environment for Push notification functionality<
|
||||
<target state="new">Profile action sheets allow you to follow, zap, or DM profiles more quickly without having to view their full profile</target>
|
||||
<note>Section footer clarifying what the profile action sheet feature does</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Profile picture is setup" xml:space="preserve">
|
||||
<source>Profile picture is setup</source>
|
||||
<target state="new">Profile picture is setup</target>
|
||||
<note>Accessibility value on profile picture image control</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Profiles" xml:space="preserve">
|
||||
<source>Profiles</source>
|
||||
<target state="new">Profiles</target>
|
||||
@@ -5497,6 +5731,11 @@ Button to save key, complete account creation, and start using the app.</note>
|
||||
<target state="new">Save your login info?</target>
|
||||
<note>Ask user if they want to save their account information.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Saved" xml:space="preserve">
|
||||
<source>Saved</source>
|
||||
<target state="new">Saved</target>
|
||||
<note>Small label indicating that the user's draft has been saved to storage</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Scan Code" xml:space="preserve">
|
||||
<source>Scan Code</source>
|
||||
<target state="new">Scan Code</target>
|
||||
@@ -5675,11 +5914,6 @@ Button to show more of a long profile description.</note>
|
||||
<target state="new">Show only from users you follow</target>
|
||||
<note>Setting to Show notifications only associated to users your follow</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show only preferred languages on Universe feed" xml:space="preserve">
|
||||
<source>Show only preferred languages on Universe feed</source>
|
||||
<target state="new">Show only preferred languages on Universe feed</target>
|
||||
<note>Toggle to show notes that are only in the device's preferred languages on the Universe feed and hide notes that are in other languages.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show profile action sheets" xml:space="preserve">
|
||||
<source>Show profile action sheets</source>
|
||||
<target state="new">Show profile action sheets</target>
|
||||
@@ -5690,6 +5924,11 @@ Button to show more of a long profile description.</note>
|
||||
<target state="new">Show wallet selector</target>
|
||||
<note>Toggle to show or hide selection of wallet.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Shows options to edit the image" xml:space="preserve">
|
||||
<source>Shows options to edit the image</source>
|
||||
<target state="new">Shows options to edit the image</target>
|
||||
<note>Accessibility hint for a button that edits an image</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Side menu" xml:space="preserve">
|
||||
<source>Side menu</source>
|
||||
<target state="new">Side menu</target>
|
||||
@@ -5740,6 +5979,11 @@ Button to show more of a long profile description.</note>
|
||||
<target state="new">Someone zapped you ⚡️</target>
|
||||
<note>Title label for a push notification where someone zapped the user</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sorry, but for some reason there has been an issue while trying to crop this image. Please try again later. If the error persists, please contact [Damus support](mailto:support@damus.io)" xml:space="preserve">
|
||||
<source>Sorry, but for some reason there has been an issue while trying to crop this image. Please try again later. If the error persists, please contact [Damus support](mailto:support@damus.io)</source>
|
||||
<target state="new">Sorry, but for some reason there has been an issue while trying to crop this image. Please try again later. If the error persists, please contact [Damus support](mailto:support@damus.io)</target>
|
||||
<note>Cropping error message</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Sorry, this QR code looks incompatible with Damus. Please try another one." xml:space="preserve">
|
||||
<source>Sorry, this QR code looks incompatible with Damus. Please try another one.</source>
|
||||
<target state="new">Sorry, this QR code looks incompatible with Damus. Please try another one.</target>
|
||||
@@ -5926,11 +6170,6 @@ Nice to meet you all! #introductions #plebchain </target>
|
||||
<target state="new">This user cannot be zapped because they have not configured zaps on their account yet. Time to orange-pill?</target>
|
||||
<note>Comment explaining why a user cannot be zapped.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Thread" xml:space="preserve">
|
||||
<source>Thread</source>
|
||||
<target state="new">Thread</target>
|
||||
<note>Navigation bar title for note thread.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Threads" xml:space="preserve">
|
||||
<source>Threads</source>
|
||||
<target state="new">Threads</target>
|
||||
@@ -5992,6 +6231,11 @@ Section header for text and appearance settings</note>
|
||||
<target state="new">Truncate timeline text</target>
|
||||
<note>Setting to truncate text in timeline</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Try checking the link again, your internet connection, whether you need to connect to a specific relay to access this content." xml:space="preserve">
|
||||
<source>Try checking the link again, your internet connection, whether you need to connect to a specific relay to access this content.</source>
|
||||
<target state="new">Try checking the link again, your internet connection, whether you need to connect to a specific relay to access this content.</target>
|
||||
<note>Tips on what to do if a note cannot be found.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Type %@ to delete" xml:space="preserve">
|
||||
<source>Type %@ to delete</source>
|
||||
<target state="new">Type %@ to delete</target>
|
||||
@@ -6188,6 +6432,11 @@ Title for section in zap settings that controls the Lightning wallet selection.<
|
||||
<target state="new">We did not detect any issues that we can automatically fix for you. If you are having issues, please contact Damus support: [support@damus.io](mailto:support@damus.io)</target>
|
||||
<note>Message indicating that no First Aid actions are available.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="We were unable to find the note you were looking for." xml:space="preserve">
|
||||
<source>We were unable to find the note you were looking for.</source>
|
||||
<target state="new">We were unable to find the note you were looking for.</target>
|
||||
<note>Text for the thread view when it is unable to find the note the user is looking for</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="We'll save your account key, so you won't need to enter it manually next time you log in." xml:space="preserve">
|
||||
<source>We'll save your account key, so you won't need to enter it manually next time you log in.</source>
|
||||
<target state="new">We'll save your account key, so you won't need to enter it manually next time you log in.</target>
|
||||
@@ -6264,6 +6513,16 @@ User confirm Yes</note>
|
||||
<target state="new">You cannot share content because you are not logged in. Please close this view, log in to your account, and try again.</target>
|
||||
<note>Label explaining that sharing cannot proceed because the user is not logged in.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You clicked on a Purple welcome link, but it does not look like the checkout is completed yet. This is likely a bug." xml:space="preserve">
|
||||
<source>You clicked on a Purple welcome link, but it does not look like the checkout is completed yet. This is likely a bug.</source>
|
||||
<target state="new">You clicked on a Purple welcome link, but it does not look like the checkout is completed yet. This is likely a bug.</target>
|
||||
<note>Error label upon continuing in the app from a Damus Purple purchase</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You clicked on a Purple welcome link, but we could not find your checkout. This is likely a bug." xml:space="preserve">
|
||||
<source>You clicked on a Purple welcome link, but we could not find your checkout. This is likely a bug.</source>
|
||||
<target state="new">You clicked on a Purple welcome link, but we could not find your checkout. This is likely a bug.</target>
|
||||
<note>Error label upon continuing in the app from a Damus Purple purchase</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="You have no bookmarks yet, add them in the context menu" xml:space="preserve">
|
||||
<source>You have no bookmarks yet, add them in the context menu</source>
|
||||
<target state="new">You have no bookmarks yet, add them in the context menu</target>
|
||||
@@ -6299,6 +6558,11 @@ User confirm Yes</note>
|
||||
<target state="new">Your content is being broadcasted to the network. Please wait.</target>
|
||||
<note>Label explaining that their content sharing action is in progress</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your draft has been saved to storage" xml:space="preserve">
|
||||
<source>Your draft has been saved to storage</source>
|
||||
<target state="new">Your draft has been saved to storage</target>
|
||||
<note>Accessibility label indicating that a user's post draft has been saved, meant only for visually impaired users</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Your report will be sent to the relays you are connected to" xml:space="preserve">
|
||||
<source>Your report will be sent to the relays you are connected to</source>
|
||||
<target state="new">Your report will be sent to the relays you are connected to</target>
|
||||
|
||||
@@ -147,6 +147,9 @@
|
||||
"ADMIN" : {
|
||||
"comment" : "Text label indicating the profile picture underneath it is the admin of the Nostr relay."
|
||||
},
|
||||
"Advice" : {
|
||||
"comment" : "Heading for some advice text to help the user with an error"
|
||||
},
|
||||
"All" : {
|
||||
"comment" : "Human-readable short description of the 'friends filter' when it is set to 'all'\nLabel for filter for all notifications."
|
||||
},
|
||||
@@ -315,8 +318,8 @@
|
||||
"Connect to Alby Wallet" : {
|
||||
"comment" : "Button to attach an Alby Wallet, a service that provides a Lightning wallet for zapping sats. Alby is the name of the service and should not be translated."
|
||||
},
|
||||
"Connect to Mutiny Wallet" : {
|
||||
"comment" : "Button to attach an Mutiny Wallet, a service that provides a Lightning wallet for zapping sats. Mutiny is the name of the service and should not be translated."
|
||||
"Connect to Coinos" : {
|
||||
"comment" : "Button to attach a Coinos Wallet, a service that provides a Lightning wallet for zapping sats. Coinos is the name of the service and should not be translated."
|
||||
},
|
||||
"Connecting" : {
|
||||
"comment" : "Relay status label that indicates a relay is connecting."
|
||||
@@ -330,6 +333,12 @@
|
||||
"Contact list has been reset" : {
|
||||
"comment" : "Message indicating that the contact list was successfully reset."
|
||||
},
|
||||
"Contact support via DMs" : {
|
||||
"comment" : "Button label to contact support from an error screen"
|
||||
},
|
||||
"Contact support via email at [support@damus.io](mailto:support@damus.io)" : {
|
||||
"comment" : "Text to contact support via email"
|
||||
},
|
||||
"Content filters" : {
|
||||
"comment" : "Section title for content filtering/moderation configuration."
|
||||
},
|
||||
@@ -387,6 +396,9 @@
|
||||
"Could not find user to mute..." : {
|
||||
"comment" : "Alert message to indicate that the muted user could not be found."
|
||||
},
|
||||
"Could not parse the URL you are trying to open." : {
|
||||
"comment" : "User visible error description"
|
||||
},
|
||||
"Create account" : {
|
||||
"comment" : "Button to navigate to create account view."
|
||||
},
|
||||
@@ -454,7 +466,7 @@
|
||||
"comment" : "Text for button to disconnect from Nostr Wallet Connect lightning wallet."
|
||||
},
|
||||
"Dismiss" : {
|
||||
"comment" : "Button to dismiss alert"
|
||||
"comment" : "Button to dismiss alert\nButton to dismiss error"
|
||||
},
|
||||
"DMs" : {
|
||||
"comment" : "Navigation title for DMs view, where DM is the English abbreviation for Direct Message.\nNavigation title for view of DMs, where DM is an English abbreviation for Direct Message.\nPicker option for DM selector for seeing only DMs that have been responded to. DM is the English abbreviation for Direct Message.\nSetting to enable DM Local Notification\nToolbar label for DMs view, where DM is the English abbreviation for Direct Message."
|
||||
@@ -477,6 +489,12 @@
|
||||
"Edit banner image" : {
|
||||
"comment" : "Accessibility label for edit banner image button"
|
||||
},
|
||||
"Edit Image" : {
|
||||
"comment" : "Accessibility label for a button that edits an image"
|
||||
},
|
||||
"Edit profile picture" : {
|
||||
"comment" : "Accessibility label for a button that edits a profile picture"
|
||||
},
|
||||
"Enable experimental Purple API support" : {
|
||||
"comment" : "Developer mode setting to enable experimental Purple API support."
|
||||
},
|
||||
@@ -510,6 +528,12 @@
|
||||
"Error syncing up push notifications preferences with the server: %@" : {
|
||||
"comment" : "Error label shown when system tries to sync up notification preferences to the push notification server but something fails"
|
||||
},
|
||||
"Error uploading profile image. Please check your internet connection and try again. If error persists, please contact Damus support (support@damus.io)." : {
|
||||
"comment" : "Error label when uploading profile image"
|
||||
},
|
||||
"Error while cropping image" : {
|
||||
"comment" : "Heading on cropping error page"
|
||||
},
|
||||
"Error, please try again" : {
|
||||
"comment" : "Text on QR code camera view indicating an error"
|
||||
},
|
||||
@@ -534,6 +558,9 @@
|
||||
"Expiry date" : {
|
||||
"comment" : "Label for Purple subscription expiry date"
|
||||
},
|
||||
"Failed to generate media for upload. Please try again. If error persists, please contact Damus support at support@damus.io" : {
|
||||
"comment" : "Error label forming media for upload after user crops the image."
|
||||
},
|
||||
"Failed to get push notification preferences from the server" : {
|
||||
"comment" : "Error label indicating about a failure in fetching notification preferences"
|
||||
},
|
||||
@@ -679,11 +706,14 @@
|
||||
"Illegal Content" : {
|
||||
"comment" : "Description of report type for illegal content."
|
||||
},
|
||||
"Image is setup" : {
|
||||
"comment" : "Accessibility value on image control"
|
||||
},
|
||||
"Image uploader" : {
|
||||
"comment" : "Prompt selection of user's image uploader"
|
||||
},
|
||||
"Image URL" : {
|
||||
"comment" : "Option to enter a url"
|
||||
"comment" : "Label for image url text field\nOption to enter a url"
|
||||
},
|
||||
"Images" : {
|
||||
"comment" : "Section title for images configuration."
|
||||
@@ -712,6 +742,9 @@
|
||||
"Invalid Tip Address" : {
|
||||
"comment" : "Title of alerting as invalid tip address."
|
||||
},
|
||||
"Invalid URL" : {
|
||||
"comment" : "Error label when user enters an invalid URL"
|
||||
},
|
||||
"It seems that you already have a translation service configured. Would you like to switch to Damus Purple as your translator?" : {
|
||||
"comment" : "Confirmation dialog question asking users if they want their translation settings to be automatically switched to the Damus Purple translation service"
|
||||
},
|
||||
@@ -748,6 +781,9 @@
|
||||
"Load media" : {
|
||||
"comment" : "Button to show media in note."
|
||||
},
|
||||
"Loading thread" : {
|
||||
"comment" : "Accessibility label for the thread view when it is loading"
|
||||
},
|
||||
"Local" : {
|
||||
"comment" : "Option for notification mode setting: Local notification mode"
|
||||
},
|
||||
@@ -811,11 +847,11 @@
|
||||
"Mute Hashtag" : {
|
||||
"comment" : "Label represnting a button that the user can tap to mute a given hashtag so they don't see it in their feed anymore."
|
||||
},
|
||||
"Mute user" : {
|
||||
"comment" : "Context menu option for muting users."
|
||||
"Mute/Block user" : {
|
||||
"comment" : "Context menu option for muting/blocking users."
|
||||
},
|
||||
"Mute User" : {
|
||||
"comment" : "Title of alert for muting a user."
|
||||
"Mute/Block User" : {
|
||||
"comment" : "Title of alert for muting/blocking a user."
|
||||
},
|
||||
"Muted" : {
|
||||
"comment" : "Navigation title of view to see list of muted users & phrases.\nSidebar menu label for muted users view."
|
||||
@@ -856,6 +892,9 @@
|
||||
"No content available to share" : {
|
||||
"comment" : "Title indicating that there was no available content to share"
|
||||
},
|
||||
"No image is currently setup" : {
|
||||
"comment" : "Accessibility value on image control"
|
||||
},
|
||||
"No logs to display" : {
|
||||
"comment" : "Label to indicate that there are no developer mode logs available to be displayed on the screen"
|
||||
},
|
||||
@@ -865,6 +904,9 @@
|
||||
"No one will 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."
|
||||
},
|
||||
"No profile picture is currently setup" : {
|
||||
"comment" : "Accessibility value on profile picture image control"
|
||||
},
|
||||
"No results" : {
|
||||
"comment" : "A label indicating that note search resulted in no results"
|
||||
},
|
||||
@@ -897,6 +939,9 @@
|
||||
},
|
||||
"Nostr Address" : {
|
||||
"comment" : "Label for the Nostr Address section of user profile form."
|
||||
},
|
||||
"Nostr is the super app. Because it’s actually an ecosystem of apps, all of which make each other better. People haven’t grasped that yet. They will when it’s more accessible and onboarding is more straightforward and intuitive." : {
|
||||
|
||||
},
|
||||
"NostrScript" : {
|
||||
"comment" : "Navigation title for the view showing NostrScript."
|
||||
@@ -913,6 +958,9 @@
|
||||
"Note from a %@ you've muted" : {
|
||||
"comment" : "Text to indicate that what is being shown is a note which has been muted."
|
||||
},
|
||||
"Note not found" : {
|
||||
"comment" : "Heading for the thread view in a not found error state"
|
||||
},
|
||||
"Note you've muted" : {
|
||||
"comment" : "Label indicating note has been muted\nText to indicate that what is being shown is a note which has been muted."
|
||||
},
|
||||
@@ -973,6 +1021,9 @@
|
||||
"OnlyZaps mode" : {
|
||||
"comment" : "Setting toggle to hide reactions."
|
||||
},
|
||||
"Oops!" : {
|
||||
"comment" : "Heading for an error screen"
|
||||
},
|
||||
"Open in browser" : {
|
||||
"comment" : "Button to open the value found in browser."
|
||||
},
|
||||
@@ -1018,6 +1069,12 @@
|
||||
"Please choose relays from the list below to filter the current feed:" : {
|
||||
"comment" : "Instructions on how to filter a specific timeline feed by choosing relay servers to filter on."
|
||||
},
|
||||
"Please double-check the checkout web page, or go to the Side Menu → \"Purple\" to check your account status. If you have already paid, but still don't see your account active, please save the URL of the checkout page where you came from, contact our support, and give us the URL to help you with this issue." : {
|
||||
"comment" : "User-facing tips on what to do if a Purple welcome link doesn't work"
|
||||
},
|
||||
"Please try again, check the URL for typos, or contact support for further help." : {
|
||||
"comment" : "User visible error tips"
|
||||
},
|
||||
"Point your camera to a QR code…" : {
|
||||
"comment" : "Text on QR code camera view instructing user to point to QR code"
|
||||
},
|
||||
@@ -1060,6 +1117,9 @@
|
||||
"Profile action sheets allow you to follow, zap, or DM profiles more quickly without having to view their full profile" : {
|
||||
"comment" : "Section footer clarifying what the profile action sheet feature does"
|
||||
},
|
||||
"Profile picture is setup" : {
|
||||
"comment" : "Accessibility value on profile picture image control"
|
||||
},
|
||||
"Profiles" : {
|
||||
"comment" : "Section title for profile view configuration."
|
||||
},
|
||||
@@ -1233,6 +1293,9 @@
|
||||
"Save your login info?" : {
|
||||
"comment" : "Ask user if they want to save their account information."
|
||||
},
|
||||
"Saved" : {
|
||||
"comment" : "Small label indicating that the user's draft has been saved to storage"
|
||||
},
|
||||
"Scan a user's pubkey" : {
|
||||
"comment" : "Text to prompt scanning a QR code of a user's pubkey to open their profile."
|
||||
},
|
||||
@@ -1338,15 +1401,15 @@
|
||||
"Show only from users you follow" : {
|
||||
"comment" : "Setting to Show notifications only associated to users your follow"
|
||||
},
|
||||
"Show only preferred languages on Universe feed" : {
|
||||
"comment" : "Toggle to show notes that are only in the device's preferred languages on the Universe feed and hide notes that are in other languages."
|
||||
},
|
||||
"Show profile action sheets" : {
|
||||
"comment" : "Setting to show profile action sheets when clicking on a user's profile picture"
|
||||
},
|
||||
"Show wallet selector" : {
|
||||
"comment" : "Toggle to show or hide selection of wallet."
|
||||
},
|
||||
"Shows options to edit the image" : {
|
||||
"comment" : "Accessibility hint for a button that edits an image"
|
||||
},
|
||||
"Side menu" : {
|
||||
"comment" : "Accessibility label for the side menu button at the topbar"
|
||||
},
|
||||
@@ -1380,6 +1443,9 @@
|
||||
"Someone zapped you ⚡️" : {
|
||||
"comment" : "Title label for a push notification where someone zapped the user"
|
||||
},
|
||||
"Sorry, but for some reason there has been an issue while trying to crop this image. Please try again later. If the error persists, please contact [Damus support](mailto:support@damus.io)" : {
|
||||
"comment" : "Cropping error message"
|
||||
},
|
||||
"Sorry, this QR code looks incompatible with Damus. Please try another one." : {
|
||||
"comment" : "Text on QR code camera view telling the user a QR is incompatible"
|
||||
},
|
||||
@@ -1482,9 +1548,6 @@
|
||||
"This user cannot be zapped because they have not configured zaps on their account yet. Time to orange-pill?" : {
|
||||
"comment" : "Comment explaining why a user cannot be zapped."
|
||||
},
|
||||
"Thread" : {
|
||||
"comment" : "Navigation bar title for note thread."
|
||||
},
|
||||
"Threads" : {
|
||||
"comment" : "Section header title for a list of threads that are muted."
|
||||
},
|
||||
@@ -1524,6 +1587,9 @@
|
||||
"Truncate timeline text" : {
|
||||
"comment" : "Setting to truncate text in timeline"
|
||||
},
|
||||
"Try checking the link again, your internet connection, whether you need to connect to a specific relay to access this content." : {
|
||||
"comment" : "Tips on what to do if a note cannot be found."
|
||||
},
|
||||
"Type %@ to delete" : {
|
||||
"comment" : "Text field prompt asking user to type DELETE in all caps to confirm that they want to proceed with deleting their account."
|
||||
},
|
||||
@@ -1629,6 +1695,9 @@
|
||||
"We did not detect any issues that we can automatically fix for you. If you are having issues, please contact Damus support: [support@damus.io](mailto:support@damus.io)" : {
|
||||
"comment" : "Message indicating that no First Aid actions are available."
|
||||
},
|
||||
"We were unable to find the note you were looking for." : {
|
||||
"comment" : "Text for the thread view when it is unable to find the note the user is looking for"
|
||||
},
|
||||
"We'll save your account key, so you won't need to enter it manually next time you log in." : {
|
||||
"comment" : "Reminder to user that they should save their account information."
|
||||
},
|
||||
@@ -1680,6 +1749,12 @@
|
||||
"You cannot share content because you are not logged in. Please close this view, log in to your account, and try again." : {
|
||||
"comment" : "Label explaining that sharing cannot proceed because the user is not logged in."
|
||||
},
|
||||
"You clicked on a Purple welcome link, but it does not look like the checkout is completed yet. This is likely a bug." : {
|
||||
"comment" : "Error label upon continuing in the app from a Damus Purple purchase"
|
||||
},
|
||||
"You clicked on a Purple welcome link, but we could not find your checkout. This is likely a bug." : {
|
||||
"comment" : "Error label upon continuing in the app from a Damus Purple purchase"
|
||||
},
|
||||
"You have no bookmarks yet, add them in the context menu" : {
|
||||
"comment" : "Text indicating that there are no bookmarks to be viewed"
|
||||
},
|
||||
@@ -1689,6 +1764,9 @@
|
||||
"Your content is being broadcasted to the network. Please wait." : {
|
||||
"comment" : "Label explaining that their content sharing action is in progress"
|
||||
},
|
||||
"Your draft has been saved to storage" : {
|
||||
"comment" : "Accessibility label indicating that a user's post draft has been saved, meant only for visually impaired users"
|
||||
},
|
||||
"Your Name" : {
|
||||
"comment" : "Label for Your Name section of user profile form."
|
||||
},
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
/* Privacy - Face ID Usage Description */
|
||||
"NSFaceIDUsageDescription" = "Local authentication to access private key";
|
||||
/* Privacy - Microphone Usage Description */
|
||||
"NSMicrophoneUsageDescription" = "Damus needs access to your microphone for creating video recording posts";
|
||||
"NSMicrophoneUsageDescription" = "Damus needs access to your microphone to allow you to create video recordings that you can choose to post publicly on the network";
|
||||
/* Privacy - Photo Library Additions Usage Description */
|
||||
"NSPhotoLibraryAddUsageDescription" = "Granting Damus access to your photos allows you to save images.";
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -209,7 +209,7 @@
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>other</key>
|
||||
<string>คำพูด</string>
|
||||
<string>อ้างอิง</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>sats</key>
|
||||
|
||||
21
damusTests/DraftTests.swift
Normal file
21
damusTests/DraftTests.swift
Normal file
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// DraftTests.swift
|
||||
// damusTests
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2025-01-15
|
||||
|
||||
import XCTest
|
||||
@testable import damus
|
||||
|
||||
class DraftTests: XCTestCase {
|
||||
func testRoundtripNIP37Draft() {
|
||||
let test_note =
|
||||
NostrEvent(
|
||||
content: "Test",
|
||||
keypair: test_keypair_full.to_keypair(),
|
||||
createdAt: UInt32(Date().timeIntervalSince1970 - 100)
|
||||
)!
|
||||
let draft = try! NIP37Draft(unwrapped_note: test_note, draft_id: "test", keypair: test_keypair_full)!
|
||||
XCTAssertEqual(draft.unwrapped_note, test_note)
|
||||
}
|
||||
}
|
||||
23
damusTests/LICENSES
Normal file
23
damusTests/LICENSES
Normal file
@@ -0,0 +1,23 @@
|
||||
Some of the fixtures in this folder are taken from https://github.com/nostr-sdk/nostr-sdk-ios under the MIT license:
|
||||
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2023 Nostr SDK
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
432
damusTests/NIP44v2EncryptionTests.swift
Normal file
432
damusTests/NIP44v2EncryptionTests.swift
Normal file
@@ -0,0 +1,432 @@
|
||||
//
|
||||
// NIP44v2EncryptionTests.swift
|
||||
// damus
|
||||
//
|
||||
// Based on NIP44v2EncryptingTests.swift, taken from https://github.com/nostr-sdk/nostr-sdk-ios under the MIT license:
|
||||
//
|
||||
// MIT License
|
||||
//
|
||||
// Copyright (c) 2023 Nostr SDK
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
//
|
||||
// Adapted by Daniel D’Aquino for damus on 2025-02-10.
|
||||
//
|
||||
import XCTest
|
||||
import CryptoKit
|
||||
@testable import damus
|
||||
|
||||
final class NIP44v2EncryptingTests: XCTestCase {
|
||||
|
||||
private lazy var vectors: NIP44Vectors = try! decodeFixture(filename: "nip44.vectors") // swiftlint:disable:this force_try
|
||||
|
||||
/// Calculate the conversation key from secret key, sec1, and public key, pub2.
|
||||
func testValidConversationKey() throws {
|
||||
let conversationKeyVectors = try XCTUnwrap(vectors.v2.valid.getConversationKey)
|
||||
|
||||
try conversationKeyVectors.forEach { vector in
|
||||
let expectedConversationKey = try XCTUnwrap(vector.conversationKey)
|
||||
let privateKeyA = try XCTUnwrap(Privkey(hex: vector.sec1))
|
||||
let publicKeyB = try XCTUnwrap(Pubkey(hex: vector.pub2))
|
||||
let conversationKeyBytes = try NIP44v2Encryption.conversationKey(
|
||||
privateKeyA: privateKeyA,
|
||||
publicKeyB: publicKeyB
|
||||
).bytes
|
||||
let conversationKey = Data(conversationKeyBytes).hexString
|
||||
XCTAssertEqual(conversationKey, expectedConversationKey)
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate ChaCha key, ChaCha nonce, and HMAC key from conversation key and nonce.
|
||||
func testValidMessageKeys() throws {
|
||||
let messageKeyVectors = try XCTUnwrap(vectors.v2.valid.getMessageKeys)
|
||||
let conversationKey = messageKeyVectors.conversationKey
|
||||
let conversationKeyBytes = try XCTUnwrap(conversationKey.hexDecoded?.bytes)
|
||||
let keys = messageKeyVectors.keys
|
||||
|
||||
try keys.forEach { vector in
|
||||
let nonce = try XCTUnwrap(vector.nonce.hexDecoded)
|
||||
let messageKeys = try NIP44v2Encryption.messageKeys(conversationKey: conversationKeyBytes, nonce: nonce)
|
||||
XCTAssertEqual(messageKeys.chaChaKey.hexString, vector.chaChaKey)
|
||||
XCTAssertEqual(messageKeys.chaChaNonce.hexString, vector.chaChaNonce)
|
||||
XCTAssertEqual(messageKeys.hmacKey.hexString, vector.hmacKey)
|
||||
}
|
||||
}
|
||||
|
||||
/// Take unpadded length (first value), calculate padded length (second value).
|
||||
func testValidCalculatePaddedLength() throws {
|
||||
let calculatePaddedLengthVectors = try XCTUnwrap(vectors.v2.valid.calculatePaddedLength)
|
||||
try calculatePaddedLengthVectors.forEach { vector in
|
||||
XCTAssertEqual(vector.count, 2)
|
||||
let paddedLength = try NIP44v2Encryption.calculatePaddedLength(vector[0])
|
||||
XCTAssertEqual(paddedLength, vector[1])
|
||||
}
|
||||
}
|
||||
|
||||
/// Emulate real conversation with a hardcoded nonce.
|
||||
/// Calculate pub2 from sec2, verify conversation key from (sec1, pub2), encrypt, verify payload.
|
||||
/// Then calculate pub1 from sec1, verify conversation key from (sec2, pub1), decrypt, verify plaintext.
|
||||
func testValidEncryptDecrypt() throws {
|
||||
let encryptDecryptVectors = try XCTUnwrap(vectors.v2.valid.encryptDecrypt)
|
||||
try encryptDecryptVectors.forEach { vector in
|
||||
let sec1 = vector.sec1
|
||||
let sec2 = vector.sec2
|
||||
let expectedConversationKey = vector.conversationKey
|
||||
let nonce = try XCTUnwrap(vector.nonce.hexDecoded)
|
||||
let plaintext = vector.plaintext
|
||||
let payload = vector.payload
|
||||
|
||||
let privateKeyA = try XCTUnwrap(Privkey(hex: vector.sec1))
|
||||
let privateKeyB = try XCTUnwrap(Privkey(hex: vector.sec2))
|
||||
let keypair1 = try XCTUnwrap(FullKeypair(privkey: privateKeyA))
|
||||
let keypair2 = try XCTUnwrap(FullKeypair(privkey: privateKeyB))
|
||||
|
||||
// Conversation key from sec1 and pub2.
|
||||
let conversationKey1Bytes = try NIP44v2Encryption.conversationKey(
|
||||
privateKeyA: keypair1.privkey,
|
||||
publicKeyB: keypair2.pubkey
|
||||
).bytes
|
||||
XCTAssertEqual(expectedConversationKey, Data(conversationKey1Bytes).hexString)
|
||||
|
||||
// Verify payload.
|
||||
let ciphertext = try NIP44v2Encryption.encrypt(
|
||||
plaintext: plaintext,
|
||||
conversationKey: conversationKey1Bytes,
|
||||
nonce: nonce
|
||||
)
|
||||
XCTAssertEqual(payload, ciphertext)
|
||||
|
||||
// Conversation key from sec2 and pub1.
|
||||
let conversationKey2Bytes = try NIP44v2Encryption.conversationKey(
|
||||
privateKeyA: keypair2.privkey,
|
||||
publicKeyB: keypair1.pubkey
|
||||
).bytes
|
||||
XCTAssertEqual(expectedConversationKey, Data(conversationKey2Bytes).hexString)
|
||||
|
||||
// Verify that decrypted data equals the plaintext that we started off with.
|
||||
let decrypted = try NIP44v2Encryption.decrypt(payload: payload, conversationKey: conversationKey2Bytes)
|
||||
XCTAssertEqual(decrypted, plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
/// Same as previous step, but instead of a full plaintext and payload, their checksum is provided.
|
||||
func testValidEncryptDecryptLongMessage() throws {
|
||||
let encryptDecryptVectors = try XCTUnwrap(vectors.v2.valid.encryptDecryptLongMessage)
|
||||
try encryptDecryptVectors.forEach { vector in
|
||||
let conversationKey = vector.conversationKey
|
||||
let conversationKeyData = try XCTUnwrap(conversationKey.hexDecoded)
|
||||
let conversationKeyBytes = conversationKeyData.bytes
|
||||
|
||||
let nonce = try XCTUnwrap(vector.nonce.hexDecoded)
|
||||
let expectedPlaintextSHA256 = vector.plaintextSHA256
|
||||
|
||||
let plaintext = String(repeating: vector.pattern, count: vector.repeatCount)
|
||||
let plaintextData = try XCTUnwrap(plaintext.data(using: .utf8))
|
||||
let plaintextSHA256 = plaintextData.sha256()
|
||||
|
||||
XCTAssertEqual(plaintextSHA256.hexString, expectedPlaintextSHA256)
|
||||
|
||||
let payloadSHA256 = vector.payloadSHA256
|
||||
|
||||
let ciphertext = try NIP44v2Encryption.encrypt(
|
||||
plaintext: plaintext,
|
||||
conversationKey: conversationKeyBytes,
|
||||
nonce: nonce
|
||||
)
|
||||
let ciphertextData = try XCTUnwrap(ciphertext.data(using: .utf8))
|
||||
let ciphertextSHA256 = ciphertextData.sha256().hexString
|
||||
XCTAssertEqual(ciphertextSHA256, payloadSHA256)
|
||||
|
||||
let decrypted = try NIP44v2Encryption.decrypt(payload: ciphertext, conversationKey: conversationKeyBytes)
|
||||
XCTAssertEqual(decrypted, plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
/// Emulate real conversation with only the public encrypt and decrypt functions,
|
||||
/// where the nonce used for encryption is a cryptographically secure pseudorandom generated series of bytes.
|
||||
func testValidEncryptDecryptRandomNonce() throws {
|
||||
let encryptDecryptVectors = try XCTUnwrap(vectors.v2.valid.encryptDecrypt)
|
||||
try encryptDecryptVectors.forEach { vector in
|
||||
let sec1 = vector.sec1
|
||||
let sec2 = vector.sec2
|
||||
let plaintext = vector.plaintext
|
||||
|
||||
let privateKeyA = try XCTUnwrap(Privkey(hex: vector.sec1))
|
||||
let privateKeyB = try XCTUnwrap(Privkey(hex: vector.sec2))
|
||||
|
||||
let keypair1 = try XCTUnwrap(FullKeypair(privkey: privateKeyA))
|
||||
let keypair2 = try XCTUnwrap(FullKeypair(privkey: privateKeyB))
|
||||
|
||||
// Encrypt plaintext with user A's private key and user B's public key.
|
||||
let ciphertext = try NIP44v2Encryption.encrypt(
|
||||
plaintext: plaintext,
|
||||
privateKeyA: keypair1.privkey,
|
||||
publicKeyB: keypair2.pubkey
|
||||
)
|
||||
|
||||
// Decrypt ciphertext with user B's private key and user A's public key.
|
||||
let decrypted = try NIP44v2Encryption.decrypt(payload: ciphertext, privateKeyA: keypair2.privkey, publicKeyB: keypair1.pubkey)
|
||||
XCTAssertEqual(decrypted, plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
/// Encrypting a plaintext message that is not at a minimum of 1 byte and maximum of 65535 bytes must throw an error.
|
||||
func testInvalidEncryptMessageLengths() throws {
|
||||
let encryptMessageLengthsVectors = try XCTUnwrap(vectors.v2.invalid.encryptMessageLengths)
|
||||
try encryptMessageLengthsVectors.forEach { length in
|
||||
let randomBytes = Data.secureRandomBytes(count: 32)
|
||||
XCTAssertThrowsError(try NIP44v2Encryption.encrypt(plaintext: String(repeating: "a", count: length), conversationKey: randomBytes))
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculating conversation key must throw an error.
|
||||
func testInvalidConversationKey() throws {
|
||||
let conversationKeyVectors = try XCTUnwrap(vectors.v2.invalid.getConversationKey)
|
||||
|
||||
try conversationKeyVectors.forEach { vector in
|
||||
let privateKeyA = try XCTUnwrap(Privkey(hex: vector.sec1))
|
||||
let publicKeyB = try XCTUnwrap(Pubkey(hex: vector.pub2))
|
||||
XCTAssertThrowsError(try NIP44v2Encryption.conversationKey(privateKeyA: privateKeyA, publicKeyB: publicKeyB), vector.note ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrypting message content must throw an error
|
||||
func testInvalidDecrypt() throws {
|
||||
let decryptVectors = try XCTUnwrap(vectors.v2.invalid.decrypt)
|
||||
try decryptVectors.forEach { vector in
|
||||
let conversationKey = try XCTUnwrap(vector.conversationKey.hexDecoded).bytes
|
||||
let payload = vector.payload
|
||||
XCTAssertThrowsError(try NIP44v2Encryption.decrypt(payload: payload, conversationKey: conversationKey), vector.note)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
struct NIP44Vectors: Decodable {
|
||||
let v2: NIP44VectorsV2
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case v2
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2: Decodable {
|
||||
let valid: NIP44VectorsV2Valid
|
||||
let invalid: NIP44VectorsV2Invalid
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case valid
|
||||
case invalid
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2Valid: Decodable {
|
||||
let getConversationKey: [NIP44VectorsV2GetConversationKey]
|
||||
let getMessageKeys: NIP44VectorsV2GetMessageKeys
|
||||
let calculatePaddedLength: [[Int]]
|
||||
let encryptDecrypt: [NIP44VectorsV2EncryptDecrypt]
|
||||
let encryptDecryptLongMessage: [NIP44VectorsV2EncryptDecryptLongMessage]
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case getConversationKey = "get_conversation_key"
|
||||
case getMessageKeys = "get_message_keys"
|
||||
case calculatePaddedLength = "calc_padded_len"
|
||||
case encryptDecrypt = "encrypt_decrypt"
|
||||
case encryptDecryptLongMessage = "encrypt_decrypt_long_msg"
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2Invalid: Decodable {
|
||||
let encryptMessageLengths: [Int]
|
||||
let getConversationKey: [NIP44VectorsV2GetConversationKey]
|
||||
let decrypt: [NIP44VectorsDecrypt]
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case encryptMessageLengths = "encrypt_msg_lengths"
|
||||
case getConversationKey = "get_conversation_key"
|
||||
case decrypt
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsDecrypt: Decodable {
|
||||
let conversationKey: String
|
||||
let nonce: String
|
||||
let plaintext: String
|
||||
let payload: String
|
||||
let note: String
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case conversationKey = "conversation_key"
|
||||
case nonce
|
||||
case plaintext
|
||||
case payload
|
||||
case note
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2GetConversationKey: Decodable {
|
||||
let sec1: String
|
||||
let pub2: String
|
||||
let conversationKey: String?
|
||||
let note: String?
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sec1
|
||||
case pub2
|
||||
case conversationKey = "conversation_key"
|
||||
case note
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2GetMessageKeys: Decodable {
|
||||
let conversationKey: String
|
||||
let keys: [NIP44VectorsV2MessageKeys]
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case conversationKey = "conversation_key"
|
||||
case keys
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2MessageKeys: Decodable {
|
||||
let nonce: String
|
||||
let chaChaKey: String
|
||||
let chaChaNonce: String
|
||||
let hmacKey: String
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case nonce
|
||||
case chaChaKey = "chacha_key"
|
||||
case chaChaNonce = "chacha_nonce"
|
||||
case hmacKey = "hmac_key"
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2EncryptDecrypt: Decodable {
|
||||
let sec1: String
|
||||
let sec2: String
|
||||
let conversationKey: String
|
||||
let nonce: String
|
||||
let plaintext: String
|
||||
let payload: String
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sec1
|
||||
case sec2
|
||||
case conversationKey = "conversation_key"
|
||||
case nonce
|
||||
case plaintext
|
||||
case payload
|
||||
}
|
||||
}
|
||||
|
||||
struct NIP44VectorsV2EncryptDecryptLongMessage: Decodable {
|
||||
let conversationKey: String
|
||||
let nonce: String
|
||||
let pattern: String
|
||||
let repeatCount: Int
|
||||
let plaintextSHA256: String
|
||||
let payloadSHA256: String
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case conversationKey = "conversation_key"
|
||||
case nonce
|
||||
case pattern
|
||||
case repeatCount = "repeat"
|
||||
case plaintextSHA256 = "plaintext_sha256"
|
||||
case payloadSHA256 = "payload_sha256"
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension Data {
|
||||
var hexString: String {
|
||||
let hexDigits = Array("0123456789abcdef".utf16)
|
||||
var hexChars = [UTF16.CodeUnit]()
|
||||
hexChars.reserveCapacity(bytes.count * 2)
|
||||
|
||||
for byte in self {
|
||||
let (index1, index2) = Int(byte).quotientAndRemainder(dividingBy: 16)
|
||||
hexChars.append(hexDigits[index1])
|
||||
hexChars.append(hexDigits[index2])
|
||||
}
|
||||
|
||||
return String(utf16CodeUnits: hexChars, count: hexChars.count)
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
var hexDecoded: Data? {
|
||||
guard self.count.isMultiple(of: 2) else { return nil }
|
||||
|
||||
// https://stackoverflow.com/a/62517446/982195
|
||||
let stringArray = Array(self)
|
||||
var data = Data()
|
||||
for i in stride(from: 0, to: count, by: 2) {
|
||||
let pair = String(stringArray[i]) + String(stringArray[i + 1])
|
||||
if let byteNum = UInt8(pair, radix: 16) {
|
||||
let byte = Data([byteNum])
|
||||
data.append(byte)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
extension NIP44v2EncryptingTests {
|
||||
func loadFixtureString(_ filename: String) throws -> String? {
|
||||
let data = try self.loadFixtureData(filename)
|
||||
|
||||
guard let originalString = String(data: data, encoding: .utf8) else {
|
||||
throw FixtureLoadingError.decodingError
|
||||
}
|
||||
|
||||
let trimmedString = originalString.filter { !"\n\t\r".contains($0) }
|
||||
return trimmedString
|
||||
}
|
||||
|
||||
func loadFixtureData(_ filename: String) throws -> Data {
|
||||
guard let bundleData = try? readBundleFile(name: filename, ext: "json") else {
|
||||
throw FixtureLoadingError.missingFile
|
||||
}
|
||||
return bundleData
|
||||
}
|
||||
|
||||
func decodeFixture<T: Decodable>(filename: String) throws -> T {
|
||||
let data = try self.loadFixtureData(filename)
|
||||
return try JSONDecoder().decode(T.self, from: data)
|
||||
}
|
||||
|
||||
func readBundleFile(name: String, ext: String) throws -> Data {
|
||||
let bundle = Bundle(for: type(of: self))
|
||||
guard let fileURL = bundle.url(forResource: name, withExtension: ext) else {
|
||||
throw CocoaError(.fileReadNoSuchFile)
|
||||
}
|
||||
|
||||
return try Data(contentsOf: fileURL)
|
||||
}
|
||||
|
||||
enum FixtureLoadingError: Error {
|
||||
case missingFile
|
||||
case decodingError
|
||||
}
|
||||
}
|
||||
648
damusTests/nip44.vectors.json
Normal file
648
damusTests/nip44.vectors.json
Normal file
@@ -0,0 +1,648 @@
|
||||
{
|
||||
"v2": {
|
||||
"valid": {
|
||||
"get_conversation_key": [
|
||||
{
|
||||
"sec1": "315e59ff51cb9209768cf7da80791ddcaae56ac9775eb25b6dee1234bc5d2268",
|
||||
"pub2": "c2f9d9948dc8c7c38321e4b85c8558872eafa0641cd269db76848a6073e69133",
|
||||
"conversation_key": "3dfef0ce2a4d80a25e7a328accf73448ef67096f65f79588e358d9a0eb9013f1"
|
||||
},
|
||||
{
|
||||
"sec1": "a1e37752c9fdc1273be53f68c5f74be7c8905728e8de75800b94262f9497c86e",
|
||||
"pub2": "03bb7947065dde12ba991ea045132581d0954f042c84e06d8c00066e23c1a800",
|
||||
"conversation_key": "4d14f36e81b8452128da64fe6f1eae873baae2f444b02c950b90e43553f2178b"
|
||||
},
|
||||
{
|
||||
"sec1": "98a5902fd67518a0c900f0fb62158f278f94a21d6f9d33d30cd3091195500311",
|
||||
"pub2": "aae65c15f98e5e677b5050de82e3aba47a6fe49b3dab7863cf35d9478ba9f7d1",
|
||||
"conversation_key": "9c00b769d5f54d02bf175b7284a1cbd28b6911b06cda6666b2243561ac96bad7"
|
||||
},
|
||||
{
|
||||
"sec1": "86ae5ac8034eb2542ce23ec2f84375655dab7f836836bbd3c54cefe9fdc9c19f",
|
||||
"pub2": "59f90272378089d73f1339710c02e2be6db584e9cdbe86eed3578f0c67c23585",
|
||||
"conversation_key": "19f934aafd3324e8415299b64df42049afaa051c71c98d0aa10e1081f2e3e2ba"
|
||||
},
|
||||
{
|
||||
"sec1": "2528c287fe822421bc0dc4c3615878eb98e8a8c31657616d08b29c00ce209e34",
|
||||
"pub2": "f66ea16104c01a1c532e03f166c5370a22a5505753005a566366097150c6df60",
|
||||
"conversation_key": "c833bbb292956c43366145326d53b955ffb5da4e4998a2d853611841903f5442"
|
||||
},
|
||||
{
|
||||
"sec1": "49808637b2d21129478041813aceb6f2c9d4929cd1303cdaf4fbdbd690905ff2",
|
||||
"pub2": "74d2aab13e97827ea21baf253ad7e39b974bb2498cc747cdb168582a11847b65",
|
||||
"conversation_key": "4bf304d3c8c4608864c0fe03890b90279328cd24a018ffa9eb8f8ccec06b505d"
|
||||
},
|
||||
{
|
||||
"sec1": "af67c382106242c5baabf856efdc0629cc1c5b4061f85b8ceaba52aa7e4b4082",
|
||||
"pub2": "bdaf0001d63e7ec994fad736eab178ee3c2d7cfc925ae29f37d19224486db57b",
|
||||
"conversation_key": "a3a575dd66d45e9379904047ebfb9a7873c471687d0535db00ef2daa24b391db"
|
||||
},
|
||||
{
|
||||
"sec1": "0e44e2d1db3c1717b05ffa0f08d102a09c554a1cbbf678ab158b259a44e682f1",
|
||||
"pub2": "1ffa76c5cc7a836af6914b840483726207cb750889753d7499fb8b76aa8fe0de",
|
||||
"conversation_key": "a39970a667b7f861f100e3827f4adbf6f464e2697686fe1a81aeda817d6b8bdf"
|
||||
},
|
||||
{
|
||||
"sec1": "5fc0070dbd0666dbddc21d788db04050b86ed8b456b080794c2a0c8e33287bb6",
|
||||
"pub2": "31990752f296dd22e146c9e6f152a269d84b241cc95bb3ff8ec341628a54caf0",
|
||||
"conversation_key": "72c21075f4b2349ce01a3e604e02a9ab9f07e35dd07eff746de348b4f3c6365e"
|
||||
},
|
||||
{
|
||||
"sec1": "1b7de0d64d9b12ddbb52ef217a3a7c47c4362ce7ea837d760dad58ab313cba64",
|
||||
"pub2": "24383541dd8083b93d144b431679d70ef4eec10c98fceef1eff08b1d81d4b065",
|
||||
"conversation_key": "dd152a76b44e63d1afd4dfff0785fa07b3e494a9e8401aba31ff925caeb8f5b1"
|
||||
},
|
||||
{
|
||||
"sec1": "df2f560e213ca5fb33b9ecde771c7c0cbd30f1cf43c2c24de54480069d9ab0af",
|
||||
"pub2": "eeea26e552fc8b5e377acaa03e47daa2d7b0c787fac1e0774c9504d9094c430e",
|
||||
"conversation_key": "770519e803b80f411c34aef59c3ca018608842ebf53909c48d35250bd9323af6"
|
||||
},
|
||||
{
|
||||
"sec1": "cffff919fcc07b8003fdc63bc8a00c0f5dc81022c1c927c62c597352190d95b9",
|
||||
"pub2": "eb5c3cca1a968e26684e5b0eb733aecfc844f95a09ac4e126a9e58a4e4902f92",
|
||||
"conversation_key": "46a14ee7e80e439ec75c66f04ad824b53a632b8409a29bbb7c192e43c00bb795"
|
||||
},
|
||||
{
|
||||
"sec1": "64ba5a685e443e881e9094647ddd32db14444bb21aa7986beeba3d1c4673ba0a",
|
||||
"pub2": "50e6a4339fac1f3bf86f2401dd797af43ad45bbf58e0801a7877a3984c77c3c4",
|
||||
"conversation_key": "968b9dbbfcede1664a4ca35a5d3379c064736e87aafbf0b5d114dff710b8a946"
|
||||
},
|
||||
{
|
||||
"sec1": "dd0c31ccce4ec8083f9b75dbf23cc2878e6d1b6baa17713841a2428f69dee91a",
|
||||
"pub2": "b483e84c1339812bed25be55cff959778dfc6edde97ccd9e3649f442472c091b",
|
||||
"conversation_key": "09024503c7bde07eb7865505891c1ea672bf2d9e25e18dd7a7cea6c69bf44b5d"
|
||||
},
|
||||
{
|
||||
"sec1": "af71313b0d95c41e968a172b33ba5ebd19d06cdf8a7a98df80ecf7af4f6f0358",
|
||||
"pub2": "2a5c25266695b461ee2af927a6c44a3c598b8095b0557e9bd7f787067435bc7c",
|
||||
"conversation_key": "fe5155b27c1c4b4e92a933edae23726a04802a7cc354a77ac273c85aa3c97a92"
|
||||
},
|
||||
{
|
||||
"sec1": "6636e8a389f75fe068a03b3edb3ea4a785e2768e3f73f48ffb1fc5e7cb7289dc",
|
||||
"pub2": "514eb2064224b6a5829ea21b6e8f7d3ea15ff8e70e8555010f649eb6e09aec70",
|
||||
"conversation_key": "ff7afacd4d1a6856d37ca5b546890e46e922b508639214991cf8048ddbe9745c"
|
||||
},
|
||||
{
|
||||
"sec1": "94b212f02a3cfb8ad147d52941d3f1dbe1753804458e6645af92c7b2ea791caa",
|
||||
"pub2": "f0cac333231367a04b652a77ab4f8d658b94e86b5a8a0c472c5c7b0d4c6a40cc",
|
||||
"conversation_key": "e292eaf873addfed0a457c6bd16c8effde33d6664265697f69f420ab16f6669b"
|
||||
},
|
||||
{
|
||||
"sec1": "aa61f9734e69ae88e5d4ced5aae881c96f0d7f16cca603d3bed9eec391136da6",
|
||||
"pub2": "4303e5360a884c360221de8606b72dd316da49a37fe51e17ada4f35f671620a6",
|
||||
"conversation_key": "8e7d44fd4767456df1fb61f134092a52fcd6836ebab3b00766e16732683ed848"
|
||||
},
|
||||
{
|
||||
"sec1": "5e914bdac54f3f8e2cba94ee898b33240019297b69e96e70c8a495943a72fc98",
|
||||
"pub2": "5bd097924f606695c59f18ff8fd53c174adbafaaa71b3c0b4144a3e0a474b198",
|
||||
"conversation_key": "f5a0aecf2984bf923c8cd5e7bb8be262d1a8353cb93959434b943a07cf5644bc"
|
||||
},
|
||||
{
|
||||
"sec1": "8b275067add6312ddee064bcdbeb9d17e88aa1df36f430b2cea5cc0413d8278a",
|
||||
"pub2": "65bbbfca819c90c7579f7a82b750a18c858db1afbec8f35b3c1e0e7b5588e9b8",
|
||||
"conversation_key": "2c565e7027eb46038c2263563d7af681697107e975e9914b799d425effd248d6"
|
||||
},
|
||||
{
|
||||
"sec1": "1ac848de312285f85e0f7ec208aac20142a1f453402af9b34ec2ec7a1f9c96fc",
|
||||
"pub2": "45f7318fe96034d23ee3ddc25b77f275cc1dd329664dd51b89f89c4963868e41",
|
||||
"conversation_key": "b56e970e5057a8fd929f8aad9248176b9af87819a708d9ddd56e41d1aec74088"
|
||||
},
|
||||
{
|
||||
"sec1": "295a1cf621de401783d29d0e89036aa1c62d13d9ad307161b4ceb535ba1b40e6",
|
||||
"pub2": "840115ddc7f1034d3b21d8e2103f6cb5ab0b63cf613f4ea6e61ae3d016715cdd",
|
||||
"conversation_key": "b4ee9c0b9b9fef88975773394f0a6f981ca016076143a1bb575b9ff46e804753"
|
||||
},
|
||||
{
|
||||
"sec1": "a28eed0fe977893856ab9667e06ace39f03abbcdb845c329a1981be438ba565d",
|
||||
"pub2": "b0f38b950a5013eba5ab4237f9ed29204a59f3625c71b7e210fec565edfa288c",
|
||||
"conversation_key": "9d3a802b45bc5aeeb3b303e8e18a92ddd353375710a31600d7f5fff8f3a7285b"
|
||||
},
|
||||
{
|
||||
"sec1": "7ab65af72a478c05f5c651bdc4876c74b63d20d04cdbf71741e46978797cd5a4",
|
||||
"pub2": "f1112159161b568a9cb8c9dd6430b526c4204bcc8ce07464b0845b04c041beda",
|
||||
"conversation_key": "943884cddaca5a3fef355e9e7f08a3019b0b66aa63ec90278b0f9fdb64821e79"
|
||||
},
|
||||
{
|
||||
"sec1": "95c79a7b75ba40f2229e85756884c138916f9d103fc8f18acc0877a7cceac9fe",
|
||||
"pub2": "cad76bcbd31ca7bbda184d20cc42f725ed0bb105b13580c41330e03023f0ffb3",
|
||||
"conversation_key": "81c0832a669eea13b4247c40be51ccfd15bb63fcd1bba5b4530ce0e2632f301b"
|
||||
},
|
||||
{
|
||||
"sec1": "baf55cc2febd4d980b4b393972dfc1acf49541e336b56d33d429bce44fa12ec9",
|
||||
"pub2": "0c31cf87fe565766089b64b39460ebbfdedd4a2bc8379be73ad3c0718c912e18",
|
||||
"conversation_key": "37e2344da9ecdf60ae2205d81e89d34b280b0a3f111171af7e4391ded93b8ea6"
|
||||
},
|
||||
{
|
||||
"sec1": "6eeec45acd2ed31693c5256026abf9f072f01c4abb61f51cf64e6956b6dc8907",
|
||||
"pub2": "e501b34ed11f13d816748c0369b0c728e540df3755bab59ed3327339e16ff828",
|
||||
"conversation_key": "afaa141b522ddb27bb880d768903a7f618bb8b6357728cae7fb03af639b946e6"
|
||||
},
|
||||
{
|
||||
"sec1": "261a076a9702af1647fb343c55b3f9a4f1096273002287df0015ba81ce5294df",
|
||||
"pub2": "b2777c863878893ae100fb740c8fab4bebd2bf7be78c761a75593670380a6112",
|
||||
"conversation_key": "76f8d2853de0734e51189ced523c09427c3e46338b9522cd6f74ef5e5b475c74"
|
||||
},
|
||||
{
|
||||
"sec1": "ed3ec71ca406552ea41faec53e19f44b8f90575eda4b7e96380f9cc73c26d6f3",
|
||||
"pub2": "86425951e61f94b62e20cae24184b42e8e17afcf55bafa58645efd0172624fae",
|
||||
"conversation_key": "f7ffc520a3a0e9e9b3c0967325c9bf12707f8e7a03f28b6cd69ae92cf33f7036"
|
||||
},
|
||||
{
|
||||
"sec1": "5a788fc43378d1303ac78639c59a58cb88b08b3859df33193e63a5a3801c722e",
|
||||
"pub2": "a8cba2f87657d229db69bee07850fd6f7a2ed070171a06d006ec3a8ac562cf70",
|
||||
"conversation_key": "7d705a27feeedf78b5c07283362f8e361760d3e9f78adab83e3ae5ce7aeb6409"
|
||||
},
|
||||
{
|
||||
"sec1": "63bffa986e382b0ac8ccc1aa93d18a7aa445116478be6f2453bad1f2d3af2344",
|
||||
"pub2": "b895c70a83e782c1cf84af558d1038e6b211c6f84ede60408f519a293201031d",
|
||||
"conversation_key": "3a3b8f00d4987fc6711d9be64d9c59cf9a709c6c6481c2cde404bcc7a28f174e"
|
||||
},
|
||||
{
|
||||
"sec1": "e4a8bcacbf445fd3721792b939ff58e691cdcba6a8ba67ac3467b45567a03e5c",
|
||||
"pub2": "b54053189e8c9252c6950059c783edb10675d06d20c7b342f73ec9fa6ed39c9d",
|
||||
"conversation_key": "7b3933b4ef8189d347169c7955589fc1cfc01da5239591a08a183ff6694c44ad"
|
||||
},
|
||||
{
|
||||
"sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364139",
|
||||
"pub2": "0000000000000000000000000000000000000000000000000000000000000002",
|
||||
"conversation_key": "8b6392dbf2ec6a2b2d5b1477fc2be84d63ef254b667cadd31bd3f444c44ae6ba",
|
||||
"note": "sec1 = n-2, pub2: random, 0x02"
|
||||
},
|
||||
{
|
||||
"sec1": "0000000000000000000000000000000000000000000000000000000000000002",
|
||||
"pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdeb",
|
||||
"conversation_key": "be234f46f60a250bef52a5ee34c758800c4ca8e5030bf4cc1a31d37ba2104d43",
|
||||
"note": "sec1 = 2, pub2: rand"
|
||||
},
|
||||
{
|
||||
"sec1": "0000000000000000000000000000000000000000000000000000000000000001",
|
||||
"pub2": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
|
||||
"conversation_key": "3b4610cb7189beb9cc29eb3716ecc6102f1247e8f3101a03a1787d8908aeb54e",
|
||||
"note": "sec1 == pub2"
|
||||
}
|
||||
],
|
||||
"get_message_keys": {
|
||||
"conversation_key": "a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54",
|
||||
"keys": [
|
||||
{
|
||||
"nonce": "e1e6f880560d6d149ed83dcc7e5861ee62a5ee051f7fde9975fe5d25d2a02d72",
|
||||
"chacha_key": "f145f3bed47cb70dbeaac07f3a3fe683e822b3715edb7c4fe310829014ce7d76",
|
||||
"chacha_nonce": "c4ad129bb01180c0933a160c",
|
||||
"hmac_key": "027c1db445f05e2eee864a0975b0ddef5b7110583c8c192de3732571ca5838c4"
|
||||
},
|
||||
{
|
||||
"nonce": "e1d6d28c46de60168b43d79dacc519698512ec35e8ccb12640fc8e9f26121101",
|
||||
"chacha_key": "e35b88f8d4a8f1606c5082f7a64b100e5d85fcdb2e62aeafbec03fb9e860ad92",
|
||||
"chacha_nonce": "22925e920cee4a50a478be90",
|
||||
"hmac_key": "46a7c55d4283cb0df1d5e29540be67abfe709e3b2e14b7bf9976e6df994ded30"
|
||||
},
|
||||
{
|
||||
"nonce": "cfc13bef512ac9c15951ab00030dfaf2626fdca638dedb35f2993a9eeb85d650",
|
||||
"chacha_key": "020783eb35fdf5b80ef8c75377f4e937efb26bcbad0e61b4190e39939860c4bf",
|
||||
"chacha_nonce": "d3594987af769a52904656ac",
|
||||
"hmac_key": "237ec0ccb6ebd53d179fa8fd319e092acff599ef174c1fdafd499ef2b8dee745"
|
||||
},
|
||||
{
|
||||
"nonce": "ea6eb84cac23c5c1607c334e8bdf66f7977a7e374052327ec28c6906cbe25967",
|
||||
"chacha_key": "ff68db24b34fa62c78ac5ffeeaf19533afaedf651fb6a08384e46787f6ce94be",
|
||||
"chacha_nonce": "50bb859aa2dde938cc49ec7a",
|
||||
"hmac_key": "06ff32e1f7b29753a727d7927b25c2dd175aca47751462d37a2039023ec6b5a6"
|
||||
},
|
||||
{
|
||||
"nonce": "8c2e1dd3792802f1f9f7842e0323e5d52ad7472daf360f26e15f97290173605d",
|
||||
"chacha_key": "2f9daeda8683fdeede81adac247c63cc7671fa817a1fd47352e95d9487989d8b",
|
||||
"chacha_nonce": "400224ba67fc2f1b76736916",
|
||||
"hmac_key": "465c05302aeeb514e41c13ed6405297e261048cfb75a6f851ffa5b445b746e4b"
|
||||
},
|
||||
{
|
||||
"nonce": "05c28bf3d834fa4af8143bf5201a856fa5fac1a3aee58f4c93a764fc2f722367",
|
||||
"chacha_key": "1e3d45777025a035be566d80fd580def73ed6f7c043faec2c8c1c690ad31c110",
|
||||
"chacha_nonce": "021905b1ea3afc17cb9bf96f",
|
||||
"hmac_key": "74a6e481a89dcd130aaeb21060d7ec97ad30f0007d2cae7b1b11256cc70dfb81"
|
||||
},
|
||||
{
|
||||
"nonce": "5e043fb153227866e75a06d60185851bc90273bfb93342f6632a728e18a07a17",
|
||||
"chacha_key": "1ea72c9293841e7737c71567d8120145a58991aaa1c436ef77bf7adb83f882f1",
|
||||
"chacha_nonce": "72f69a5a5f795465cee59da8",
|
||||
"hmac_key": "e9daa1a1e9a266ecaa14e970a84bce3fbbf329079bbccda626582b4e66a0d4c9"
|
||||
},
|
||||
{
|
||||
"nonce": "7be7338eaf06a87e274244847fe7a97f5c6a91f44adc18fcc3e411ad6f786dbf",
|
||||
"chacha_key": "881e7968a1f0c2c80742ee03cd49ea587e13f22699730f1075ade01931582bf6",
|
||||
"chacha_nonce": "6e69be92d61c04a276021565",
|
||||
"hmac_key": "901afe79e74b19967c8829af23617d7d0ffbf1b57190c096855c6a03523a971b"
|
||||
},
|
||||
{
|
||||
"nonce": "94571c8d590905bad7becd892832b472f2aa5212894b6ce96e5ba719c178d976",
|
||||
"chacha_key": "f80873dd48466cb12d46364a97b8705c01b9b4230cb3ec3415a6b9551dc42eef",
|
||||
"chacha_nonce": "3dda53569cfcb7fac1805c35",
|
||||
"hmac_key": "e9fc264345e2839a181affebc27d2f528756e66a5f87b04bf6c5f1997047051e"
|
||||
},
|
||||
{
|
||||
"nonce": "13a6ee974b1fd759135a2c2010e3cdda47081c78e771125e4f0c382f0284a8cb",
|
||||
"chacha_key": "bc5fb403b0bed0d84cf1db872b6522072aece00363178c98ad52178d805fca85",
|
||||
"chacha_nonce": "65064239186e50304cc0f156",
|
||||
"hmac_key": "e872d320dde4ed3487958a8e43b48aabd3ced92bc24bb8ff1ccb57b590d9701a"
|
||||
},
|
||||
{
|
||||
"nonce": "082fecdb85f358367b049b08be0e82627ae1d8edb0f27327ccb593aa2613b814",
|
||||
"chacha_key": "1fbdb1cf6f6ea816349baf697932b36107803de98fcd805ebe9849b8ad0e6a45",
|
||||
"chacha_nonce": "2e605e1d825a3eaeb613db9c",
|
||||
"hmac_key": "fae910f591cf3c7eb538c598583abad33bc0a03085a96ca4ea3a08baf17c0eec"
|
||||
},
|
||||
{
|
||||
"nonce": "4c19020c74932c30ec6b2d8cd0d5bb80bd0fc87da3d8b4859d2fb003810afd03",
|
||||
"chacha_key": "1ab9905a0189e01cda82f843d226a82a03c4f5b6dbea9b22eb9bc953ba1370d4",
|
||||
"chacha_nonce": "cbb2530ea653766e5a37a83a",
|
||||
"hmac_key": "267f68acac01ac7b34b675e36c2cef5e7b7a6b697214add62a491bedd6efc178"
|
||||
},
|
||||
{
|
||||
"nonce": "67723a3381497b149ce24814eddd10c4c41a1e37e75af161930e6b9601afd0ff",
|
||||
"chacha_key": "9ecbd25e7e2e6c97b8c27d376dcc8c5679da96578557e4e21dba3a7ef4e4ac07",
|
||||
"chacha_nonce": "ef649fcf335583e8d45e3c2e",
|
||||
"hmac_key": "04dbbd812fa8226fdb45924c521a62e3d40a9e2b5806c1501efdeba75b006bf1"
|
||||
},
|
||||
{
|
||||
"nonce": "42063fe80b093e8619b1610972b4c3ab9e76c14fd908e642cd4997cafb30f36c",
|
||||
"chacha_key": "211c66531bbcc0efcdd0130f9f1ebc12a769105eb39608994bcb188fa6a73a4a",
|
||||
"chacha_nonce": "67803605a7e5010d0f63f8c8",
|
||||
"hmac_key": "e840e4e8921b57647369d121c5a19310648105dbdd008200ebf0d3b668704ff8"
|
||||
},
|
||||
{
|
||||
"nonce": "b5ac382a4be7ac03b554fe5f3043577b47ea2cd7cfc7e9ca010b1ffbb5cf1a58",
|
||||
"chacha_key": "b3b5f14f10074244ee42a3837a54309f33981c7232a8b16921e815e1f7d1bb77",
|
||||
"chacha_nonce": "4e62a0073087ed808be62469",
|
||||
"hmac_key": "c8efa10230b5ea11633816c1230ca05fa602ace80a7598916d83bae3d3d2ccd7"
|
||||
},
|
||||
{
|
||||
"nonce": "e9d1eba47dd7e6c1532dc782ff63125db83042bb32841db7eeafd528f3ea7af9",
|
||||
"chacha_key": "54241f68dc2e50e1db79e892c7c7a471856beeb8d51b7f4d16f16ab0645d2f1a",
|
||||
"chacha_nonce": "a963ed7dc29b7b1046820a1d",
|
||||
"hmac_key": "aba215c8634530dc21c70ddb3b3ee4291e0fa5fa79be0f85863747bde281c8b2"
|
||||
},
|
||||
{
|
||||
"nonce": "a94ecf8efeee9d7068de730fad8daf96694acb70901d762de39fa8a5039c3c49",
|
||||
"chacha_key": "c0565e9e201d2381a2368d7ffe60f555223874610d3d91fbbdf3076f7b1374dd",
|
||||
"chacha_nonce": "329bb3024461e84b2e1c489b",
|
||||
"hmac_key": "ac42445491f092481ce4fa33b1f2274700032db64e3a15014fbe8c28550f2fec"
|
||||
},
|
||||
{
|
||||
"nonce": "533605ea214e70c25e9a22f792f4b78b9f83a18ab2103687c8a0075919eaaa53",
|
||||
"chacha_key": "ab35a5e1e54d693ff023db8500d8d4e79ad8878c744e0eaec691e96e141d2325",
|
||||
"chacha_nonce": "653d759042b85194d4d8c0a7",
|
||||
"hmac_key": "b43628e37ba3c31ce80576f0a1f26d3a7c9361d29bb227433b66f49d44f167ba"
|
||||
},
|
||||
{
|
||||
"nonce": "7f38df30ceea1577cb60b355b4f5567ff4130c49e84fed34d779b764a9cc184c",
|
||||
"chacha_key": "a37d7f211b84a551a127ff40908974eb78415395d4f6f40324428e850e8c42a3",
|
||||
"chacha_nonce": "b822e2c959df32b3cb772a7c",
|
||||
"hmac_key": "1ba31764f01f69b5c89ded2d7c95828e8052c55f5d36f1cd535510d61ba77420"
|
||||
},
|
||||
{
|
||||
"nonce": "11b37f9dbc4d0185d1c26d5f4ed98637d7c9701fffa65a65839fa4126573a4e5",
|
||||
"chacha_key": "964f38d3a31158a5bfd28481247b18dd6e44d69f30ba2a40f6120c6d21d8a6ba",
|
||||
"chacha_nonce": "5f72c5b87c590bcd0f93b305",
|
||||
"hmac_key": "2fc4553e7cedc47f29690439890f9f19c1077ef3e9eaeef473d0711e04448918"
|
||||
},
|
||||
{
|
||||
"nonce": "8be790aa483d4cdd843189f71f135b3ec7e31f381312c8fe9f177aab2a48eafa",
|
||||
"chacha_key": "95c8c74d633721a131316309cf6daf0804d59eaa90ea998fc35bac3d2fbb7a94",
|
||||
"chacha_nonce": "409a7654c0e4bf8c2c6489be",
|
||||
"hmac_key": "21bb0b06eb2b460f8ab075f497efa9a01c9cf9146f1e3986c3bf9da5689b6dc4"
|
||||
},
|
||||
{
|
||||
"nonce": "19fd2a718ea084827d6bd73f509229ddf856732108b59fc01819f611419fd140",
|
||||
"chacha_key": "cc6714b9f5616c66143424e1413d520dae03b1a4bd202b82b0a89b0727f5cdc8",
|
||||
"chacha_nonce": "1b7fd2534f015a8f795d8f32",
|
||||
"hmac_key": "2bef39c4ce5c3c59b817e86351373d1554c98bc131c7e461ed19d96cfd6399a0"
|
||||
},
|
||||
{
|
||||
"nonce": "3c2acd893952b2f6d07d8aea76f545ca45961a93fe5757f6a5a80811d5e0255d",
|
||||
"chacha_key": "c8de6c878cb469278d0af894bc181deb6194053f73da5014c2b5d2c8db6f2056",
|
||||
"chacha_nonce": "6ffe4f1971b904a1b1a81b99",
|
||||
"hmac_key": "df1cd69dd3646fca15594284744d4211d70e7d8472e545d276421fbb79559fd4"
|
||||
},
|
||||
{
|
||||
"nonce": "7dbea4cead9ac91d4137f1c0a6eebb6ba0d1fb2cc46d829fbc75f8d86aca6301",
|
||||
"chacha_key": "c8e030f6aa680c3d0b597da9c92bb77c21c4285dd620c5889f9beba7446446b0",
|
||||
"chacha_nonce": "a9b5a67d081d3b42e737d16f",
|
||||
"hmac_key": "355a85f551bc3cce9a14461aa60994742c9bbb1c81a59ca102dc64e61726ab8e"
|
||||
},
|
||||
{
|
||||
"nonce": "45422e676cdae5f1071d3647d7a5f1f5adafb832668a578228aa1155a491f2f3",
|
||||
"chacha_key": "758437245f03a88e2c6a32807edfabff51a91c81ca2f389b0b46f2c97119ea90",
|
||||
"chacha_nonce": "263830a065af33d9c6c5aa1f",
|
||||
"hmac_key": "7c581cf3489e2de203a95106bfc0de3d4032e9d5b92b2b61fb444acd99037e17"
|
||||
},
|
||||
{
|
||||
"nonce": "babc0c03fad24107ad60678751f5db2678041ff0d28671ede8d65bdf7aa407e9",
|
||||
"chacha_key": "bd68a28bd48d9ffa3602db72c75662ac2848a0047a313d2ae2d6bc1ac153d7e9",
|
||||
"chacha_nonce": "d0f9d2a1ace6c758f594ffdd",
|
||||
"hmac_key": "eb435e3a642adfc9d59813051606fc21f81641afd58ea6641e2f5a9f123bb50a"
|
||||
},
|
||||
{
|
||||
"nonce": "7a1b8aac37d0d20b160291fad124ab697cfca53f82e326d78fef89b4b0ea8f83",
|
||||
"chacha_key": "9e97875b651a1d30d17d086d1e846778b7faad6fcbc12e08b3365d700f62e4fe",
|
||||
"chacha_nonce": "ccdaad5b3b7645be430992eb",
|
||||
"hmac_key": "6f2f55cf35174d75752f63c06cc7cbc8441759b142999ed2d5a6d09d263e1fc4"
|
||||
},
|
||||
{
|
||||
"nonce": "8370e4e32d7e680a83862cab0da6136ef607014d043e64cdf5ecc0c4e20b3d9a",
|
||||
"chacha_key": "1472bed5d19db9c546106de946e0649cd83cc9d4a66b087a65906e348dcf92e2",
|
||||
"chacha_nonce": "ed02dece5fc3a186f123420b",
|
||||
"hmac_key": "7b3f7739f49d30c6205a46b174f984bb6a9fc38e5ccfacef2dac04fcbd3b184e"
|
||||
},
|
||||
{
|
||||
"nonce": "9f1c5e8a29cd5677513c2e3a816551d6833ee54991eb3f00d5b68096fc8f0183",
|
||||
"chacha_key": "5e1a7544e4d4dafe55941fcbdf326f19b0ca37fc49c4d47e9eec7fb68cde4975",
|
||||
"chacha_nonce": "7d9acb0fdc174e3c220f40de",
|
||||
"hmac_key": "e265ab116fbbb86b2aefc089a0986a0f5b77eda50c7410404ad3b4f3f385c7a7"
|
||||
},
|
||||
{
|
||||
"nonce": "c385aa1c37c2bfd5cc35fcdbdf601034d39195e1cabff664ceb2b787c15d0225",
|
||||
"chacha_key": "06bf4e60677a13e54c4a38ab824d2ef79da22b690da2b82d0aa3e39a14ca7bdd",
|
||||
"chacha_nonce": "26b450612ca5e905b937e147",
|
||||
"hmac_key": "22208152be2b1f5f75e6bfcc1f87763d48bb7a74da1be3d102096f257207f8b3"
|
||||
},
|
||||
{
|
||||
"nonce": "3ff73528f88a50f9d35c0ddba4560bacee5b0462d0f4cb6e91caf41847040ce4",
|
||||
"chacha_key": "850c8a17a23aa761d279d9901015b2bbdfdff00adbf6bc5cf22bd44d24ecabc9",
|
||||
"chacha_nonce": "4a296a1fb0048e5020d3b129",
|
||||
"hmac_key": "b1bf49a533c4da9b1d629b7ff30882e12d37d49c19abd7b01b7807d75ee13806"
|
||||
},
|
||||
{
|
||||
"nonce": "2dcf39b9d4c52f1cb9db2d516c43a7c6c3b8c401f6a4ac8f131a9e1059957036",
|
||||
"chacha_key": "17f8057e6156ba7cc5310d01eda8c40f9aa388f9fd1712deb9511f13ecc37d27",
|
||||
"chacha_nonce": "a8188daff807a1182200b39d",
|
||||
"hmac_key": "47b89da97f68d389867b5d8a2d7ba55715a30e3d88a3cc11f3646bc2af5580ef"
|
||||
}
|
||||
]
|
||||
},
|
||||
"calc_padded_len": [
|
||||
[16, 32],
|
||||
[32, 32],
|
||||
[33, 64],
|
||||
[37, 64],
|
||||
[45, 64],
|
||||
[49, 64],
|
||||
[64, 64],
|
||||
[65, 96],
|
||||
[100, 128],
|
||||
[111, 128],
|
||||
[200, 224],
|
||||
[250, 256],
|
||||
[320, 320],
|
||||
[383, 384],
|
||||
[384, 384],
|
||||
[400, 448],
|
||||
[500, 512],
|
||||
[512, 512],
|
||||
[515, 640],
|
||||
[700, 768],
|
||||
[800, 896],
|
||||
[900, 1024],
|
||||
[1020, 1024],
|
||||
[65536, 65536]
|
||||
],
|
||||
"encrypt_decrypt": [
|
||||
{
|
||||
"sec1": "0000000000000000000000000000000000000000000000000000000000000001",
|
||||
"sec2": "0000000000000000000000000000000000000000000000000000000000000002",
|
||||
"conversation_key": "c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d",
|
||||
"nonce": "0000000000000000000000000000000000000000000000000000000000000001",
|
||||
"plaintext": "a",
|
||||
"payload": "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABee0G5VSK0/9YypIObAtDKfYEAjD35uVkHyB0F4DwrcNaCXlCWZKaArsGrY6M9wnuTMxWfp1RTN9Xga8no+kF5Vsb"
|
||||
},
|
||||
{
|
||||
"sec1": "0000000000000000000000000000000000000000000000000000000000000002",
|
||||
"sec2": "0000000000000000000000000000000000000000000000000000000000000001",
|
||||
"conversation_key": "c41c775356fd92eadc63ff5a0dc1da211b268cbea22316767095b2871ea1412d",
|
||||
"nonce": "f00000000000000000000000000000f00000000000000000000000000000000f",
|
||||
"plaintext": "🍕🫃",
|
||||
"payload": "AvAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAPSKSK6is9ngkX2+cSq85Th16oRTISAOfhStnixqZziKMDvB0QQzgFZdjLTPicCJaV8nDITO+QfaQ61+KbWQIOO2Yj"
|
||||
},
|
||||
{
|
||||
"sec1": "5c0c523f52a5b6fad39ed2403092df8cebc36318b39383bca6c00808626fab3a",
|
||||
"sec2": "4b22aa260e4acb7021e32f38a6cdf4b673c6a277755bfce287e370c924dc936d",
|
||||
"conversation_key": "3e2b52a63be47d34fe0a80e34e73d436d6963bc8f39827f327057a9986c20a45",
|
||||
"nonce": "b635236c42db20f021bb8d1cdff5ca75dd1a0cc72ea742ad750f33010b24f73b",
|
||||
"plaintext": "表ポあA鷗ŒéB逍Üߪąñ丂㐀𠀀",
|
||||
"payload": "ArY1I2xC2yDwIbuNHN/1ynXdGgzHLqdCrXUPMwELJPc7s7JqlCMJBAIIjfkpHReBPXeoMCyuClwgbT419jUWU1PwaNl4FEQYKCDKVJz+97Mp3K+Q2YGa77B6gpxB/lr1QgoqpDf7wDVrDmOqGoiPjWDqy8KzLueKDcm9BVP8xeTJIxs="
|
||||
},
|
||||
{
|
||||
"sec1": "8f40e50a84a7462e2b8d24c28898ef1f23359fff50d8c509e6fb7ce06e142f9c",
|
||||
"sec2": "b9b0a1e9cc20100c5faa3bbe2777303d25950616c4c6a3fa2e3e046f936ec2ba",
|
||||
"conversation_key": "d5a2f879123145a4b291d767428870f5a8d9e5007193321795b40183d4ab8c2b",
|
||||
"nonce": "b20989adc3ddc41cd2c435952c0d59a91315d8c5218d5040573fc3749543acaf",
|
||||
"plaintext": "ability🤝的 ȺȾ",
|
||||
"payload": "ArIJia3D3cQc0sQ1lSwNWakTFdjFIY1QQFc/w3SVQ6yvbG2S0x4Yu86QGwPTy7mP3961I1XqB6SFFTzqDZZavhxoWMj7mEVGMQIsh2RLWI5EYQaQDIePSnXPlzf7CIt+voTD"
|
||||
},
|
||||
{
|
||||
"sec1": "875adb475056aec0b4809bd2db9aa00cff53a649e7b59d8edcbf4e6330b0995c",
|
||||
"sec2": "9c05781112d5b0a2a7148a222e50e0bd891d6b60c5483f03456e982185944aae",
|
||||
"conversation_key": "3b15c977e20bfe4b8482991274635edd94f366595b1a3d2993515705ca3cedb8",
|
||||
"nonce": "8d4442713eb9d4791175cb040d98d6fc5be8864d6ec2f89cf0895a2b2b72d1b1",
|
||||
"plaintext": "pepper👀їжак",
|
||||
"payload": "Ao1EQnE+udR5EXXLBA2Y1vxb6IZNbsL4nPCJWisrctGxY3AduCS+jTUgAAnfvKafkmpy15+i9YMwCdccisRa8SvzW671T2JO4LFSPX31K4kYUKelSAdSPwe9NwO6LhOsnoJ+"
|
||||
},
|
||||
{
|
||||
"sec1": "eba1687cab6a3101bfc68fd70f214aa4cc059e9ec1b79fdb9ad0a0a4e259829f",
|
||||
"sec2": "dff20d262bef9dfd94666548f556393085e6ea421c8af86e9d333fa8747e94b3",
|
||||
"conversation_key": "4f1538411098cf11c8af216836444787c462d47f97287f46cf7edb2c4915b8a5",
|
||||
"nonce": "2180b52ae645fcf9f5080d81b1f0b5d6f2cd77ff3c986882bb549158462f3407",
|
||||
"plaintext": "( ͡° ͜ʖ ͡°)",
|
||||
"payload": "AiGAtSrmRfz59QgNgbHwtdbyzXf/PJhogrtUkVhGLzQHv4qhKQwnFQ54OjVMgqCea/Vj0YqBSdhqNR777TJ4zIUk7R0fnizp6l1zwgzWv7+ee6u+0/89KIjY5q1wu6inyuiv"
|
||||
},
|
||||
{
|
||||
"sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e",
|
||||
"sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214",
|
||||
"conversation_key": "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd",
|
||||
"nonce": "e4cd5f7ce4eea024bc71b17ad456a986a74ac426c2c62b0a15eb5c5c8f888b68",
|
||||
"plaintext": "مُنَاقَشَةُ سُبُلِ اِسْتِخْدَامِ اللُّغَةِ فِي النُّظُمِ الْقَائِمَةِ وَفِيم يَخُصَّ التَّطْبِيقَاتُ الْحاسُوبِيَّةُ،",
|
||||
"payload": "AuTNX3zk7qAkvHGxetRWqYanSsQmwsYrChXrXFyPiItoIBsWu1CB+sStla2M4VeANASHxM78i1CfHQQH1YbBy24Tng7emYW44ol6QkFD6D8Zq7QPl+8L1c47lx8RoODEQMvNCbOk5ffUV3/AhONHBXnffrI+0025c+uRGzfqpYki4lBqm9iYU+k3Tvjczq9wU0mkVDEaM34WiQi30MfkJdRbeeYaq6kNvGPunLb3xdjjs5DL720d61Flc5ZfoZm+CBhADy9D9XiVZYLKAlkijALJur9dATYKci6OBOoc2SJS2Clai5hOVzR0yVeyHRgRfH9aLSlWW5dXcUxTo7qqRjNf8W5+J4jF4gNQp5f5d0YA4vPAzjBwSP/5bGzNDslKfcAH"
|
||||
},
|
||||
{
|
||||
"sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e",
|
||||
"sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214",
|
||||
"conversation_key": "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd",
|
||||
"nonce": "38d1ca0abef9e5f564e89761a86cee04574b6825d3ef2063b10ad75899e4b023",
|
||||
"plaintext": "الكل في المجمو عة (5)",
|
||||
"payload": "AjjRygq++eX1ZOiXYahs7gRXS2gl0+8gY7EK11iZ5LAjbOTrlfrxak5Lki42v2jMPpLSicy8eHjsWkkMtF0i925vOaKG/ZkMHh9ccQBdfTvgEGKzztedqDCAWb5TP1YwU1PsWaiiqG3+WgVvJiO4lUdMHXL7+zKKx8bgDtowzz4QAwI="
|
||||
},
|
||||
{
|
||||
"sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e",
|
||||
"sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214",
|
||||
"conversation_key": "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd",
|
||||
"nonce": "4f1a31909f3483a9e69c8549a55bbc9af25fa5bbecf7bd32d9896f83ef2e12e0",
|
||||
"plaintext": "𝖑𝖆𝖟𝖞 社會科學院語學研究所",
|
||||
"payload": "Ak8aMZCfNIOp5pyFSaVbvJryX6W77Pe9MtmJb4PvLhLgh/TsxPLFSANcT67EC1t/qxjru5ZoADjKVEt2ejdx+xGvH49mcdfbc+l+L7gJtkH7GLKpE9pQNQWNHMAmj043PAXJZ++fiJObMRR2mye5VHEANzZWkZXMrXF7YjuG10S1pOU="
|
||||
},
|
||||
{
|
||||
"sec1": "d5633530f5bcfebceb5584cfbbf718a30df0751b729dd9a789b9f30c0587d74e",
|
||||
"sec2": "b74e6a341fb134127272b795a08b59250e5fa45a82a2eb4095e4ce9ed5f5e214",
|
||||
"conversation_key": "75fe686d21a035f0c7cd70da64ba307936e5ca0b20710496a6b6b5f573377bdd",
|
||||
"nonce": "a3e219242d85465e70adcd640b564b3feff57d2ef8745d5e7a0663b2dccceb54",
|
||||
"plaintext": "🙈 🙉 🙊 0️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ 6️⃣ 7️⃣ 8️⃣ 9️⃣ 🔟 Powerلُلُصّبُلُلصّبُررً ॣ ॣh ॣ ॣ冗",
|
||||
"payload": "AqPiGSQthUZecK3NZAtWSz/v9X0u+HRdXnoGY7LczOtUf05aMF89q1FLwJvaFJYICZoMYgRJHFLwPiOHce7fuAc40kX0wXJvipyBJ9HzCOj7CgtnC1/cmPCHR3s5AIORmroBWglm1LiFMohv1FSPEbaBD51VXxJa4JyWpYhreSOEjn1wd0lMKC9b+osV2N2tpbs+rbpQem2tRen3sWflmCqjkG5VOVwRErCuXuPb5+hYwd8BoZbfCrsiAVLd7YT44dRtKNBx6rkabWfddKSLtreHLDysOhQUVOp/XkE7OzSkWl6sky0Hva6qJJ/V726hMlomvcLHjE41iKmW2CpcZfOedg=="
|
||||
}
|
||||
],
|
||||
"encrypt_decrypt_long_msg": [
|
||||
{
|
||||
"conversation_key": "8fc262099ce0d0bb9b89bac05bb9e04f9bc0090acc181fef6840ccee470371ed",
|
||||
"nonce": "326bcb2c943cd6bb717588c9e5a7e738edf6ed14ec5f5344caa6ef56f0b9cff7",
|
||||
"pattern": "x",
|
||||
"repeat": 65535,
|
||||
"plaintext_sha256": "09ab7495d3e61a76f0deb12cb0306f0696cbb17ffc12131368c7a939f12f56d3",
|
||||
"payload_sha256": "90714492225faba06310bff2f249ebdc2a5e609d65a629f1c87f2d4ffc55330a"
|
||||
},
|
||||
{
|
||||
"conversation_key": "56adbe3720339363ab9c3b8526ffce9fd77600927488bfc4b59f7a68ffe5eae0",
|
||||
"nonce": "ad68da81833c2a8ff609c3d2c0335fd44fe5954f85bb580c6a8d467aa9fc5dd0",
|
||||
"pattern": "!",
|
||||
"repeat": 65535,
|
||||
"plaintext_sha256": "6af297793b72ae092c422e552c3bb3cbc310da274bd1cf9e31023a7fe4a2d75e",
|
||||
"payload_sha256": "8013e45a109fad3362133132b460a2d5bce235fe71c8b8f4014793fb52a49844"
|
||||
},
|
||||
{
|
||||
"conversation_key": "7fc540779979e472bb8d12480b443d1e5eb1098eae546ef2390bee499bbf46be",
|
||||
"nonce": "34905e82105c20de9a2f6cd385a0d541e6bcc10601d12481ff3a7575dc622033",
|
||||
"pattern": "🦄",
|
||||
"repeat": 16383,
|
||||
"plaintext_sha256": "a249558d161b77297bc0cb311dde7d77190f6571b25c7e4429cd19044634a61f",
|
||||
"payload_sha256": "b3348422471da1f3c59d79acfe2fe103f3cd24488109e5b18734cdb5953afd15"
|
||||
}
|
||||
]
|
||||
},
|
||||
"invalid": {
|
||||
"encrypt_msg_lengths": [0, 65536, 100000, 10000000],
|
||||
"get_conversation_key": [
|
||||
{
|
||||
"sec1": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
"note": "sec1 higher than curve.n"
|
||||
},
|
||||
{
|
||||
"sec1": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
"note": "sec1 is 0"
|
||||
},
|
||||
{
|
||||
"sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364139",
|
||||
"pub2": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
"note": "pub2 is invalid, no sqrt, all-ff"
|
||||
},
|
||||
{
|
||||
"sec1": "fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141",
|
||||
"pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
"note": "sec1 == curve.n"
|
||||
},
|
||||
{
|
||||
"sec1": "0000000000000000000000000000000000000000000000000000000000000002",
|
||||
"pub2": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
|
||||
"note": "pub2 is invalid, no sqrt"
|
||||
},
|
||||
{
|
||||
"sec1": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
|
||||
"pub2": "0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"note": "pub2 is point of order 3 on twist"
|
||||
},
|
||||
{
|
||||
"sec1": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
|
||||
"pub2": "eb1f7200aecaa86682376fb1c13cd12b732221e774f553b0a0857f88fa20f86d",
|
||||
"note": "pub2 is point of order 13 on twist"
|
||||
},
|
||||
{
|
||||
"sec1": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
|
||||
"pub2": "709858a4c121e4a84eb59c0ded0261093c71e8ca29efeef21a6161c447bcaf9f",
|
||||
"note": "pub2 is point of order 3319 on twist"
|
||||
}
|
||||
],
|
||||
"decrypt": [
|
||||
{
|
||||
"conversation_key": "ca2527a037347b91bea0c8a30fc8d9600ffd81ec00038671e3a0f0cb0fc9f642",
|
||||
"nonce": "daaea5ca345b268e5b62060ca72c870c48f713bc1e00ff3fc0ddb78e826f10db",
|
||||
"plaintext": "n o b l e",
|
||||
"payload": "#Atqupco0WyaOW2IGDKcshwxI9xO8HgD/P8Ddt46CbxDbrhdG8VmJdU0MIDf06CUvEvdnr1cp1fiMtlM/GrE92xAc1K5odTpCzUB+mjXgbaqtntBUbTToSUoT0ovrlPwzGjyp",
|
||||
"note": "unknown encryption version"
|
||||
},
|
||||
{
|
||||
"conversation_key": "36f04e558af246352dcf73b692fbd3646a2207bd8abd4b1cd26b234db84d9481",
|
||||
"nonce": "ad408d4be8616dc84bb0bf046454a2a102edac937c35209c43cd7964c5feb781",
|
||||
"plaintext": "⚠️",
|
||||
"payload": "AK1AjUvoYW3IS7C/BGRUoqEC7ayTfDUgnEPNeWTF/reBZFaha6EAIRueE9D1B1RuoiuFScC0Q94yjIuxZD3JStQtE8JMNacWFs9rlYP+ZydtHhRucp+lxfdvFlaGV/sQlqZz",
|
||||
"note": "unknown encryption version 0"
|
||||
},
|
||||
{
|
||||
"conversation_key": "ca2527a037347b91bea0c8a30fc8d9600ffd81ec00038671e3a0f0cb0fc9f642",
|
||||
"nonce": "daaea5ca345b268e5b62060ca72c870c48f713bc1e00ff3fc0ddb78e826f10db",
|
||||
"plaintext": "n o s t r",
|
||||
"payload": "Atфupco0WyaOW2IGDKcshwxI9xO8HgD/P8Ddt46CbxDbrhdG8VmJZE0UICD06CUvEvdnr1cp1fiMtlM/GrE92xAc1EwsVCQEgWEu2gsHUVf4JAa3TpgkmFc3TWsax0v6n/Wq",
|
||||
"note": "invalid base64"
|
||||
},
|
||||
{
|
||||
"conversation_key": "cff7bd6a3e29a450fd27f6c125d5edeb0987c475fd1e8d97591e0d4d8a89763c",
|
||||
"nonce": "09ff97750b084012e15ecb84614ce88180d7b8ec0d468508a86b6d70c0361a25",
|
||||
"plaintext": "¯\\_(ツ)_/¯",
|
||||
"payload": "Agn/l3ULCEAS4V7LhGFM6IGA17jsDUaFCKhrbXDANholyySBfeh+EN8wNB9gaLlg4j6wdBYh+3oK+mnxWu3NKRbSvQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
"note": "invalid MAC"
|
||||
},
|
||||
{
|
||||
"conversation_key": "cfcc9cf682dfb00b11357f65bdc45e29156b69db424d20b3596919074f5bf957",
|
||||
"nonce": "65b14b0b949aaa7d52c417eb753b390e8ad6d84b23af4bec6d9bfa3e03a08af4",
|
||||
"plaintext": "🥎",
|
||||
"payload": "AmWxSwuUmqp9UsQX63U7OQ6K1thLI69L7G2b+j4DoIr0oRWQ8avl4OLqWZiTJ10vIgKrNqjoaX+fNhE9RqmR5g0f6BtUg1ijFMz71MO1D4lQLQfW7+UHva8PGYgQ1QpHlKgR",
|
||||
"note": "invalid MAC"
|
||||
},
|
||||
{
|
||||
"conversation_key": "5254827d29177622d40a7b67cad014fe7137700c3c523903ebbe3e1b74d40214",
|
||||
"nonce": "7ab65dbb8bbc2b8e35cafb5745314e1f050325a864d11d0475ef75b3660d91c1",
|
||||
"plaintext": "elliptic-curve cryptography",
|
||||
"payload": "Anq2XbuLvCuONcr7V0UxTh8FAyWoZNEdBHXvdbNmDZHB573MI7R7rrTYftpqmvUpahmBC2sngmI14/L0HjOZ7lWGJlzdh6luiOnGPc46cGxf08MRC4CIuxx3i2Lm0KqgJ7vA",
|
||||
"note": "invalid padding"
|
||||
},
|
||||
{
|
||||
"conversation_key": "fea39aca9aa8340c3a78ae1f0902aa7e726946e4efcd7783379df8096029c496",
|
||||
"nonce": "7d4283e3b54c885d6afee881f48e62f0a3f5d7a9e1cb71ccab594a7882c39330",
|
||||
"plaintext": "noble",
|
||||
"payload": "An1Cg+O1TIhdav7ogfSOYvCj9dep4ctxzKtZSniCw5MwRrrPJFyAQYZh5VpjC2QYzny5LIQ9v9lhqmZR4WBYRNJ0ognHVNMwiFV1SHpvUFT8HHZN/m/QarflbvDHAtO6pY16",
|
||||
"note": "invalid padding"
|
||||
},
|
||||
{
|
||||
"conversation_key": "0c4cffb7a6f7e706ec94b2e879f1fc54ff8de38d8db87e11787694d5392d5b3f",
|
||||
"nonce": "6f9fd72667c273acd23ca6653711a708434474dd9eb15c3edb01ce9a95743e9b",
|
||||
"plaintext": "censorship-resistant and global social network",
|
||||
"payload": "Am+f1yZnwnOs0jymZTcRpwhDRHTdnrFcPtsBzpqVdD6b2NZDaNm/TPkZGr75kbB6tCSoq7YRcbPiNfJXNch3Tf+o9+zZTMxwjgX/nm3yDKR2kHQMBhVleCB9uPuljl40AJ8kXRD0gjw+aYRJFUMK9gCETZAjjmrsCM+nGRZ1FfNsHr6Z",
|
||||
"note": "invalid padding"
|
||||
},
|
||||
{
|
||||
"conversation_key": "5cd2d13b9e355aeb2452afbd3786870dbeecb9d355b12cb0a3b6e9da5744cd35",
|
||||
"nonce": "b60036976a1ada277b948fd4caa065304b96964742b89d26f26a25263a5060bd",
|
||||
"plaintext": "0",
|
||||
"payload": "",
|
||||
"note": "invalid payload length: 0"
|
||||
},
|
||||
{
|
||||
"conversation_key": "d61d3f09c7dfe1c0be91af7109b60a7d9d498920c90cbba1e137320fdd938853",
|
||||
"nonce": "1a29d02c8b4527745a2ccb38bfa45655deb37bc338ab9289d756354cea1fd07c",
|
||||
"plaintext": "1",
|
||||
"payload": "Ag==",
|
||||
"note": "invalid payload length: 4"
|
||||
},
|
||||
{
|
||||
"conversation_key": "873bb0fc665eb950a8e7d5971965539f6ebd645c83c08cd6a85aafbad0f0bc47",
|
||||
"nonce": "c826d3c38e765ab8cc42060116cd1464b2a6ce01d33deba5dedfb48615306d4a",
|
||||
"plaintext": "2",
|
||||
"payload": "AqxgToSh3H7iLYRJjoWAM+vSv/Y1mgNlm6OWWjOYUClrFF8=",
|
||||
"note": "invalid payload length: 48"
|
||||
},
|
||||
{
|
||||
"conversation_key": "9f2fef8f5401ac33f74641b568a7a30bb19409c76ffdc5eae2db6b39d2617fbe",
|
||||
"nonce": "9ff6484642545221624eaac7b9ea27133a4cc2356682a6033aceeef043549861",
|
||||
"plaintext": "3",
|
||||
"payload": "Ap/2SEZCVFIhYk6qx7nqJxM6TMI1ZoKmAzrO7vBDVJhhuZXWiM20i/tIsbjT0KxkJs2MZjh1oXNYMO9ggfk7i47WQA==",
|
||||
"note": "invalid payload length: 92"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
16
docs/DEV_TIPS.md
Normal file
16
docs/DEV_TIPS.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Dev tips
|
||||
|
||||
A collection of tips when developing or testing Damus.
|
||||
|
||||
|
||||
## Logging
|
||||
|
||||
- Info and debug messages must be activated in the macOS Console to become visible, they are not visible by default. To activate, go to Console > Action > Include Info Messages.
|
||||
|
||||
|
||||
## Testing push notifications
|
||||
|
||||
- Dev builds (i.e. anything that isn't an official build from TestFlight or AppStore) only work with the development/sandbox APNS environment. If testing push notifications on a local damus build, ensure that:
|
||||
- Damus is configured to use the "staging" push notifications environment, under Settings > Developer settings.
|
||||
- Ensure that Nostr events are sent to `wss://notify-staging.damus.io`.
|
||||
|
||||
Reference in New Issue
Block a user