Compare commits

..

1 Commits

Author SHA1 Message Date
180c5ce7ee Fix nostr URL scheme to open properly even if there's already a different view open
Changelog-Fixed: Fix nostr URL scheme to open properly even if there's already a different view open
2023-05-14 14:32:56 -04:00
225 changed files with 2628 additions and 7231 deletions

View File

@@ -1,53 +1,3 @@
## [1.5-5] - 2023-06-24
### Fixed
- Remove note zaps to fit apples appstore guidelines
- Fix zap sheet popping (William Casarin)
- Fix CustomizeZapView from randomly disappearing (William Casarin)
- Fix "zapped your profile" strings to say "zapped you" (Terry Yiu)
- Fix reconnect loop issues on iOS17 (William Casarin)
- Fix some more thread jankiness (William Casarin)
- Fix spelling of Nostr to use Titlecase instead of lowercase (Terry Yiu)
- Rename all usages of the term Post as a noun to Note to conform to the Nostr spec (Terry Yiu)
- Fix text cutoff on login with npub (gladiusKatana)
- Fix hangs due to video player (William Casarin)
[1.5-5]: https://github.com/damus-io/damus/releases/tag/v1.5-5
## [1.5-2] - 2023-05-30
### Added
- Add new full-bleed video player (William Casarin)
- Add ability to show multiple posts per user in Universe (Ben Weeks)
- Custom iconography added for other areas of the app. (Ben Weeks)
- Custom iconography for the left navigation. (Ben Weeks)
- Custom iconography for the tab buttons. (Ben Weeks)
- Added dots under image carousel (Ben Weeks)
- Add profile caching (Bryan Montz)
- Add mention parsing and fine-grained text selection on description in ProfileView (Terry Yiu)
### Changed
- Redesign phase 1 (text, icons)
- Updated UI to use custom font (Ben Weeks)
### Fixed
- Fix side menu bug in landscape (OlegAba)
- Use "Follow me on nostr" text when looking at someone else's QR code (Ben Weeks)
- Fix issue where cursor dissapears when typing long message (gladiusKatana)
- Attempt fix for randomly broken animated gifs (William Casarin)
- Fix cursor jumping when pressing return (gladius)
- Fix side menu label size so that translations in longer languages fit without wrapping (Terry Yiu)
- Fix reaction notification title to be consistent with ReactionView (Terry Yiu)
- Fix nostr URL scheme to open properly even if there's already a different view open (Terry Yiu)
- Fix crash related to preloading events (Bryan Montz)
## v1.4.3 - 2023-05-08
### Added

View File

@@ -94,33 +94,10 @@ damus implements the following [Nostr Implementation Possibilities][nips]
Contributors welcome! Start by examining known issues: https://github.com/damus-io/damus/issues.
### Mailing lists
We have a few mailing lists that anyone can join to get involved in damus development:
- [dev][dev-list] - development discussions
- [patches][patches-list] - code submission and review
- [product][product-list] - product discussions
- [design][design-list] - design discussions
[dev-list]: https://damus.io/list/dev
[patches-list]: https://damus.io/list/patches
[product-list]: https://damus.io/list/product
[design-list]: https://damus.io/list/design
### Code
[Email patches][git-send-email] to patches@damus.io are preferred, but I accept PRs on GitHub as well. Patches sent via email may include a bolt11 lightning invoice, choosing the price you think the patch is worth, and I will pay it once the patch is accepted and if I think the price isn't unreasonable. You can also send an any-amount invoice and I will pay what I think it's worth if you prefer not to choose. You can include the bolt11 in the commit body or email so that it can be paid once it is applied.
[Email patches][git-send-email] to jb55@jb55.com are preferred, but I accept PRs on GitHub as well.
Recommended settings when submitting code via email:
```
$ git config sendemail.to "patches@damus.io"
$ git config format.subjectPrefix "PATCH damus"
$ git config --global sendemail.annotate yes
$ git config format.signOff yes
```
[git-send-email]: http://git-send-email.io
### Privacy

View File

@@ -110,6 +110,21 @@ static inline int peek_char(struct cursor *cur, int ind) {
return *(cur->p + ind);
}
static int parse_digit(struct cursor *cur, int *digit) {
int c;
if ((c = peek_char(cur, 0)) == -1)
return 0;
c -= '0';
if (c >= 0 && c <= 9) {
*digit = c;
cur->p++;
return 1;
}
return 0;
}
static inline int pull_byte(struct cursor *cur, u8 *byte) {
if (cur->p >= cur->end)

View File

@@ -12,22 +12,6 @@
#include <stdlib.h>
#include <string.h>
static int parse_digit(struct cursor *cur, int *digit) {
int c;
if ((c = peek_char(cur, 0)) == -1)
return 0;
c -= '0';
if (c >= 0 && c <= 9) {
*digit = c;
cur->p++;
return 1;
}
return 0;
}
static int parse_mention_index(struct cursor *cur, struct block *block) {
int d1, d2, d3, ind;
const u8 *start = cur->p;

View File

@@ -26,7 +26,7 @@ bool hex_decode(const char *str, size_t slen, void *buf, size_t bufsize);
/**
* hex_encode - Create a nul-terminated hex string
* @buf: the buffer to read the data from
* @bufsize: the length of buf
* @bufsize: the length of @buf
* @dest: the string to fill
* @destsize: the max size of the string
*

View File

@@ -11,16 +11,13 @@
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */; };
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.swift */; };
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; };
3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */; };
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */; };
3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */; };
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */; };
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F229A91366008A0F29 /* ProfileViewTests.swift */; };
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */; };
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; };
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */; };
3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; };
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; };
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
@@ -52,13 +49,10 @@
4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2229DDDB8100516EAC /* IconLabel.swift */; };
4C1A9A2529DDDF2600516EAC /* ZapSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */; };
4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */; };
4C1A9A2A29DDF54400516EAC /* DamusVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A2929DDF54400516EAC /* DamusVideoPlayer.swift */; };
4C216F32286E388800040376 /* DMChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F31286E388800040376 /* DMChatView.swift */; };
4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; };
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */; };
4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F372871EDE300040376 /* DirectMessageModel.swift */; };
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C28595F2A12A2BE004746F7 /* SupporterBadge.swift */; };
4C2859622A12A7F0004746F7 /* GoldSupportGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2859612A12A7F0004746F7 /* GoldSupportGradient.swift */; };
4C285C8228385570008A31F1 /* CarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8128385570008A31F1 /* CarouselView.swift */; };
4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8328385690008A31F1 /* CreateAccountView.swift */; };
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C85283892E7008A31F1 /* CreateAccountModel.swift */; };
@@ -132,7 +126,6 @@
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */; };
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */; };
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 4C649880286E0EE300EAE2B3 /* secp256k1 */; };
4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C73C5132A4437C10062CAC0 /* ZapUserView.swift */; };
4C75EFA427FA577B0006080F /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; };
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA527FF87A20006080F /* Nostr.swift */; };
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFAC28049CFB0006080F /* PostButton.swift */; };
@@ -171,8 +164,6 @@
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD19283AA67F008EE7EF /* Bech32.swift */; };
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */; };
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C987B56283FD07F0042CE38 /* FollowersModel.swift */; };
4C9AA1482A44442E003F49FD /* CustomizeZapModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9AA1472A44442E003F49FD /* CustomizeZapModel.swift */; };
4C9AA14A2A4587A6003F49FD /* NotificationStatusModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9AA1492A4587A6003F49FD /* NotificationStatusModel.swift */; };
4C9BB83129C0ED4F00FC4E37 /* DisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */; };
4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */; };
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */; };
@@ -200,7 +191,6 @@
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AD2976FA9300DC99E7 /* FormatTests.swift */; };
4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AF297705DD00DC99E7 /* ZapButton.swift */; };
4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883B5297730E400DC99E7 /* LNUrls.swift */; };
4CB8FC232A41ABA800763C51 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8FC222A41ABA500763C51 /* AboutView.swift */; };
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */; };
4CB9D4A92992D2F400A9A7E4 /* FollowsYou.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */; };
4CBCA930297DB57F00EC6B2F /* WebsiteLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */; };
@@ -215,8 +205,6 @@
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC7AAF9297F64AC00430951 /* EventMenu.swift */; };
4CCEB7AE29B53D260078AA28 /* SearchingEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */; };
4CCEB7B029B5415A0078AA28 /* SearchingProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */; };
4CCF9AAF2A1FDBDB00E03CFB /* VideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CCF9AAE2A1FDBDB00E03CFB /* VideoPlayer.swift */; };
4CCF9AB22A1FE80C00E03CFB /* GSPlayer in Frameworks */ = {isa = PBXBuildFile; productRef = 4CCF9AB12A1FE80C00E03CFB /* GSPlayer */; };
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */; };
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CD7641A28A1641400B6928F /* EndBlock.swift */; };
4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDA128929E9D10C0006FA5A /* SignalView.swift */; };
@@ -244,7 +232,7 @@
4CE8794829941DA700F758CC /* RelayFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794729941DA700F758CC /* RelayFilters.swift */; };
4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */; };
4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794D2996B16A00F758CC /* RelayToggle.swift */; };
4CE879502996B2BD00F758CC /* RelayStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794F2996B2BD00F758CC /* RelayStatusView.swift */; };
4CE879502996B2BD00F758CC /* RelayStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE8794F2996B2BD00F758CC /* RelayStatus.swift */; };
4CE879522996B68900F758CC /* RelayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879512996B68900F758CC /* RelayType.swift */; };
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879542996BAB900F758CC /* RelayPaidDetail.swift */; };
4CE879582996C45300F758CC /* ZapsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE879572996C45300F758CC /* ZapsView.swift */; };
@@ -275,21 +263,13 @@
4CFF8F6D29CD022E008DB934 /* WideEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6C29CD022E008DB934 /* WideEventView.swift */; };
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; };
50088DA129E8271A008A1FDF /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50088DA029E8271A008A1FDF /* WebSocket.swift */; };
5019CADD2A0FB0A9000069E1 /* ProfileDatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5019CADC2A0FB0A9000069E1 /* ProfileDatabaseTests.swift */; };
501F8C5529FF5EF6001AFC1D /* PersistedProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5429FF5EF6001AFC1D /* PersistedProfile.swift */; };
501F8C5829FF5FC5001AFC1D /* Damus.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5629FF5FC5001AFC1D /* Damus.xcdatamodeld */; };
501F8C5A29FF70F5001AFC1D /* ProfileDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */; };
501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C7F2A0220E1001AFC1D /* KeychainStorage.swift */; };
501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */; };
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; };
50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B5685229F97CB400A23243 /* CredentialHandler.swift */; };
50DA11262A16A23F00236234 /* Launch.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50DA11252A16A23F00236234 /* Launch.storyboard */; };
5C0707D12A1ECB38004E7B51 /* DamusLogoGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */; };
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; };
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */; };
5C6E1DAF2A194075008FC15A /* PinkGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C6E1DAE2A194075008FC15A /* PinkGradient.swift */; };
5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF72FC129B9142F00124A13 /* ShareAction.swift */; };
6439E014296790CF0020672B /* ProfilePicImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6439E013296790CF0020672B /* ProfilePicImageView.swift */; };
643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643EA5C7296B764E005081BB /* RelayFilterView.swift */; };
@@ -304,9 +284,7 @@
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; };
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; };
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.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 */; };
F757933A29D7AECD007DEAC1 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = F757933929D7AECD007DEAC1 /* ImagePicker.swift */; };
@@ -345,7 +323,6 @@
3A185A04297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A185A05297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A185A06297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "lv-LV"; path = "lv-LV.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButtonModel.swift; sourceTree = "<group>"; };
3A25EF132992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -371,7 +348,6 @@
3A41E559299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A41E55A299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = id.lproj/Localizable.strings; sourceTree = "<group>"; };
3A41E55B299D52BE001FA465 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = id; path = id.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CondensedProfilePicturesView.swift; sourceTree = "<group>"; };
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutedThreadsManager.swift; sourceTree = "<group>"; };
3A5C4575296A879E0032D398 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "es-419"; path = "es-419.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
@@ -389,7 +365,6 @@
3A8624D9299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A8624DA299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
3A8624DB299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringUtil.swift; sourceTree = "<group>"; };
3A929C20297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A929C21297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A929C22297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "it-IT"; path = "it-IT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -465,13 +440,10 @@
4C1A9A2229DDDB8100516EAC /* IconLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconLabel.swift; sourceTree = "<group>"; };
4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapSettingsView.swift; sourceTree = "<group>"; };
4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslationSettingsView.swift; sourceTree = "<group>"; };
4C1A9A2929DDF54400516EAC /* DamusVideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusVideoPlayer.swift; sourceTree = "<group>"; };
4C216F31286E388800040376 /* DMChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMChatView.swift; sourceTree = "<group>"; };
4C216F33286F5ACD00040376 /* DMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMView.swift; sourceTree = "<group>"; };
4C216F352870A9A700040376 /* InputDismissKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDismissKeyboard.swift; sourceTree = "<group>"; };
4C216F372871EDE300040376 /* DirectMessageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessageModel.swift; sourceTree = "<group>"; };
4C28595F2A12A2BE004746F7 /* SupporterBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupporterBadge.swift; sourceTree = "<group>"; };
4C2859612A12A7F0004746F7 /* GoldSupportGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoldSupportGradient.swift; sourceTree = "<group>"; };
4C285C8128385570008A31F1 /* CarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselView.swift; sourceTree = "<group>"; };
4C285C8328385690008A31F1 /* CreateAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateAccountView.swift; sourceTree = "<group>"; };
4C285C85283892E7008A31F1 /* CreateAccountModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateAccountModel.swift; sourceTree = "<group>"; };
@@ -574,7 +546,6 @@
4C633351283D419F00B1C9C3 /* SignalModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalModel.swift; sourceTree = "<group>"; };
4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesView.swift; sourceTree = "<group>"; };
4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesModel.swift; sourceTree = "<group>"; };
4C73C5132A4437C10062CAC0 /* ZapUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapUserView.swift; sourceTree = "<group>"; };
4C75EFA327FA577B0006080F /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = "<group>"; };
4C75EFA527FF87A20006080F /* Nostr.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nostr.swift; sourceTree = "<group>"; };
4C75EFA72804823E0006080F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
@@ -618,8 +589,6 @@
4C90BD19283AA67F008EE7EF /* Bech32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32.swift; sourceTree = "<group>"; };
4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Tests.swift; sourceTree = "<group>"; };
4C987B56283FD07F0042CE38 /* FollowersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersModel.swift; sourceTree = "<group>"; };
4C9AA1472A44442E003F49FD /* CustomizeZapModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapModel.swift; sourceTree = "<group>"; };
4C9AA1492A4587A6003F49FD /* NotificationStatusModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationStatusModel.swift; sourceTree = "<group>"; };
4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayName.swift; sourceTree = "<group>"; };
4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventProfileName.swift; sourceTree = "<group>"; };
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = "<group>"; };
@@ -647,7 +616,6 @@
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormatTests.swift; sourceTree = "<group>"; };
4CB883AF297705DD00DC99E7 /* ZapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButton.swift; sourceTree = "<group>"; };
4CB883B5297730E400DC99E7 /* LNUrls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrls.swift; sourceTree = "<group>"; };
4CB8FC222A41ABA500763C51 /* AboutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNameView.swift; sourceTree = "<group>"; };
4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowsYou.swift; sourceTree = "<group>"; };
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsiteLink.swift; sourceTree = "<group>"; };
@@ -662,7 +630,6 @@
4CC7AAF9297F64AC00430951 /* EventMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMenu.swift; sourceTree = "<group>"; };
4CCEB7AD29B53D260078AA28 /* SearchingEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingEventView.swift; sourceTree = "<group>"; };
4CCEB7AF29B5415A0078AA28 /* SearchingProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingProfileView.swift; sourceTree = "<group>"; };
4CCF9AAE2A1FDBDB00E03CFB /* VideoPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayer.swift; sourceTree = "<group>"; };
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageUploadModel.swift; sourceTree = "<group>"; };
4CD7641A28A1641400B6928F /* EndBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndBlock.swift; sourceTree = "<group>"; };
4CDA128929E9D10C0006FA5A /* SignalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalView.swift; sourceTree = "<group>"; };
@@ -693,7 +660,7 @@
4CE8794729941DA700F758CC /* RelayFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilters.swift; sourceTree = "<group>"; };
4CE8794B2995B59E00F758CC /* RelayMetadatas.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayMetadatas.swift; sourceTree = "<group>"; };
4CE8794D2996B16A00F758CC /* RelayToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayToggle.swift; sourceTree = "<group>"; };
4CE8794F2996B2BD00F758CC /* RelayStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayStatusView.swift; sourceTree = "<group>"; };
4CE8794F2996B2BD00F758CC /* RelayStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayStatus.swift; sourceTree = "<group>"; };
4CE879512996B68900F758CC /* RelayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayType.swift; sourceTree = "<group>"; };
4CE879542996BAB900F758CC /* RelayPaidDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayPaidDetail.swift; sourceTree = "<group>"; };
4CE879572996C45300F758CC /* ZapsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapsView.swift; sourceTree = "<group>"; };
@@ -725,21 +692,13 @@
4CFF8F6C29CD022E008DB934 /* WideEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WideEventView.swift; sourceTree = "<group>"; };
4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; };
50088DA029E8271A008A1FDF /* WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocket.swift; sourceTree = "<group>"; };
5019CADC2A0FB0A9000069E1 /* ProfileDatabaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDatabaseTests.swift; sourceTree = "<group>"; };
501F8C5429FF5EF6001AFC1D /* PersistedProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistedProfile.swift; sourceTree = "<group>"; };
501F8C5729FF5FC5001AFC1D /* Damus.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Damus.xcdatamodel; sourceTree = "<group>"; };
501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDatabase.swift; sourceTree = "<group>"; };
501F8C7F2A0220E1001AFC1D /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = "<group>"; };
501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorageTests.swift; sourceTree = "<group>"; };
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; };
50B5685229F97CB400A23243 /* CredentialHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialHandler.swift; sourceTree = "<group>"; };
50DA11252A16A23F00236234 /* Launch.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Launch.storyboard; sourceTree = "<group>"; };
5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusLogoGradient.swift; sourceTree = "<group>"; };
5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = "<group>"; };
5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; };
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientButtonStyle.swift; sourceTree = "<group>"; };
5C6E1DAE2A194075008FC15A /* PinkGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinkGradient.swift; sourceTree = "<group>"; };
5CF72FC129B9142F00124A13 /* ShareAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAction.swift; sourceTree = "<group>"; };
6439E013296790CF0020672B /* ProfilePicImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePicImageView.swift; sourceTree = "<group>"; };
643EA5C7296B764E005081BB /* RelayFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayFilterView.swift; sourceTree = "<group>"; };
@@ -754,9 +713,7 @@
9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachMediaUtility.swift; sourceTree = "<group>"; };
BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; };
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; };
D2277EE92A089BD5006C3807 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = "<group>"; };
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTests.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>"; };
F757933929D7AECD007DEAC1 /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
@@ -776,7 +733,6 @@
buildActionMask = 2147483647;
files = (
4C06670428FC7EC500038D2A /* Kingfisher in Frameworks */,
4CCF9AB22A1FE80C00E03CFB /* GSPlayer in Frameworks */,
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -878,7 +834,6 @@
4C0A3F8D280F63FF000448DE /* Models */ = {
isa = PBXGroup;
children = (
4C9AA1462A444422003F49FD /* Zaps */,
4C54AA0829A55416003E4487 /* Notifications */,
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */,
4C0A3F8E280F640A000448DE /* ThreadModel.swift */,
@@ -920,7 +875,6 @@
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */,
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */,
4C7D09772A0B0CC900943473 /* WalletModel.swift */,
3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -952,20 +906,10 @@
4C1A9A2029DDD3E100516EAC /* KeySettingsView.swift */,
4C1A9A2429DDDF2600516EAC /* ZapSettingsView.swift */,
4C1A9A2629DDE31900516EAC /* TranslationSettingsView.swift */,
E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */,
);
path = Settings;
sourceTree = "<group>";
};
4C1A9A2829DDF53B00516EAC /* Video */ = {
isa = PBXGroup;
children = (
4C1A9A2929DDF54400516EAC /* DamusVideoPlayer.swift */,
4CCF9AAE2A1FDBDB00E03CFB /* VideoPlayer.swift */,
);
path = Video;
sourceTree = "<group>";
};
4C30AC7029A5676F00E2BD5A /* Notifications */ = {
isa = PBXGroup;
children = (
@@ -982,7 +926,6 @@
children = (
4C54AA0929A55429003E4487 /* EventGroup.swift */,
4C54AA0B29A5543C003E4487 /* ZapGroup.swift */,
4C9AA1492A4587A6003F49FD /* NotificationStatusModel.swift */,
);
path = Notifications;
sourceTree = "<group>";
@@ -993,7 +936,6 @@
4C7D09692A0AEA0400943473 /* CodeScanner */,
4C7D095A2A098C5C00943473 /* Wallet */,
4C8D1A6D29F31E4100ACDF75 /* Buttons */,
4C1A9A2829DDF53B00516EAC /* Video */,
4C1A9A1B29DDCF8B00516EAC /* Settings */,
4CFF8F6129CC9A80008DB934 /* Images */,
4CCEB7AC29B53D180078AA28 /* Search */,
@@ -1048,7 +990,6 @@
4CF0ABD529817F5B00D66079 /* ReportView.swift */,
4CF0ABE42981EE0C00D66079 /* EULAView.swift */,
3AA247FE297E3D900090C62D /* RepostsView.swift */,
50DA11252A16A23F00236234 /* Launch.storyboard */,
5C513FCB2984ACA60072348F /* QRCodeView.swift */,
643EA5C7296B764E005081BB /* RelayFilterView.swift */,
);
@@ -1058,7 +999,6 @@
4C75EFAB28049CC80006080F /* Nostr */ = {
isa = PBXGroup;
children = (
501F8C5329FF5EE2001AFC1D /* CoreData */,
4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */,
4C75EFA527FF87A20006080F /* Nostr.swift */,
4C75EFAE28049D340006080F /* NostrFilter.swift */,
@@ -1069,7 +1009,6 @@
4C75EFBA2804A34C0006080F /* ProofOfWork.swift */,
4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */,
4CACA9DB280C38C000D9BBE8 /* Profiles.swift */,
501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */,
4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */,
4C363A8F28247A1D006E126D /* NostrLink.swift */,
50088DA029E8271A008A1FDF /* WebSocket.swift */,
@@ -1102,9 +1041,6 @@
children = (
4C7D09712A0AEF5E00943473 /* DamusGradient.swift */,
4C7D09732A0AEF9000943473 /* AlbyGradient.swift */,
4C2859612A12A7F0004746F7 /* GoldSupportGradient.swift */,
5C6E1DAE2A194075008FC15A /* PinkGradient.swift */,
5C0707D02A1ECB38004E7B51 /* DamusLogoGradient.swift */,
);
path = Gradients;
sourceTree = "<group>";
@@ -1156,8 +1092,6 @@
4CA5588229F33F5B00DC6A45 /* StringCodable.swift */,
50B5685229F97CB400A23243 /* CredentialHandler.swift */,
4C7D09582A05BEAD00943473 /* KeyboardVisible.swift */,
3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */,
D2277EE92A089BD5006C3807 /* Router.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -1171,14 +1105,6 @@
path = Buttons;
sourceTree = "<group>";
};
4C9AA1462A444422003F49FD /* Zaps */ = {
isa = PBXGroup;
children = (
4C9AA1472A44442E003F49FD /* CustomizeZapModel.swift */,
);
path = Zaps;
sourceTree = "<group>";
};
4CAAD8AE29888A9B00060CEA /* Relays */ = {
isa = PBXGroup;
children = (
@@ -1188,7 +1114,7 @@
4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */,
F7908E91298B0F0700AB113A /* RelayDetailView.swift */,
4CE8794D2996B16A00F758CC /* RelayToggle.swift */,
4CE8794F2996B2BD00F758CC /* RelayStatusView.swift */,
4CE8794F2996B2BD00F758CC /* RelayStatus.swift */,
4CE879512996B68900F758CC /* RelayType.swift */,
4CDA128929E9D10C0006FA5A /* SignalView.swift */,
);
@@ -1219,7 +1145,6 @@
4CB9D4A52992D01900A9A7E4 /* Profile */ = {
isa = PBXGroup;
children = (
4CB8FC222A41ABA500763C51 /* AboutView.swift */,
4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */,
4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */,
F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */,
@@ -1231,7 +1156,6 @@
4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */,
4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */,
4C8D1A6B29F1DFC200ACDF75 /* FriendIcon.swift */,
3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */,
);
path = Profile;
sourceTree = "<group>";
@@ -1294,8 +1218,6 @@
4CE4F0F729DB7399005914DB /* ThiccDivider.swift */,
4C1A9A2229DDDB8100516EAC /* IconLabel.swift */,
4C8D00C929DF80350036AF10 /* TruncatedText.swift */,
4C28595F2A12A2BE004746F7 /* SupporterBadge.swift */,
5C6E1DAC2A193EC2008FC15A /* GradientButtonStyle.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -1310,9 +1232,7 @@
4CE6DEE427F7A08100C66700 /* Products */,
4CEE2AE62804F57B00AB5EEF /* Frameworks */,
);
indentWidth = 4;
sourceTree = "<group>";
tabWidth = 4;
};
4CE6DEE427F7A08100C66700 /* Products */ = {
isa = PBXGroup;
@@ -1373,7 +1293,6 @@
4CB883AD2976FA9300DC99E7 /* FormatTests.swift */,
3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */,
3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */,
5019CADC2A0FB0A9000069E1 /* ProfileDatabaseTests.swift */,
3A3040F229A91366008A0F29 /* ProfileViewTests.swift */,
3A30410029AB12AA008A0F29 /* EventGroupViewTests.swift */,
4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */,
@@ -1416,7 +1335,6 @@
4CE879572996C45300F758CC /* ZapsView.swift */,
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */,
4CA3FA0F29F593D000FDB3C3 /* ZapTypePicker.swift */,
4C73C5132A4437C10062CAC0 /* ZapUserView.swift */,
);
path = Zaps;
sourceTree = "<group>";
@@ -1466,15 +1384,6 @@
path = Images;
sourceTree = "<group>";
};
501F8C5329FF5EE2001AFC1D /* CoreData */ = {
isa = PBXGroup;
children = (
501F8C5429FF5EF6001AFC1D /* PersistedProfile.swift */,
501F8C5629FF5FC5001AFC1D /* Damus.xcdatamodeld */,
);
path = CoreData;
sourceTree = "<group>";
};
7C0F392D29B57C8F0039859C /* Extensions */ = {
isa = PBXGroup;
children = (
@@ -1519,7 +1428,6 @@
packageProductDependencies = (
4C649880286E0EE300EAE2B3 /* secp256k1 */,
4C06670328FC7EC500038D2A /* Kingfisher */,
4CCF9AB12A1FE80C00E03CFB /* GSPlayer */,
);
productName = damus;
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
@@ -1624,7 +1532,6 @@
packageReferences = (
4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */,
4C06670228FC7EC500038D2A /* XCRemoteSwiftPackageReference "Kingfisher" */,
4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */,
);
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
projectDirPath = "";
@@ -1645,7 +1552,6 @@
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */,
4CE6DEEE27F7A08200C66700 /* Preview Assets.xcassets in Resources */,
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */,
50DA11262A16A23F00236234 /* Launch.storyboard in Resources */,
4CE6DEEB27F7A08200C66700 /* Assets.xcassets in Resources */,
4C198DF129F88C6B004C165C /* License.txt in Resources */,
4C198DF029F88C6B004C165C /* Readme.md in Resources */,
@@ -1682,7 +1588,6 @@
4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */,
4C216F34286F5ACD00040376 /* DMView.swift in Sources */,
4C3EA64428FF558100C48A62 /* sha256.c in Sources */,
4CCF9AAF2A1FDBDB00E03CFB /* VideoPlayer.swift in Sources */,
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */,
4C363AA828297703006E126D /* InsertSort.swift in Sources */,
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */,
@@ -1701,7 +1606,6 @@
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
4CEE2AF5280B29E600AB5EEF /* TimeAgo.swift in Sources */,
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */,
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */,
4CB55EF5295E679D007FD187 /* UserRelaysView.swift in Sources */,
4C363AA228296A7E006E126D /* SearchView.swift in Sources */,
4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */,
@@ -1760,7 +1664,6 @@
4CD348EF29C3659D00497EB2 /* ImageUploadModel.swift in Sources */,
4C7D096E2A0AEA0400943473 /* ScannerCoordinator.swift in Sources */,
4C3BEFDC281DCE6100B3DE84 /* Liked.swift in Sources */,
501F8C5A29FF70F5001AFC1D /* ProfileDatabase.swift in Sources */,
4CF0ABE7298444FD00D66079 /* MutedEventView.swift in Sources */,
9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */,
4CF0ABE12981A83900D66079 /* MutelistView.swift in Sources */,
@@ -1786,7 +1689,6 @@
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */,
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
4C7D09682A0AE9B200943473 /* NWCScannerView.swift in Sources */,
4C9AA1482A44442E003F49FD /* CustomizeZapModel.swift in Sources */,
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */,
4C7D09722A0AEF5E00943473 /* DamusGradient.swift in Sources */,
@@ -1826,11 +1728,8 @@
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */,
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */,
F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */,
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */,
4C1A9A2A29DDF54400516EAC /* DamusVideoPlayer.swift in Sources */,
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */,
@@ -1839,7 +1738,6 @@
4C363A9C282838B9006E126D /* EventRef.swift in Sources */,
4C7D095F2A098C5D00943473 /* ConnectWalletView.swift in Sources */,
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */,
5C6E1DAF2A194075008FC15A /* PinkGradient.swift in Sources */,
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */,
4C3EA66528FF5F6800C48A62 /* mem.c in Sources */,
4C198DEF29F88C6B004C165C /* BlurHashEncode.swift in Sources */,
@@ -1855,18 +1753,13 @@
4CB88396296F7F8B00DC99E7 /* ReactionView.swift in Sources */,
4CFF8F6B29CD0079008DB934 /* RepostedEvent.swift in Sources */,
4C8682872814DE470026224F /* ProfileView.swift in Sources */,
5C0707D12A1ECB38004E7B51 /* DamusLogoGradient.swift in Sources */,
4C5F9114283D694D0052CD1C /* FollowTarget.swift in Sources */,
4CF0ABD629817F5B00D66079 /* ReportView.swift in Sources */,
4C1A9A2729DDE31900516EAC /* TranslationSettingsView.swift in Sources */,
4CB8838629656C8B00DC99E7 /* NIP05.swift in Sources */,
4CF0ABD82981980C00D66079 /* Lists.swift in Sources */,
501F8C5829FF5FC5001AFC1D /* Damus.xcdatamodeld in Sources */,
4C30AC8029A6A53F00E2BD5A /* ProfilePicturesView.swift in Sources */,
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */,
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */,
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */,
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */,
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */,
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
@@ -1914,13 +1807,11 @@
4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */,
4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */,
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */,
4CB8FC232A41ABA800763C51 /* AboutView.swift in Sources */,
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */,
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */,
F7908E92298B0F0700AB113A /* RelayDetailView.swift in Sources */,
4C3A1D332960DB0500558C0F /* Markdown.swift in Sources */,
4CE879552996BAB900F758CC /* RelayPaidDetail.swift in Sources */,
501F8C5529FF5EF6001AFC1D /* PersistedProfile.swift in Sources */,
4CF0ABD42980996B00D66079 /* Report.swift in Sources */,
4C06670B28FDE64700038D2A /* damus.c in Sources */,
4C1A9A2529DDDF2600516EAC /* ZapSettingsView.swift in Sources */,
@@ -1929,27 +1820,23 @@
4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */,
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */,
3AE45AF6297BB2E700C1D842 /* LibreTranslateServer.swift in Sources */,
4CE879502996B2BD00F758CC /* RelayStatusView.swift in Sources */,
4CE879502996B2BD00F758CC /* RelayStatus.swift in Sources */,
4CC7AAF4297F18B400430951 /* ReplyDescription.swift in Sources */,
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */,
4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */,
4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */,
501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */,
4C1A9A1D29DDCF9B00516EAC /* NotificationSettingsView.swift in Sources */,
4C75EFB528049D790006080F /* Relay.swift in Sources */,
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */,
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */,
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */,
3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */,
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
4C9AA14A2A4587A6003F49FD /* NotificationStatusModel.swift in Sources */,
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */,
4C2859622A12A7F0004746F7 /* GoldSupportGradient.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1957,7 +1844,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5019CADD2A0FB0A9000069E1 /* ProfileDatabaseTests.swift in Sources */,
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */,
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */,
@@ -2242,7 +2128,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -2257,10 +2143,8 @@
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UILaunchStoryboardName = Launch.storyboard;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -2291,7 +2175,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 24;
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
DEVELOPMENT_TEAM = XK7H4JAB3D;
ENABLE_PREVIEWS = YES;
@@ -2306,10 +2190,8 @@
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UILaunchStoryboardName = Launch.storyboard;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -2340,7 +2222,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = XK7H4JAB3D;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.3;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damusTests;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -2360,7 +2242,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = XK7H4JAB3D;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.3;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damusTests;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -2465,14 +2347,6 @@
revision = 40b4b38b3b1c83f7088c76189a742870e0ca06a9;
};
};
4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/wxxsw/GSPlayer";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.2.26;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
@@ -2486,25 +2360,7 @@
package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */;
productName = secp256k1;
};
4CCF9AB12A1FE80C00E03CFB /* GSPlayer */ = {
isa = XCSwiftPackageProductDependency;
package = 4CCF9AB02A1FE80B00E03CFB /* XCRemoteSwiftPackageReference "GSPlayer" */;
productName = GSPlayer;
};
/* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */
501F8C5629FF5FC5001AFC1D /* Damus.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
501F8C5729FF5FC5001AFC1D /* Damus.xcdatamodel */,
);
currentVersion = 501F8C5729FF5FC5001AFC1D /* Damus.xcdatamodel */;
path = Damus.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
};
/* End XCVersionGroup section */
};
rootObject = 4CE6DEDB27F7A08100C66700 /* Project object */;
}

View File

@@ -1,14 +1,5 @@
{
"pins" : [
{
"identity" : "gsplayer",
"kind" : "remoteSourceControl",
"location" : "https://github.com/wxxsw/GSPlayer",
"state" : {
"revision" : "aa6dad7943d52f5207f7fcc2ad3e4274583443b8",
"version" : "0.2.26"
}
},
{
"identity" : "kingfisher",
"kind" : "remoteSourceControl",

View File

@@ -1,28 +0,0 @@
<svg width="430" height="813" viewBox="0 0 430 813" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_f_1069_29012)">
<path d="M44.0811 256.851L186.315 111L276.203 223.574L244.02 388.295L69.9751 697.084L100.678 498.338L44.0811 256.851Z" fill="url(#paint0_linear_1069_29012)"/>
</g>
<g filter="url(#filter1_f_1069_29012)">
<path d="M116.509 587.348L206.677 479.401L230.746 273.265L266.666 231.183L367.424 396.975L281.292 659.008L266.665 801.413L66.889 763.694L116.509 587.348Z" fill="url(#paint1_linear_1069_29012)"/>
</g>
<defs>
<filter id="filter0_f_1069_29012" x="-66.6248" y="0.294121" width="453.534" height="807.496" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="55.3529" result="effect1_foregroundBlur_1069_29012"/>
</filter>
<filter id="filter1_f_1069_29012" x="-43.8172" y="120.477" width="521.947" height="791.642" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="55.3529" result="effect1_foregroundBlur_1069_29012"/>
</filter>
<linearGradient id="paint0_linear_1069_29012" x1="230.179" y1="166.577" x2="-67.7956" y2="310.108" gradientUnits="userSpaceOnUse">
<stop stop-color="#D34CD9"/>
<stop offset="1" stop-color="#4E4DF4"/>
</linearGradient>
<linearGradient id="paint1_linear_1069_29012" x1="139.483" y1="462.902" x2="377.854" y2="565.47" gradientUnits="userSpaceOnUse">
<stop stop-color="#0DE8FF"/>
<stop offset="1" stop-color="#641AAE"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,12 +0,0 @@
{
"images" : [
{
"filename" : "gradient.jpg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,14 +0,0 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect opacity="0.2" width="48" height="48" rx="24" fill="url(#paint0_linear_1843_42349)"/>
<path d="M19.9993 36.0001H27.9993M28.8095 28.0001C31.5199 26.3669 33.3327 23.3952 33.3327 20.0001C33.3327 14.8454 29.154 10.6667 23.9993 10.6667C18.8447 10.6667 14.666 14.8454 14.666 20.0001C14.666 23.3952 16.4788 26.3669 19.1892 28.0001M28.8095 28.0001C28.5475 28.1579 28.2772 28.3032 27.9993 28.4352V31.3334C27.9993 31.7016 27.7009 32.0001 27.3327 32.0001H20.666C20.2978 32.0001 19.9993 31.7016 19.9993 31.3334V28.4352C19.7215 28.3032 19.4512 28.1579 19.1892 28.0001M28.8095 28.0001H19.1892" stroke="url(#paint1_linear_1843_42349)" stroke-width="2.66667" stroke-linecap="round" stroke-linejoin="round"/>
<defs>
<linearGradient id="paint0_linear_1843_42349" x1="5.41935" y1="0.774194" x2="37.9355" y2="47.2258" gradientUnits="userSpaceOnUse">
<stop stop-color="#F9AD1C"/>
<stop offset="1" stop-color="#DF7E0C"/>
</linearGradient>
<linearGradient id="paint1_linear_1843_42349" x1="16.7735" y1="11.0754" x2="35.0141" y2="30.2759" gradientUnits="userSpaceOnUse">
<stop stop-color="#F9AD1C"/>
<stop offset="1" stop-color="#DF7E0C"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,12 +0,0 @@
{
"images" : [
{
"filename" : "header.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -1,12 +0,0 @@
{
"images" : [
{
"filename" : "nostr-logo.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,12 +1,15 @@
{
"images" : [
{
"filename" : "eula-bg.svg",
"filename" : "shaka-full.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@@ -0,0 +1,88 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.000000 -0.073975 cm
1.000000 1.000000 1.000000 scn
1.295334 8.661732 m
3.613694 8.367855 l
4.475733 8.733568 5.268113 9.771931 5.474915 10.327032 c
6.083156 11.959681 5.507567 14.604573 5.474915 15.061715 c
5.448792 15.427428 6.008246 15.693006 6.291239 15.780080 c
7.571236 15.858447 8.508359 14.876789 8.642253 13.984165 c
8.740212 13.331103 8.576948 11.752880 8.381030 10.849482 c
8.979668 10.936556 10.980525 10.901726 11.868687 10.849482 c
12.756847 10.797236 13.474895 10.196423 14.193260 9.412750 c
14.767952 8.237244 13.953805 7.725680 13.474895 7.616838 c
13.834077 7.257654 l
14.781013 5.918882 13.649043 5.178749 13.115711 5.004600 c
13.474895 4.743376 l
14.487136 3.763786 13.246323 2.751544 13.017752 2.882155 c
11.058574 3.176033 l
15.499378 1.673996 l
16.054478 0.400530 15.074889 0.073999 14.781013 0.073999 c
8.576947 1.673996 l
6.291239 1.673996 5.311650 1.869914 4.299407 2.163791 c
4.157911 2.131138 3.659409 1.987464 2.797370 1.673996 c
1.935332 1.360527 1.219143 2.087601 0.968804 2.490320 c
-0.285071 4.083785 -0.467927 7.257655 1.295334 8.661732 c
h
f
n
Q
endstream
endobj
3 0 obj
1149
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 15.666626 15.710510 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001239 00000 n
0000001262 00000 n
0000001435 00000 n
0000001509 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1568
%%EOF

View File

@@ -1,12 +1,15 @@
{
"images" : [
{
"filename" : "lightbulb.svg",
"filename" : "shaka-line.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@@ -0,0 +1,323 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 0.474731 -0.563965 cm
1.000000 1.000000 1.000000 scn
3.613694 9.332577 m
3.553993 8.861599 l
3.637261 8.851044 3.721838 8.862753 3.799107 8.895533 c
3.613694 9.332577 l
h
1.295334 9.626453 m
1.355035 10.097433 l
1.227973 10.113539 1.099794 10.077623 0.999601 9.997839 c
1.295334 9.626453 l
h
0.968804 3.455042 m
1.372000 3.705677 l
1.362764 3.720535 1.352713 3.734872 1.341894 3.748621 c
0.968804 3.455042 l
h
4.299407 3.128512 m
4.431771 3.584435 l
4.353942 3.607030 4.271623 3.609325 4.192656 3.591103 c
4.299407 3.128512 l
h
8.576947 2.638718 m
8.695503 3.098424 l
8.656776 3.108411 8.616942 3.113465 8.576947 3.113465 c
8.576947 2.638718 l
h
14.781013 1.038721 m
14.662457 0.579016 l
14.701184 0.569027 14.741018 0.563974 14.781013 0.563974 c
14.781013 1.038721 l
h
15.499378 2.638718 m
15.934578 2.828420 l
15.881091 2.951125 15.778289 3.045548 15.651489 3.088437 c
15.499378 2.638718 l
h
11.058574 4.140755 m
11.128998 4.610250 l
10.885809 4.646729 10.655017 4.491467 10.597156 4.252461 c
10.539293 4.013455 10.673516 3.769826 10.906463 3.691035 c
11.058574 4.140755 l
h
13.017752 3.846877 m
13.253292 4.259073 l
13.202273 4.288227 13.146286 4.307655 13.088176 4.316372 c
13.017752 3.846877 l
h
13.474895 5.708097 m
13.805044 6.049252 l
13.789093 6.064689 13.772079 6.078987 13.754128 6.092043 c
13.474895 5.708097 l
h
13.115711 5.969321 m
12.968349 6.420619 l
12.798800 6.365256 12.674588 6.219535 12.646772 6.043359 c
12.618958 5.867183 12.692234 5.690281 12.836478 5.585376 c
13.115711 5.969321 l
h
13.834077 8.222376 m
14.221668 8.496526 l
14.206144 8.518474 14.188784 8.539063 14.169774 8.558073 c
13.834077 8.222376 l
h
13.474895 8.581559 m
13.369680 9.044500 l
13.201114 9.006190 13.066693 8.879284 13.018762 8.713197 c
12.970830 8.547110 13.016963 8.368095 13.139197 8.245862 c
13.474895 8.581559 l
h
14.193260 10.377472 m
14.619765 10.585986 l
14.599768 10.626891 14.573989 10.664707 14.543221 10.698271 c
14.193260 10.377472 l
h
8.381030 11.814203 m
7.917068 11.914822 l
7.884080 11.762714 7.927746 11.604099 8.033934 11.490305 c
8.140121 11.376513 8.295343 11.321997 8.449365 11.344399 c
8.381030 11.814203 l
h
8.642253 14.948887 m
9.111748 15.019311 l
8.642253 14.948887 l
h
6.291239 16.744801 m
6.262227 17.218662 l
6.224693 17.216364 6.187564 17.209614 6.151623 17.198555 c
6.291239 16.744801 l
h
5.474915 16.026436 m
5.948456 16.060261 l
5.474915 16.026436 l
h
5.474915 11.291754 m
5.030037 11.457493 l
5.474915 11.291754 l
h
3.673396 9.803555 m
1.355035 10.097433 l
1.235632 9.155476 l
3.553993 8.861599 l
3.673396 9.803555 l
h
0.999601 9.997839 m
-0.029049 9.178730 -0.454726 7.875908 -0.474048 6.619066 c
-0.493367 5.362488 -0.110331 4.058727 0.595713 3.161463 c
1.341894 3.748621 l
0.794064 4.444821 0.458734 5.524729 0.475334 6.604470 c
0.491930 7.683949 0.856455 8.670100 1.591066 9.255068 c
0.999601 9.997839 l
h
0.565608 3.204407 m
0.721970 2.952868 1.013515 2.611341 1.407507 2.372385 c
1.811404 2.127421 2.357187 1.973489 2.959612 2.192553 c
2.635129 3.084882 l
2.375515 2.990478 2.132184 3.043347 1.899893 3.184233 c
1.657696 3.331126 1.465977 3.554496 1.372000 3.705677 c
0.565608 3.204407 l
h
2.959612 2.192553 m
3.816493 2.504146 4.293336 2.639887 4.406158 2.665923 c
4.192656 3.591103 l
4.022485 3.551832 3.502325 3.400227 2.635129 3.084882 c
2.959612 2.192553 l
h
4.167043 2.672591 m
5.229115 2.364247 6.254152 2.163970 8.576947 2.163970 c
8.576947 3.113465 l
6.328326 3.113465 5.394184 3.305025 4.431771 3.584435 c
4.167043 2.672591 l
h
8.458392 2.179011 m
14.662457 0.579016 l
14.899569 1.498427 l
8.695503 3.098424 l
8.458392 2.179011 l
h
14.781013 0.563974 m
15.036198 0.563974 15.495326 0.684875 15.814721 1.047266 c
16.180891 1.462728 16.264221 2.072176 15.934578 2.828420 c
15.064179 2.449016 l
15.289635 1.931793 15.160722 1.741243 15.102402 1.675073 c
15.055794 1.622190 14.990156 1.579316 14.916806 1.549556 c
14.881134 1.535082 14.847747 1.525430 14.820526 1.519657 c
14.791491 1.513498 14.777695 1.513469 14.781013 1.513469 c
14.781013 0.563974 l
h
15.651489 3.088437 m
11.210685 4.590474 l
10.906463 3.691035 l
15.347267 2.188998 l
15.651489 3.088437 l
h
10.988150 3.671260 m
12.947328 3.377382 l
13.088176 4.316372 l
11.128998 4.610250 l
10.988150 3.671260 l
h
12.782211 3.434681 m
12.991495 3.315090 13.204453 3.370091 13.288217 3.396689 c
13.400116 3.432221 13.506123 3.490767 13.598186 3.554502 c
13.783985 3.683133 13.977411 3.877748 14.120350 4.119644 c
14.264680 4.363894 14.369576 4.678114 14.335162 5.031647 c
14.300108 5.391746 14.125634 5.739002 13.805044 6.049252 c
13.144745 5.366943 l
13.330275 5.187398 13.380290 5.040778 13.390134 4.939653 c
13.400617 4.831963 13.370820 4.717613 13.302905 4.602680 c
13.233600 4.485394 13.137231 4.390213 13.057724 4.335170 c
13.017135 4.307070 12.996612 4.300308 13.000857 4.301657 c
13.003194 4.302399 13.024761 4.309311 13.061064 4.310122 c
13.095938 4.310902 13.170414 4.306433 13.253292 4.259073 c
12.782211 3.434681 l
h
13.754128 6.092043 m
13.394944 6.353267 l
12.836478 5.585376 l
13.195662 5.324152 l
13.754128 6.092043 l
h
13.263074 5.518023 m
13.593105 5.625790 14.123367 5.907292 14.433812 6.409482 c
14.595931 6.671733 14.696482 6.993351 14.669847 7.364054 c
14.643518 7.730516 14.495621 8.109214 14.221668 8.496526 c
13.446486 7.948226 l
13.646002 7.666152 13.711709 7.450294 13.722795 7.296009 c
13.733575 7.145966 13.695351 7.020646 13.626177 6.908748 c
13.474038 6.662641 13.171650 6.487002 12.968349 6.420619 c
13.263074 5.518023 l
h
14.169774 8.558073 m
13.810592 8.917255 l
13.139197 8.245862 l
13.498380 7.886679 l
14.169774 8.558073 l
h
13.580109 8.118617 m
13.896242 8.190466 14.344993 8.395787 14.624650 8.816864 c
14.929440 9.275781 14.963785 9.882310 14.619765 10.585986 c
13.766754 10.168959 l
13.997427 9.697128 13.912044 9.460121 13.833706 9.342171 c
13.730235 9.186377 13.532457 9.081495 13.369680 9.044500 c
13.580109 8.118617 l
h
14.543221 10.698271 m
13.820906 11.486253 12.989320 12.223852 11.896564 12.288132 c
11.840808 11.340275 l
12.524374 11.300065 13.128883 10.836036 13.843298 10.056674 c
14.543221 10.698271 l
h
11.896564 12.288132 m
11.441970 12.314873 10.711069 12.336796 10.019300 12.341186 c
9.341933 12.345484 8.654247 12.333687 8.312695 12.284006 c
8.449365 11.344399 l
8.706450 11.381794 9.318512 11.396118 10.013274 11.391710 c
10.693633 11.387392 11.407242 11.365778 11.840808 11.340275 c
11.896564 12.288132 l
h
8.844993 11.713585 m
8.948084 12.188952 9.040332 12.829445 9.094679 13.432834 c
9.147870 14.023395 9.169946 14.631327 9.111748 15.019311 c
8.172758 14.878462 l
8.212520 14.613384 8.201942 14.105675 8.149012 13.518009 c
8.097237 12.943172 8.009893 12.342852 7.917068 11.914822 c
8.844993 11.713585 l
h
9.111748 15.019311 m
8.944062 16.137217 7.805658 17.313158 6.262227 17.218662 c
6.320251 16.270941 l
7.336813 16.333179 8.072657 15.545805 8.172758 14.878462 c
9.111748 15.019311 l
h
6.151623 17.198555 m
5.976391 17.144638 5.715709 17.036982 5.490986 16.876261 c
5.292936 16.734617 4.969444 16.439627 5.001374 15.992612 c
5.948456 16.060261 l
5.951383 16.019283 5.934667 15.999795 5.943361 16.012491 c
5.954769 16.029152 5.984430 16.061831 6.043331 16.103956 c
6.162553 16.189222 6.323094 16.257891 6.430855 16.291048 c
6.151623 17.198555 l
h
5.001374 15.992612 m
5.011176 15.855374 5.059216 15.566318 5.104405 15.255149 c
5.152757 14.922197 5.207128 14.509316 5.241940 14.062993 c
5.312967 13.152368 5.295928 12.171200 5.030037 11.457493 c
5.919792 11.126015 l
6.262142 12.044956 6.261431 13.202559 6.188560 14.136827 c
6.151423 14.612950 6.093790 15.049047 6.044043 15.391605 c
5.991133 15.755945 5.954979 15.968927 5.948456 16.060261 c
5.001374 15.992612 l
h
5.030037 11.457493 m
4.953650 11.252455 4.742510 10.903708 4.434547 10.555828 c
4.127778 10.209298 3.769400 9.914337 3.428282 9.769621 c
3.799107 8.895533 l
4.320028 9.116529 4.788858 9.523607 5.145489 9.926461 c
5.500926 10.327968 5.789377 10.775953 5.919792 11.126015 c
5.030037 11.457493 l
h
f
n
Q
endstream
endobj
3 0 obj
7995
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 16.615845 16.660034 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000008085 00000 n
0000008108 00000 n
0000008281 00000 n
0000008355 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
8414
%%EOF

View File

@@ -1,47 +0,0 @@
//
// GradientButtonStyle.swift
// damus
//
// Created by eric on 5/20/23.
//
import SwiftUI
struct GradientButtonStyle: ButtonStyle {
func makeBody(configuration: Self.Configuration) -> some View {
return configuration.label
.padding()
.foregroundColor(Color.white)
.background {
RoundedRectangle(cornerRadius: 12)
.fill(PinkGradient.gradient)
}
.scaleEffect(configuration.isPressed ? 0.8 : 1)
}
}
struct GradientButtonStyle_Previews: PreviewProvider {
static var previews: some View {
VStack {
Button(action: {
print("dynamic size")
}) {
Text(verbatim: "Dynamic Size")
}
.buttonStyle(GradientButtonStyle())
Button(action: {
print("infinite width")
}) {
HStack {
Text(verbatim: "Infinite Width")
}
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
}
.buttonStyle(GradientButtonStyle())
.padding()
}
}
}

View File

@@ -14,13 +14,9 @@ fileprivate let damus_grad = [damus_grad_c1, damus_grad_c2, damus_grad_c3]
struct DamusGradient: View {
var body: some View {
DamusGradient.gradient
LinearGradient(colors: damus_grad, startPoint: .bottomLeading, endPoint: .topTrailing)
.edgesIgnoringSafeArea([.top,.bottom])
}
static var gradient: LinearGradient {
LinearGradient(colors: damus_grad, startPoint: .bottomLeading, endPoint: .topTrailing)
}
}
struct DamusGradient_Previews: PreviewProvider {

View File

@@ -1,29 +0,0 @@
//
// DamusLogoGradient.swift
// damus
//
// Created by eric on 5/24/23.
//
import SwiftUI
fileprivate let damus_logo_grad_c1 = hex_col(r: 0x30, g: 0xb3, b: 0xf1)
fileprivate let damus_logo_grad_c2 = hex_col(r: 0xc5, g: 0x39, b: 0xf9)
fileprivate let damus_logo_grad = [damus_logo_grad_c1, damus_logo_grad_c2]
struct DamusLogoGradient: View {
var body: some View {
DamusLogoGradient.gradient
.edgesIgnoringSafeArea([.top,.bottom])
}
static var gradient: LinearGradient {
LinearGradient(colors: damus_logo_grad, startPoint: .leading, endPoint: .trailing)
}
}
struct DamusLogoGradient_Previews: PreviewProvider {
static var previews: some View {
DamusLogoGradient()
}
}

View File

@@ -1,29 +0,0 @@
//
// GoldSupportGradient.swift
// damus
//
// Created by William Casarin on 2023-05-15.
//
import SwiftUI
fileprivate let gold_grad_c1 = hex_col(r: 226, g: 168, b: 0)
fileprivate let gold_grad_c2 = hex_col(r: 249, g: 243, b: 100)
fileprivate let gold_grad = [gold_grad_c2, gold_grad_c1]
let GoldGradient: LinearGradient =
LinearGradient(colors: gold_grad, startPoint: .bottomLeading, endPoint: .topTrailing)
struct GoldGradientView: View {
var body: some View {
GoldGradient
.edgesIgnoringSafeArea([.top,.bottom])
}
}
struct GoldGradientView_Previews: PreviewProvider {
static var previews: some View {
GoldGradientView()
}
}

View File

@@ -1,30 +0,0 @@
//
// PinkGradient.swift
// damus
//
// Created by eric on 5/20/23.
//
import SwiftUI
fileprivate let damus_grad_c1 = hex_col(r: 0xd3, g: 0x4c, b: 0xd9)
fileprivate let damus_grad_c2 = hex_col(r: 0xf8, g: 0x69, b: 0xb6)
fileprivate let pink_grad = [damus_grad_c1, damus_grad_c2]
struct PinkGradient: View {
var body: some View {
PinkGradient.gradient
.edgesIgnoringSafeArea([.top,.bottom])
}
static var gradient: LinearGradient {
LinearGradient(colors: pink_grad, startPoint: .topTrailing, endPoint: .bottom)
}
}
struct PinkGradient_Previews: PreviewProvider {
static var previews: some View {
PinkGradient()
}
}

View File

@@ -21,7 +21,7 @@ struct IconLabel: View {
var body: some View {
HStack(spacing: 0) {
Image(img_name)
Image(systemName: img_name)
.foregroundColor(img_color)
.frame(width: 20)
.padding([.trailing], 20)

View File

@@ -52,10 +52,8 @@ enum ImageShape {
}
}
// MARK: - Image Carousel
@MainActor
struct ImageCarousel: View {
var urls: [MediaUrl]
var urls: [URL]
let evid: String
@@ -64,19 +62,14 @@ struct ImageCarousel: View {
@State private var open_sheet: Bool = false
@State private var current_url: URL? = nil
@State private var image_fill: ImageFill? = nil
@State private var fillHeight: CGFloat = 350
@State private var maxHeight: CGFloat = UIScreen.main.bounds.height * 1.2 // 1.2
@State private var firstImageHeight: CGFloat? = nil
@State private var currentImageHeight: CGFloat?
@State private var selectedIndex = 0
@State private var video_size: CGSize? = nil
init(state: DamusState, evid: String, urls: [MediaUrl]) {
let fillHeight: CGFloat = 350
let maxHeight: CGFloat = UIScreen.main.bounds.height * 1.2
init(state: DamusState, evid: String, urls: [URL]) {
_open_sheet = State(initialValue: false)
_current_url = State(initialValue: nil)
let media_model = state.events.get_cache_data(evid).media_metadata_model
_image_fill = State(initialValue: media_model.fill)
_image_fill = State(initialValue: state.previews.lookup_image_meta(evid))
self.urls = urls
self.evid = evid
self.state = state
@@ -87,154 +80,75 @@ struct ImageCarousel: View {
}
var height: CGFloat {
firstImageHeight ?? image_fill?.height ?? fillHeight
image_fill?.height ?? fillHeight
}
func Placeholder(url: URL, geo_size: CGSize, num_urls: Int) -> some View {
func Placeholder(url: URL, geo_size: CGSize) -> some View {
Group {
if num_urls > 1 {
// jb55: quick hack since carousel with multiple images looks horrible with blurhash background
Color.clear
} else if let meta = state.events.lookup_img_metadata(url: url),
if let meta = state.events.lookup_img_metadata(url: url),
case .processed(let blurhash) = meta.state {
Image(uiImage: blurhash)
.resizable()
.frame(width: geo_size.width * UIScreen.main.scale, height: self.height * UIScreen.main.scale)
} else {
Color.clear
EmptyView()
}
}
.onAppear {
if self.image_fill == nil, let size = state.events.lookup_media_size(url: url) {
if self.image_fill == nil,
let meta = state.events.lookup_img_metadata(url: url),
let size = meta.meta.dim?.size
{
let fill = ImageFill.calculate_image_fill(geo_size: geo_size, img_size: size, maxHeight: maxHeight, fillHeight: fillHeight)
self.image_fill = fill
}
}
}
func video_model(_ url: URL) -> VideoPlayerModel {
return state.events.get_video_player_model(url: url)
}
func Media(geo: GeometryProxy, url: MediaUrl, index: Int) -> some View {
Group {
switch url {
case .image(let url):
Img(geo: geo, url: url, index: index)
.onTapGesture {
open_sheet = true
}
case .video(let url):
DamusVideoPlayer(url: url, model: video_model(url), video_size: $video_size)
.onChange(of: video_size) { size in
guard let size else { return }
let fill = ImageFill.calculate_image_fill(geo_size: geo.size, img_size: size, maxHeight: maxHeight, fillHeight: fillHeight)
print("video_size changed \(size)")
if self.image_fill == nil {
print("video_size firstImageHeight \(fill.height)")
firstImageHeight = fill.height
state.events.get_cache_data(evid).media_metadata_model.fill = fill
}
self.image_fill = fill
}
}
}
}
func Img(geo: GeometryProxy, url: URL, index: Int) -> some View {
KFAnimatedImage(url)
.callbackQueue(.dispatch(.global(qos:.background)))
.backgroundDecode(true)
.imageContext(.note, disable_animation: state.settings.disable_animation)
.image_fade(duration: 0.25)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in
state.events.get_cache_data(evid).media_metadata_model.fill = fill
// blur hash can be discarded when we have the url
// NOTE: this is the wrong place for this... we need to remove
// it when the image is loaded in memory. This may happen
// earlier than this (by the preloader, etc)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
state.events.lookup_img_metadata(url: url)?.state = .not_needed
}
image_fill = fill
if index == 0 {
firstImageHeight = fill.height
//maxHeight = firstImageHeight ?? maxHeight
} else {
//maxHeight = firstImageHeight ?? fill.height
}
}
.background {
Placeholder(url: url, geo_size: geo.size, num_urls: urls.count)
}
.aspectRatio(contentMode: filling ? .fill : .fit)
.position(x: geo.size.width / 2, y: geo.size.height / 2)
.tabItem {
Text(url.absoluteString)
}
.id(url.absoluteString)
.padding(0)
}
var Medias: some View {
TabView(selection: $selectedIndex) {
ForEach(urls.indices, id: \.self) { index in
var body: some View {
TabView {
ForEach(urls, id: \.absoluteString) { url in
GeometryReader { geo in
Media(geo: geo, url: urls[index], index: index)
KFAnimatedImage(url)
.callbackQueue(.dispatch(.global(qos:.background)))
.backgroundDecode(true)
.imageContext(.note, disable_animation: state.settings.disable_animation)
.image_fade(duration: 0.25)
.cancelOnDisappear(true)
.configure { view in
view.framePreloadCount = 3
}
.imageFill(for: geo.size, max: maxHeight, fill: fillHeight) { fill in
state.previews.cache_image_meta(evid: evid, image_fill: fill)
// blur hash can be discarded when we have the url
// NOTE: this is the wrong place for this... we need to remove
// it when the image is loaded in memory. This may happen
// earlier than this (by the preloader, etc)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
state.events.lookup_img_metadata(url: url)?.state = .not_needed
}
image_fill = fill
}
.background {
Placeholder(url: url, geo_size: geo.size)
}
.aspectRatio(contentMode: filling ? .fill : .fit)
.tabItem {
Text(url.absoluteString)
}
.id(url.absoluteString)
}
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.fullScreenCover(isPresented: $open_sheet) {
ImageView(cache: state.events, urls: urls, disable_animation: state.settings.disable_animation)
ImageView(urls: urls, disable_animation: state.settings.disable_animation)
}
.frame(height: height)
.onChange(of: selectedIndex) { value in
selectedIndex = value
.frame(height: self.height)
.onTapGesture {
open_sheet = true
}
.tabViewStyle(PageTabViewStyle())
}
var body: some View {
VStack {
Medias
.onTapGesture { }
// This is our custom carousel image indicator
CarouselDotsView(urls: urls, selectedIndex: $selectedIndex)
}
}
}
// MARK: - Custom Carousel
struct CarouselDotsView<T>: View {
let urls: [T]
@Binding var selectedIndex: Int
var body: some View {
if urls.count > 1 {
HStack {
ForEach(urls.indices, id: \.self) { index in
Circle()
.fill(index == selectedIndex ? Color("DamusPurple") : Color("DamusLightGrey"))
.frame(width: 10, height: 10)
.onTapGesture {
selectedIndex = index
}
}
}
.padding(.top, CGFloat(8))
.id(UUID())
}
}
}
// MARK: - Image Modifier
@@ -285,11 +199,9 @@ public struct ImageFill {
}
}
// MARK: - Preview Provider
struct ImageCarousel_Previews: PreviewProvider {
static var previews: some View {
let url: MediaUrl = .image(URL(string: "https://jb55.com/red-me.jpg")!)
ImageCarousel(state: test_damus_state(), evid: "evid", urls: [url, url])
ImageCarousel(state: test_damus_state(), evid: "evid", urls: [URL(string: "https://jb55.com/red-me.jpg")!,URL(string: "https://jb55.com/red-me.jpg")!])
}
}

View File

@@ -25,10 +25,10 @@ struct InvoiceView: View {
UIPasteboard.general.string = invoice.string
} label: {
if !copied {
Image("copy2")
Image(systemName: "doc.on.clipboard")
.foregroundColor(.gray)
} else {
Image("check-circle")
Image(systemName: "checkmark.circle")
.foregroundColor(DamusColors.green)
}
}
@@ -37,7 +37,7 @@ struct InvoiceView: View {
var PayButton: some View {
Button {
if settings.show_wallet_selector {
present_sheet(.select_wallet(invoice: invoice.string))
showing_select_wallet = true
} else {
open_with_wallet(wallet: settings.default_wallet.model, invoice: invoice.string)
}
@@ -63,7 +63,7 @@ struct InvoiceView: View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("", image: "zap.fill")
Label("", systemImage: "bolt.fill")
.foregroundColor(.orange)
Text("Lightning Invoice", comment: "Indicates that the view is for paying a Lightning invoice.")
Spacer()
@@ -79,6 +79,9 @@ struct InvoiceView: View {
}
.padding(30)
}
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
SelectWalletView(default_wallet: settings.default_wallet, showingSelectWallet: $showing_select_wallet, our_pubkey: our_pubkey, invoice: invoice.string)
}
}
}
@@ -113,7 +116,3 @@ struct InvoiceView_Previews: PreviewProvider {
}
}
func present_sheet(_ sheet: Sheets) {
notify(.present_sheet, sheet)
}

View File

@@ -32,11 +32,11 @@ struct NIP05Badge: View {
Group {
if nip05_color {
LINEAR_GRADIENT
.mask(Image("check-circle.fill")
.mask(Image(systemName: "checkmark.seal.fill")
.resizable()
).frame(width: 14, height: 14)
} else if show_domain {
Image("check-circle.fill")
Image(systemName: "checkmark.seal.fill")
.font(.footnote)
.nip05_colorized(gradient: nip05_color)
}

View File

@@ -14,11 +14,11 @@ struct Reposted: View {
var body: some View {
HStack(alignment: .center) {
Image("repost")
Image(systemName: "arrow.2.squarepath")
.foregroundColor(Color.gray)
ProfileName(pubkey: pubkey, profile: profile, damus: damus, show_nip5_domain: false)
.foregroundColor(Color.gray)
Text("Reposted", comment: "Text indicating that the note was reposted (i.e. re-shared).")
Text("Reposted", comment: "Text indicating that the post was reposted (i.e. re-shared).")
.foregroundColor(Color.gray)
}
}

View File

@@ -1,73 +0,0 @@
//
// SupporterBadge.swift
// damus
//
// Created by William Casarin on 2023-05-15.
//
import SwiftUI
struct SupporterBadge: View {
let percent: Int
let size: CGFloat = 17
var body: some View {
if percent < 100 {
Image("star.fill")
.resizable()
.frame(width:size, height:size)
.foregroundColor(support_level_color(percent))
} else {
Image("star.fill")
.resizable()
.frame(width:size, height:size)
.foregroundStyle(GoldGradient)
}
}
}
func support_level_color(_ percent: Int) -> Color {
if percent == 0 {
return .gray
}
let percent_f = Double(percent) / 100.0
let cutoff = 0.5
let h = cutoff + (percent_f * cutoff); // Hue (note 0.2 = Green, see huge chart below)
let s = 0.9; // Saturation
let b = 0.9; // Brightness
return Color(hue: h, saturation: s, brightness: b)
}
struct SupporterBadge_Previews: PreviewProvider {
static func Level(_ p: Int) -> some View {
HStack(alignment: .center) {
SupporterBadge(percent: p)
.frame(width: 50)
Text(verbatim: p.formatted())
.frame(width: 50)
}
}
static var previews: some View {
VStack(spacing: 0) {
VStack(spacing: 0) {
Level(1)
Level(10)
Level(20)
Level(30)
Level(40)
Level(50)
}
Level(60)
Level(70)
Level(80)
Level(90)
Level(100)
}
}
}

View File

@@ -12,7 +12,7 @@ struct TruncatedText: View {
let maxChars: Int = 280
var body: some View {
let truncatedAttributedString: AttributedString? = text.attributed.truncateOrNil(maxLength: maxChars)
let truncatedAttributedString: AttributedString? = getTruncatedString()
if let truncatedAttributedString {
Text(truncatedAttributedString)
@@ -28,6 +28,16 @@ struct TruncatedText: View {
.allowsHitTesting(false)
}
}
func getTruncatedString() -> AttributedString? {
let nsAttributedString = NSAttributedString(text.attributed)
if nsAttributedString.length < maxChars { return nil }
let range = NSRange(location: 0, length: maxChars)
let truncatedAttributedString = nsAttributedString.attributedSubstring(from: range)
return AttributedString(truncatedAttributedString) + "..."
}
}
struct TruncatedText_Previews: PreviewProvider {

View File

@@ -11,27 +11,30 @@ struct UserViewRow: View {
let damus_state: DamusState
let pubkey: String
@State var navigating: Bool = false
var body: some View {
let dest = ProfileView(damus_state: damus_state, pubkey: pubkey)
UserView(damus_state: damus_state, pubkey: pubkey)
.contentShape(Rectangle())
.background(.clear)
.background(
NavigationLink(destination: dest, isActive: $navigating) {
EmptyView()
}
)
.onTapGesture {
navigating = true
}
}
}
struct UserView: View {
let damus_state: DamusState
let pubkey: String
let spacer: Bool
@State var about_text: Text? = nil
init(damus_state: DamusState, pubkey: String, spacer: Bool = true) {
self.damus_state = damus_state
self.pubkey = pubkey
self.spacer = spacer
}
var body: some View {
VStack {
HStack {
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
@@ -39,16 +42,14 @@ struct UserView: View {
VStack(alignment: .leading) {
let profile = damus_state.profiles.lookup(id: pubkey)
ProfileName(pubkey: pubkey, profile: profile, damus: damus_state, show_nip5_domain: false)
if let about_text {
about_text
if let about = profile?.about {
Text(about)
.lineLimit(3)
.font(.footnote)
}
}
if spacer {
Spacer()
}
Spacer()
}
}
}

View File

@@ -13,7 +13,7 @@ struct WebsiteLink: View {
var body: some View {
HStack {
Image("link")
Image(systemName: "link")
.foregroundColor(.gray)
.font(.footnote)
@@ -23,8 +23,6 @@ struct WebsiteLink: View {
Text(link_text)
.font(.footnote)
.foregroundColor(.accentColor)
.truncationMode(.tail)
.lineLimit(1)
})
}
}

View File

@@ -10,41 +10,46 @@ import SwiftUI
enum ZappingEventType {
case failed(ZappingError)
case got_zap_invoice(String)
case sent_from_nwc
}
enum ZappingError {
case fetching_invoice
case bad_lnurl
case canceled
case send_failed
}
struct ZappingEvent {
let is_custom: Bool
let type: ZappingEventType
let target: ZapTarget
let event: NostrEvent
}
class ZapButtonModel: ObservableObject {
var invoice: String? = nil
@Published var zapping: String = ""
@Published var showing_select_wallet: Bool = false
@Published var showing_zap_customizer: Bool = false
}
struct ZapButton: View {
let damus_state: DamusState
let target: ZapTarget
let event: NostrEvent
let lnurl: String
@ObservedObject var zaps: ZapsDataModel
@StateObject var button: ZapButtonModel = ZapButtonModel()
var our_zap: Zapping? {
zaps.zaps.first(where: { z in z.request.ev.pubkey == damus_state.pubkey })
zaps.zaps.first(where: { z in z.request.pubkey == damus_state.pubkey })
}
var zap_img: String {
switch our_zap {
case .none:
return "zap"
return "bolt"
case .zap:
return "zap.fill"
return "bolt.fill"
case .pending:
return "zap.fill"
return "bolt.fill"
}
}
@@ -55,11 +60,18 @@ struct ZapButton: View {
// always orange !
return Color.orange
/*
if our_zap.is_paid {
return Color.orange
} else {
return Color.yellow
}
*/
}
func tap() {
guard let our_zap else {
send_zap(damus_state: damus_state, target: target, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type)
send_zap(damus_state: damus_state, event: event, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type)
return
}
@@ -106,17 +118,12 @@ struct ZapButton: View {
var body: some View {
HStack(spacing: 4) {
if !damus_state.settings.nozaps || zaps.zap_total > 0 {
Button(action: {
}, label: {
Image(zap_img)
.resizable()
.foregroundColor(zap_color)
.font(.footnote.weight(.medium))
.aspectRatio(contentMode: .fit)
.frame(width:20, height: 20)
})
}
Button(action: {
}, label: {
Image(systemName: zap_img)
.foregroundColor(zap_color)
.font(.footnote.weight(.medium))
})
if zaps.zap_total > 0 {
Text(verbatim: format_msats_abbrev(zaps.zap_total))
@@ -126,15 +133,41 @@ struct ZapButton: View {
}
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
.simultaneousGesture(LongPressGesture().onEnded {_ in
guard !damus_state.settings.nozaps else { return }
present_sheet(.zap(target: target, lnurl: lnurl))
button.showing_zap_customizer = true
})
.highPriorityGesture(TapGesture().onEnded {
guard !damus_state.settings.nozaps else { return }
tap()
})
.sheet(isPresented: $button.showing_zap_customizer) {
CustomizeZapView(state: damus_state, event: event, lnurl: lnurl)
}
.sheet(isPresented: $button.showing_select_wallet, onDismiss: {button.showing_select_wallet = false}) {
SelectWalletView(default_wallet: damus_state.settings.default_wallet, showingSelectWallet: $button.showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: button.invoice ?? "")
}
.onReceive(handle_notify(.zapping)) { notif in
let zap_ev = notif.object as! ZappingEvent
guard zap_ev.event.id == self.event.id else {
return
}
guard !zap_ev.is_custom else {
return
}
switch zap_ev.type {
case .failed:
break
case .got_zap_invoice(let inv):
if damus_state.settings.show_wallet_selector {
self.button.invoice = inv
self.button.showing_select_wallet = true
} else {
let wallet = damus_state.settings.default_wallet.model
open_with_wallet(wallet: wallet, invoice: inv)
}
}
}
}
}
@@ -144,7 +177,7 @@ struct ZapButton_Previews: PreviewProvider {
let pending_zap = PendingZap(amount_msat: 1000, target: ZapTarget.note(id: "noteid", author: "author"), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice)))
let zaps = ZapsDataModel([.pending(pending_zap)])
ZapButton(damus_state: test_damus_state(), target: ZapTarget.note(id: test_event.id, author: test_event.pubkey), lnurl: "lnurl", zaps: zaps)
ZapButton(damus_state: test_damus_state(), event: test_event, lnurl: "lnurl", zaps: zaps)
}
}
@@ -160,13 +193,14 @@ func initial_pending_zap_state(settings: UserSettingsStore) -> PendingZapState {
return .external(ExtPendingZapState(state: .fetching_invoice))
}
func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) {
func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) {
guard let keypair = damus_state.keypair.to_full() else {
return
}
// Only take the first 10 because reasons
let relays = Array(damus_state.pool.our_descriptors.prefix(10))
let target = ZapTarget.note(id: event.id, author: event.pubkey)
let content = comment ?? ""
guard let mzapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type) else {
@@ -174,7 +208,8 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
return
}
let amount_msat = Int64(amount_sats ?? damus_state.settings.default_zap_amount) * 1000
let zap_amount = amount_sats ?? damus_state.settings.default_zap_amount
let amount_msat = Int64(zap_amount) * 1000
let pending_zap_state = initial_pending_zap_state(settings: damus_state.settings)
let pending_zap = PendingZap(amount_msat: amount_msat, target: target, request: mzapreq, type: zap_type, state: pending_zap_state)
let zapreq = mzapreq.potentially_anon_outer_request.ev
@@ -194,7 +229,7 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
DispatchQueue.main.async {
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
let typ = ZappingEventType.failed(.bad_lnurl)
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
notify(.zapping, ev)
}
return
@@ -204,11 +239,11 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
damus_state.lnurls.endpoints[target.pubkey] = payreq
}
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, msats: amount_msat, zap_type: zap_type, comment: comment) else {
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount, zap_type: zap_type, comment: comment) else {
DispatchQueue.main.async {
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
let typ = ZappingEventType.failed(.fetching_invoice)
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
notify(.zapping, ev)
}
return
@@ -221,50 +256,21 @@ func send_zap(damus_state: DamusState, target: ZapTarget, lnurl: String, is_cust
// don't both continuing, user has canceled
if case .cancel_fetching_invoice = nwc_state.state {
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
let typ = ZappingEventType.failed(.canceled)
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
notify(.zapping, ev)
return
}
var flusher: OnFlush? = nil
// donations are only enabled on one-tap zaps and off appstore
if !damus_state.settings.nozaps && !is_custom && damus_state.settings.donation_percent > 0 {
flusher = .once({ pe in
// send donation zap when the pending zap is flushed, this allows user to cancel and not send a donation
Task { @MainActor in
await send_donation_zap(pool: damus_state.pool, postbox: damus_state.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat)
}
})
}
// we don't have a delay on one-tap nozaps (since this will be from customize zap view)
let delay = damus_state.settings.nozaps ? nil : 5.0
let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, delay: delay, on_flush: flusher)
guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else {
print("nwc: failed to send nwc request for zapreq \(reqid.reqid)")
let typ = ZappingEventType.failed(.send_failed)
let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
notify(.zapping, ev)
guard let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv),
case .nwc(let pzap_state) = pending_zap_state
else {
return
}
print("nwc: sending request \(nwc_req.id) zap_req_id \(reqid.reqid)")
if pzap_state.update_state(state: .postbox_pending(nwc_req)) {
// we don't need to trigger a ZapsDataModel update here
}
let ev = ZappingEvent(is_custom: is_custom, type: .sent_from_nwc, target: target)
notify(.zapping, ev)
case .external(let pending_ext):
pending_ext.state = .done
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), target: target)
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), event: event)
notify(.zapping, ev)
}
}

View File

@@ -6,7 +6,6 @@
//
import SwiftUI
import AVKit
struct TimestampedProfile {
let profile: Profile
@@ -14,38 +13,17 @@ struct TimestampedProfile {
let event: NostrEvent
}
struct ZapSheet {
let target: ZapTarget
let lnurl: String
}
struct SelectWallet {
let invoice: String
}
enum Sheets: Identifiable {
case post(PostAction)
case report(ReportTarget)
case event(NostrEvent)
case zap(ZapSheet)
case select_wallet(SelectWallet)
case filter
static func zap(target: ZapTarget, lnurl: String) -> Sheets {
return .zap(ZapSheet(target: target, lnurl: lnurl))
}
static func select_wallet(invoice: String) -> Sheets {
return .select_wallet(SelectWallet(invoice: invoice))
}
var id: String {
switch self {
case .report: return "report"
case .post(let action): return "post-" + (action.ev?.id ?? "")
case .event(let ev): return "event-" + ev.id
case .zap(let sheet): return "zap-" + sheet.target.id
case .select_wallet: return "select-wallet"
case .filter: return "filter"
}
}
@@ -82,14 +60,21 @@ struct ContentView: View {
@State var damus_state: DamusState? = nil
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home
@State var is_deleted_account: Bool = false
@State var active_profile: String? = nil
@State var active_search: NostrFilter? = nil
@State var active_event: NostrEvent? = nil
@State var profile_open: Bool = false
@State var thread_open: Bool = false
@State var search_open: Bool = false
@State var wallet_open: Bool = false
@State var active_nwc: WalletConnectURL? = nil
@State var muting: String? = nil
@State var confirm_mute: Bool = false
@State var user_muted_confirm: Bool = false
@State var confirm_overwrite_mutelist: Bool = false
@SceneStorage("ContentView.filter_state") var filter_state : FilterState = .posts_and_replies
@State private var isSideBarOpened = false
var home: HomeModel = HomeModel()
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
@StateObject var home: HomeModel = HomeModel()
let sub_id = UUID().description
@@ -121,7 +106,7 @@ struct ContentView: View {
if privkey != nil {
PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) {
self.active_sheet = .post(.posting(.none))
self.active_sheet = .post(.posting)
}
}
}
@@ -129,8 +114,8 @@ struct ContentView: View {
.safeAreaInset(edge: .top, spacing: 0) {
VStack(spacing: 0) {
CustomPicker(selection: $filter_state, content: {
Text("Notes", comment: "Label for filter for seeing only notes (instead of notes and replies).").tag(FilterState.posts)
Text("Notes & Replies", comment: "Label for filter for seeing notes and replies (instead of only notes).").tag(FilterState.posts_and_replies)
Text("Posts", comment: "Label for filter for seeing only posts (instead of posts and replies).").tag(FilterState.posts)
Text("Posts & Replies", comment: "Label for filter for seeing posts and replies (instead of only posts).").tag(FilterState.posts_and_replies)
})
Divider()
.frame(height: 1)
@@ -142,13 +127,16 @@ struct ContentView: View {
func contentTimelineView(filter: (@escaping (NostrEvent) -> Bool)) -> some View {
ZStack {
if let damus = self.damus_state {
TimelineView(events: home.events, loading: .constant(false), damus: damus, show_friend_icon: false, filter: filter)
TimelineView(events: home.events, loading: $home.loading, damus: damus, show_friend_icon: false, filter: filter)
}
}
}
func popToRoot() {
navigationCoordinator.popToRoot()
profile_open = false
thread_open = false
search_open = false
wallet_open = false
isSideBarOpened = false
}
@@ -159,6 +147,21 @@ struct ContentView: View {
func MainContent(damus: DamusState) -> some View {
VStack {
NavigationLink(destination: WalletView(model: damus_state!.wallet), isActive: $wallet_open) {
EmptyView()
}
NavigationLink(destination: MaybeProfileView, isActive: $profile_open) {
EmptyView()
}
if let active_event {
let thread = ThreadModel(event: active_event, damus_state: damus_state!)
NavigationLink(destination: ThreadView(state: damus_state!, thread: thread), isActive: $thread_open) {
EmptyView()
}
}
NavigationLink(destination: MaybeSearchView, isActive: $search_open) {
EmptyView()
}
switch selected_timeline {
case .search:
if #available(iOS 16.0, *) {
@@ -200,6 +203,28 @@ struct ContentView: View {
}
}
var MaybeSearchView: some View {
Group {
if let search = self.active_search {
SearchView(appstate: damus_state!, search: SearchModel(state: damus_state!, search: search))
} else {
EmptyView()
}
}
}
var MaybeProfileView: some View {
Group {
if let pk = self.active_profile {
let profile_model = ProfileModel(pubkey: pk, damus: damus_state!)
let followers = FollowersModel(damus_state: damus_state!, target: pk)
ProfileView(damus_state: damus_state!, profile: profile_model, followers: followers)
} else {
EmptyView()
}
}
}
func MaybeReportView(target: ReportTarget) -> some View {
Group {
if let damus_state {
@@ -215,30 +240,32 @@ struct ContentView: View {
}
func open_event(ev: NostrEvent) {
let thread = ThreadModel(event: ev, damus_state: damus_state!)
navigationCoordinator.push(route: Route.Thread(thread: thread))
popToRoot()
self.active_event = ev
self.thread_open = true
}
func open_wallet(nwc: WalletConnectURL) {
self.damus_state!.wallet.new(nwc)
navigationCoordinator.push(route: Route.Wallet(wallet: damus_state!.wallet))
self.wallet_open = true
}
func open_profile(id: String) {
let profile_model = ProfileModel(pubkey: id, damus: damus_state!)
let followers = FollowersModel(damus_state: damus_state!, target: id)
navigationCoordinator.push(route: Route.Profile(profile: profile_model, followers: followers))
popToRoot()
self.active_profile = id
self.profile_open = true
}
func open_search(filt: NostrFilter) {
let search = SearchModel(state: damus_state!, search: filt)
navigationCoordinator.push(route: Route.Search(search: search))
popToRoot()
self.active_search = filt
self.search_open = true
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
if let damus = self.damus_state {
NavigationStack(path: $navigationCoordinator.path) {
NavigationView {
TabView { // Prevents navbar appearance change on scroll
MainContent(damus: damus)
.toolbar() {
@@ -261,14 +288,13 @@ struct ContentView: View {
if selected_timeline == .search {
Button(action: {
//isFilterVisible.toggle()
present_sheet(.filter)
self.active_sheet = .filter
}) {
// checklist, checklist.checked, lisdt.bullet, list.bullet.circle, line.3.horizontal.decrease..., line.3.horizontail.decrease
Label(NSLocalizedString("Filter", comment: "Button label text for filtering relay servers."), image: "filter")
Label(NSLocalizedString("Filter", comment: "Button label text for filtering relay servers."), systemImage: "line.3.horizontal.decrease")
.foregroundColor(.gray)
//.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
}
}
@@ -278,16 +304,10 @@ struct ContentView: View {
.overlay(
SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened.animation())
)
.navigationDestination(for: Route.self) { route in
route.view(navigationCordinator: navigationCoordinator, damusState: damus_state!)
}
.onReceive(handle_notify(.switched_timeline)) { _ in
navigationCoordinator.popToRoot()
}
}
.navigationViewStyle(.stack)
TabBar(nstatus: home.notification_status, selected: $selected_timeline, settings: damus.settings, action: switch_timeline)
TabBar(new_events: $home.new_events, selected: $selected_timeline, settings: damus.settings, action: switch_timeline)
.padding([.bottom], 8)
.background(Color(uiColor: .systemBackground).ignoresSafeArea())
}
@@ -295,7 +315,6 @@ struct ContentView: View {
.ignoresSafeArea(.keyboard)
.onAppear() {
self.connect()
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers)
setup_notifications()
}
.sheet(item: $active_sheet) { item in
@@ -306,10 +325,6 @@ struct ContentView: View {
PostView(action: action, damus_state: damus_state!)
case .event:
EventDetailView()
case .zap(let zapsheet):
CustomizeZapView(state: damus_state!, target: zapsheet.target, lnurl: zapsheet.lnurl)
case .select_wallet(let select):
SelectWalletView(default_wallet: damus_state!.settings.default_wallet, active_sheet: $active_sheet, our_pubkey: damus_state!.pubkey, invoice: select.invoice)
case .filter:
let timeline = selected_timeline
if #available(iOS 16.0, *) {
@@ -382,7 +397,7 @@ struct ContentView: View {
return
}
ds.postbox.send(ev)
if let profile = ds.profiles.lookup_with_timestamp(id: ev.pubkey) {
if let profile = ds.profiles.profiles[ev.pubkey] {
ds.postbox.send(profile.event)
}
}
@@ -417,31 +432,6 @@ struct ContentView: View {
.onReceive(handle_notify(.unmute_thread)) { notif in
home.filter_events()
}
.onReceive(handle_notify(.present_sheet)) { notif in
let sheet = notif.object as! Sheets
self.active_sheet = sheet
}
.onReceive(handle_notify(.zapping)) { notif in
let zap_ev = notif.object as! ZappingEvent
guard !zap_ev.is_custom else {
return
}
switch zap_ev.type {
case .failed:
break
case .got_zap_invoice(let inv):
if damus_state!.settings.show_wallet_selector {
present_sheet(.select_wallet(invoice: inv))
} else {
let wallet = damus_state!.settings.default_wallet.model
open_with_wallet(wallet: wallet, invoice: inv)
}
case .sent_from_nwc:
break
}
}
.onChange(of: scenePhase) { (phase: ScenePhase) in
switch phase {
case .background:
@@ -464,11 +454,6 @@ struct ContentView: View {
let damus_state else {
return
}
if local.type == .profile_zap {
open_profile(id: local.event_id)
return
}
guard let target = damus_state.events.lookup(local.event_id) else {
return
@@ -477,16 +462,13 @@ struct ContentView: View {
switch local.type {
case .dm:
selected_timeline = .dms
damus_state.dms.set_active_dm(target.pubkey)
navigationCoordinator.push(route: Route.DMChat(dms: damus_state.dms.active_model))
damus_state.dms.open_dm_by_pk(target.pubkey)
case .like: fallthrough
case .zap: fallthrough
case .mention: fallthrough
case .repost:
open_event(ev: target)
case .profile_zap:
// Handled separately above.
break
}
}
.onReceive(handle_notify(.onlyzaps_mode)) { notif in
@@ -517,7 +499,7 @@ struct ContentView: View {
}, message: {
if let pubkey = self.muting {
let profile = damus_state!.profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
let name = Profile.displayName(profile: profile, pubkey: pubkey).username
Text("\(name) has been muted", comment: "Alert message that informs a user was muted.")
} else {
Text("User has been muted", comment: "Alert message that informs a user was d.")
@@ -577,7 +559,7 @@ struct ContentView: View {
}, message: {
if let pubkey = muting {
let profile = damus_state?.profiles.lookup(id: pubkey)
let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50)
let name = Profile.displayName(profile: profile, pubkey: pubkey).username
Text("Mute \(name)?", comment: "Alert message prompt to ask if a user should be muted.")
} else {
Text("Could not find user to mute...", comment: "Alert message to indicate that the muted user could not be found.")
@@ -645,8 +627,7 @@ struct ContentView: View {
bootstrap_relays: bootstrap_relays,
replies: ReplyCounter(our_pubkey: pubkey),
muted_threads: MutedThreadsManager(keypair: keypair),
wallet: WalletModel(settings: settings),
nav: self.navigationCoordinator
wallet: WalletModel(settings: settings)
)
home.damus_state = self.damus_state!
@@ -711,7 +692,7 @@ func update_filters_with_since(last_of_kind: [Int: NostrEvent], filters: [NostrF
let kinds = filter.kinds ?? []
let initial: Int64? = nil
let earliest = kinds.reduce(initial) { earliest, kind in
let last = last_of_kind[kind.rawValue]
let last = last_of_kind[kind]
let since: Int64? = get_since_time(last_event: last)
if earliest == nil {
@@ -755,57 +736,24 @@ func setup_notifications() {
}
}
struct FindEvent {
let type: FindEventType
let find_from: [String]?
static func profile(pubkey: String, find_from: [String]? = nil) -> FindEvent {
return FindEvent(type: .profile(pubkey), find_from: find_from)
}
static func event(evid: String, find_from: [String]? = nil) -> FindEvent {
return FindEvent(type: .event(evid), find_from: find_from)
}
}
enum FindEventType {
case profile(String)
case event(String)
}
enum FoundEvent {
case profile(Profile, NostrEvent)
case invalid_profile(NostrEvent)
case event(NostrEvent)
}
func find_event(state: DamusState, query query_: FindEvent, callback: @escaping (FoundEvent?) -> ()) {
var filter: NostrFilter? = nil
let find_from = query_.find_from
let query = query_.type
switch query {
case .profile(let pubkey):
if let profile = state.profiles.lookup_with_timestamp(id: pubkey) {
callback(.profile(profile.profile, profile.event))
return
}
filter = NostrFilter(kinds: [.metadata], limit: 1, authors: [pubkey])
case .event(let evid):
if let ev = state.events.lookup(evid) {
callback(.event(ev))
return
}
filter = NostrFilter(ids: [evid], limit: 1)
func find_event(state: DamusState, evid: String, search_type: SearchType, find_from: [String]?, callback: @escaping (NostrEvent?) -> ()) {
if let ev = state.events.lookup(evid) {
callback(ev)
return
}
let subid = UUID().description
var attempts: Int = 0
var has_event = false
guard let filter else { return }
var filter = search_type == .event ? NostrFilter.filter_ids([ evid ]) : NostrFilter.filter_authors([ evid ])
if search_type == .profile {
filter.kinds = [NostrKind.metadata.rawValue]
}
filter.limit = 1
var attempts = 0
state.pool.subscribe_to(sub_id: subid, filters: [filter], to: find_from) { relay_id, res in
guard case .nostr_event(let ev) = res else {
@@ -821,22 +769,15 @@ func find_event(state: DamusState, query query_: FindEvent, callback: @escaping
break
case .event(_, let ev):
has_event = true
state.pool.unsubscribe(sub_id: subid)
switch query {
case .profile:
if ev.known_kind == .metadata {
process_metadata_event(events: state.events, our_pubkey: state.pubkey, profiles: state.profiles, ev: ev) { profile in
guard let profile else {
callback(.invalid_profile(ev))
return
}
callback(.profile(profile, ev))
return
}
if search_type == .profile && ev.known_kind == .metadata {
process_metadata_event(events: state.events, our_pubkey: state.pubkey, profiles: state.profiles, ev: ev) {
callback(ev)
}
case .event:
callback(.event(ev))
} else {
callback(ev)
}
case .eose:
if !has_event {
@@ -859,11 +800,11 @@ func timeline_name(_ timeline: Timeline?) -> String {
}
switch timeline {
case .home:
return NSLocalizedString("Home", comment: "Navigation bar title for Home view where notes and replies appear from those who the user is following.")
return NSLocalizedString("Home", comment: "Navigation bar title for Home view where posts and replies appear from those who the user is following.")
case .notifications:
return NSLocalizedString("Notifications", comment: "Toolbar label for Notifications view.")
case .search:
return NSLocalizedString("Universe 🛸", comment: "Toolbar label for the universal view where notes from all connected relay servers appear.")
return NSLocalizedString("Universe 🛸", comment: "Toolbar label for the universal view where posts from all connected relay servers appear.")
case .dms:
return NSLocalizedString("DMs", comment: "Toolbar label for DMs view, where DM is the English abbreviation for Direct Message.")
}
@@ -961,9 +902,10 @@ func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) ->
if ref.key == "p" {
result(.profile(ref.ref_id))
} else if ref.key == "e" {
find_event(state: state, query: .event(evid: ref.ref_id)) { res in
guard let res, case .event(let ev) = res else { return }
result(.event(ev))
find_event(state: state, evid: ref.ref_id, search_type: .event, find_from: nil) { ev in
if let ev {
result(.event(ev))
}
}
}
case .filter(let filt):

View File

@@ -20,7 +20,7 @@ class ActionBarModel: ObservableObject {
@Published var our_zap: Zapping?
@Published var likes: Int
@Published var boosts: Int
@Published private(set) var zaps: Int
@Published var zaps: Int
@Published var zap_total: Int64
@Published var replies: Int

View File

@@ -11,8 +11,6 @@ import Foundation
class Contacts {
private var friends: Set<String> = Set()
private var friend_of_friends: Set<String> = Set()
/// Tracks which friends are friends of a given pubkey.
private var pubkey_to_our_friends = [String : Set<String>]()
private var muted: Set<String> = Set()
let our_pubkey: String
@@ -60,10 +58,6 @@ class Contacts {
func remove_friend(_ pubkey: String) {
friends.remove(pubkey)
pubkey_to_our_friends.forEach {
pubkey_to_our_friends[$0.key]?.remove(pubkey)
}
}
func get_friend_list() -> [String] {
@@ -79,15 +73,6 @@ class Contacts {
for tag in contact.tags {
if tag.count >= 2 && tag[0] == "p" {
friend_of_friends.insert(tag[1])
// Exclude themself and us.
if contact.pubkey != our_pubkey && contact.pubkey != tag[1] {
if pubkey_to_our_friends[tag[1]] == nil {
pubkey_to_our_friends[tag[1]] = Set<String>()
}
pubkey_to_our_friends[tag[1]]?.insert(contact.pubkey)
}
}
}
}
@@ -111,11 +96,6 @@ class Contacts {
func follow_state(_ pubkey: String) -> FollowState {
return is_friend(pubkey) ? .follows : .unfollows
}
/// Gets the list of pubkeys of our friends who follow the given pubkey.
func get_friended_followers(_ pubkey: String) -> [String] {
return Array((pubkey_to_our_friends[pubkey] ?? Set()))
}
}
func follow_user(pool: RelayPool, our_contacts: NostrEvent?, pubkey: String, privkey: String, follow: ReferencedId) -> NostrEvent? {

View File

@@ -30,23 +30,13 @@ struct DamusState {
let replies: ReplyCounter
let muted_threads: MutedThreadsManager
let wallet: WalletModel
let nav: NavigationCoordinator
@discardableResult
func add_zap(zap: Zapping) -> Bool {
// store generic zap mapping
self.zaps.add_zap(zap: zap)
let stored = self.events.store_zap(zap: zap)
// thread zaps
if let ev = zap.event, !settings.nozaps, zap.is_in_thread {
// [nozaps]: thread zaps are only available outside of the app store
replies.count_replies(ev)
events.add_replies(ev: ev)
}
// associate with events as well
return stored
return self.events.store_zap(zap: zap)
}
var pubkey: String {
@@ -58,5 +48,5 @@ struct DamusState {
}
static var empty: DamusState {
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil)), wallet: WalletModel(settings: UserSettingsStore()), nav: NavigationCoordinator()) }
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil)), wallet: WalletModel()) }
}

View File

@@ -30,6 +30,16 @@ class DirectMessagesModel: ObservableObject {
self.active_model = model
}
func open_dm_by_pk(_ pubkey: String) {
self.set_active_dm(pubkey)
self.open_dm = true
}
func open_dm_by_model(_ model: DirectMessageModel) {
self.set_active_dm_model(model)
self.open_dm = true
}
func set_active_dm(_ pubkey: String) {
for model in self.dms where model.pubkey == pubkey {
self.set_active_dm_model(model)

View File

@@ -24,7 +24,7 @@ class EventsModel: ObservableObject {
}
private func get_filter() -> NostrFilter {
var filter = NostrFilter(kinds: [kind])
var filter = NostrFilter.filter_kinds([kind.rawValue])
filter.referenced_ids = [target]
filter.limit = 500
return filter

View File

@@ -30,8 +30,9 @@ class FollowersModel: ObservableObject {
}
func get_filter() -> NostrFilter {
NostrFilter(kinds: [.contacts],
pubkeys: [target])
var filter = NostrFilter.filter_contacts
filter.pubkeys = [target]
return filter
}
func subscribe() {
@@ -55,13 +56,14 @@ class FollowersModel: ObservableObject {
}
func load_profiles(relay_id: String) {
let authors = find_profiles_to_fetch_from_keys(profiles: damus_state.profiles, pks: contacts ?? [])
var filter = NostrFilter.filter_profiles
let authors = find_profiles_to_fetch_pk(profiles: damus_state.profiles, event_pubkeys: contacts ?? [])
if authors.isEmpty {
return
}
let filter = NostrFilter(kinds: [.metadata],
authors: authors)
filter.authors = authors
damus_state.pool.subscribe_to(sub_id: profiles_id, filters: [filter], to: [relay_id], handler: handle_event)
}

View File

@@ -21,10 +21,10 @@ class FollowingModel {
}
func get_filter() -> NostrFilter {
var f = NostrFilter(kinds: [.metadata])
var f = NostrFilter.filter_kinds([NostrKind.metadata.rawValue])
f.authors = self.contacts.reduce(into: Array<String>()) { acc, pk in
// don't fetch profiles we already have
if damus_state.profiles.has_fresh_profile(id: pk) {
if damus_state.profiles.lookup(id: pk) != nil {
return
}
acc.append(pk)

View File

@@ -23,7 +23,7 @@ struct NewEventsBits: OptionSet {
static let notifications: NewEventsBits = [.zaps, .likes, .reposts, .mentions]
}
class HomeModel {
class HomeModel: ObservableObject {
// Don't trigger a user notification for events older than a certain age
static let event_max_age_for_notification: TimeInterval = 12 * 60 * 60
@@ -49,10 +49,9 @@ class HomeModel {
var signal = SignalModel()
var notifications = NotificationsModel()
var notification_status = NotificationStatusModel()
var events: EventHolder = EventHolder()
var zap_button: ZapButtonModel = ZapButtonModel()
@Published var new_events: NewEventsBits = NewEventsBits()
@Published var notifications = NotificationsModel()
@Published var events: EventHolder = EventHolder()
init() {
self.damus_state = DamusState.empty
@@ -133,11 +132,11 @@ class HomeModel {
case .nwc_request:
break
case .nwc_response:
handle_nwc_response(ev, relay: relay_id)
handle_nwc_response(ev)
}
}
func handle_nwc_response(_ ev: NostrEvent, relay: String) {
func handle_nwc_response(_ ev: NostrEvent) {
Task { @MainActor in
// TODO: Adapt KeychainStorage to StringCodable and instead of parsing to WalletConnectURL every time
guard let nwc_str = damus_state.settings.nostr_wallet_connect,
@@ -149,54 +148,78 @@ class HomeModel {
// since command results are not returned for ephemeral events,
// remove the request from the postbox which is likely failing over and over
if damus_state.postbox.remove_relayer(relay_id: nwc.relay.id, event_id: resp.req_id) {
print("nwc: got response, removed \(resp.req_id) from the postbox [\(relay)]")
print("nwc: got response, removed \(resp.req_id) from the postbox")
} else {
print("nwc: \(resp.req_id) not found in the postbox, nothing to remove [\(relay)]")
print("nwc: \(resp.req_id) not found in the postbox, nothing to remove")
}
guard resp.response.error != nil else {
print("nwc success: \(resp.response.result.debugDescription) [\(relay)]")
nwc_success(state: self.damus_state, resp: resp)
if resp.response.error == nil {
nwc_success(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
return
}
print("nwc error: \(resp.response)")
nwc_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
}
}
func handle_zap_event(_ ev: NostrEvent) {
process_zap_event(damus_state: damus_state, ev: ev) { zapres in
guard case .done(let zap) = zapres else { return }
guard zap.target.pubkey == self.damus_state.keypair.pubkey else {
return
}
func handle_zap_event_with_zapper(profiles: Profiles, ev: NostrEvent, our_keypair: Keypair, zapper: String) {
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
return
}
if !self.notifications.insert_zap(.zap(zap)) {
return
}
damus_state.add_zap(zap: .zap(zap))
guard zap.target.pubkey == our_keypair.pubkey else {
return
}
if !notifications.insert_zap(.zap(zap)) {
return
}
guard let new_bits = handle_last_events(new_events: self.notification_status.new_events, ev: ev, timeline: .notifications, shouldNotify: true) else {
return
}
if self.damus_state.settings.zap_vibration {
if handle_last_event(ev: ev, timeline: .notifications) {
if damus_state.settings.zap_vibration {
// Generate zap vibration
zap_vibrate(zap_amount: zap.invoice.amount)
}
if self.damus_state.settings.zap_notification {
if damus_state.settings.zap_notification {
// Create in-app local notification for zap received.
switch zap.target {
case .profile(let profile_id):
create_in_app_profile_zap_notification(profiles: self.damus_state.profiles, zap: zap, profile_id: profile_id)
case .note(let note_target):
create_in_app_event_zap_notification(profiles: self.damus_state.profiles, zap: zap, evId: note_target.note_id)
}
create_in_app_zap_notification(profiles: profiles, zap: zap, evId: ev.referenced_ids.first?.id ?? "")
}
}
return
}
func handle_zap_event(_ ev: NostrEvent) {
// These are zap notifications
guard let ptag = event_tag(ev, name: "p") else {
return
}
let our_keypair = damus_state.keypair
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) {
handle_zap_event_with_zapper(profiles: self.damus_state.profiles, ev: ev, our_keypair: our_keypair, zapper: local_zapper)
return
}
guard let profile = damus_state.profiles.lookup(id: ptag) else {
return
}
guard let lnurl = profile.lnurl else {
return
}
Task {
guard let zapper = await fetch_zapper_from_lnurl(lnurl) else {
return
}
self.notification_status.new_events = new_bits
DispatchQueue.main.async {
self.damus_state.profiles.zappers[ptag] = zapper
self.handle_zap_event_with_zapper(profiles: self.damus_state.profiles, ev: ev, our_keypair: our_keypair, zapper: zapper)
}
}
}
@@ -371,12 +394,13 @@ class HomeModel {
/// Send the initial filters, just our contact list mostly
func send_initial_filters(relay_id: String) {
let filter = NostrFilter(kinds: [.contacts], limit: 1, authors: [damus_state.pubkey])
let subscription = NostrSubscribe(filters: [filter], sub_id: init_subid)
pool.send(.subscribe(subscription), to: [relay_id])
var filter = NostrFilter.filter_contacts
filter.authors = [self.damus_state.pubkey]
filter.limit = 1
pool.send(.subscribe(.init(filters: [filter], sub_id: init_subid)), to: [relay_id])
}
/// After initial connection or reconnect, send subscription filters for the home timeline, DMs, and notifications
func send_home_filters(relay_id: String?) {
// TODO: since times should be based on events from a specific relay
// perhaps we could mark this in the relay pool somehow
@@ -384,19 +408,23 @@ class HomeModel {
var friends = damus_state.contacts.get_friend_list()
friends.append(damus_state.pubkey)
var contacts_filter = NostrFilter(kinds: [.metadata])
var contacts_filter = NostrFilter.filter_kinds([NostrKind.metadata.rawValue])
contacts_filter.authors = friends
var our_contacts_filter = NostrFilter(kinds: [.contacts, .metadata])
var our_contacts_filter = NostrFilter.filter_kinds([NostrKind.contacts.rawValue, NostrKind.metadata.rawValue])
our_contacts_filter.authors = [damus_state.pubkey]
var our_blocklist_filter = NostrFilter(kinds: [.list])
var our_blocklist_filter = NostrFilter.filter_kinds([NostrKind.list.rawValue])
our_blocklist_filter.parameter = ["mute"]
our_blocklist_filter.authors = [damus_state.pubkey]
var dms_filter = NostrFilter(kinds: [.dm])
var dms_filter = NostrFilter.filter_kinds([
NostrKind.dm.rawValue,
])
var our_dms_filter = NostrFilter(kinds: [.dm])
var our_dms_filter = NostrFilter.filter_kinds([
NostrKind.dm.rawValue,
])
// friends only?...
//dms_filter.authors = friends
@@ -405,27 +433,27 @@ class HomeModel {
our_dms_filter.authors = [ damus_state.pubkey ]
// TODO: separate likes?
var home_filter_kinds: [NostrKind] = [
.text,
.boost
var home_filter_kinds = [
NostrKind.text.rawValue,
NostrKind.boost.rawValue
]
if !damus_state.settings.onlyzaps_mode {
home_filter_kinds.append(.like)
home_filter_kinds.append(NostrKind.like.rawValue)
}
var home_filter = NostrFilter(kinds: home_filter_kinds)
var home_filter = NostrFilter.filter_kinds(home_filter_kinds)
// include our pubkey as well even if we're not technically a friend
home_filter.authors = friends
home_filter.limit = 500
var notifications_filter_kinds: [NostrKind] = [
.text,
.boost,
.zap,
var notifications_filter_kinds = [
NostrKind.text.rawValue,
NostrKind.boost.rawValue,
NostrKind.zap.rawValue,
]
if !damus_state.settings.onlyzaps_mode {
notifications_filter_kinds.append(.like)
notifications_filter_kinds.append(NostrKind.like.rawValue)
}
var notifications_filter = NostrFilter(kinds: notifications_filter_kinds)
var notifications_filter = NostrFilter.filter_kinds(notifications_filter_kinds)
notifications_filter.pubkeys = [damus_state.pubkey]
notifications_filter.limit = 500
@@ -441,7 +469,7 @@ class HomeModel {
notifications_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: notifications_filters)
dms_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: dms_filters)
//print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters])
print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters])
if let relay_id {
pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid)), to: [relay_id])
@@ -524,8 +552,8 @@ class HomeModel {
@discardableResult
func handle_last_event(ev: NostrEvent, timeline: Timeline, shouldNotify: Bool = true) -> Bool {
if let new_bits = handle_last_events(new_events: self.notification_status.new_events, ev: ev, timeline: timeline, shouldNotify: shouldNotify) {
self.notification_status.new_events = new_bits
if let new_bits = handle_last_events(new_events: self.new_events, ev: ev, timeline: timeline, shouldNotify: shouldNotify) {
new_events = new_bits
return true
} else {
return false
@@ -557,7 +585,7 @@ class HomeModel {
}
func got_new_dm(notifs: NewEventsBits, ev: NostrEvent) {
notification_status.new_events = notifs
self.new_events = notifs
if damus_state.settings.dm_notification && ev.age < HomeModel.event_max_age_for_notification {
let convo = ev.decrypted(privkey: self.damus_state.keypair.privkey) ?? NSLocalizedString("New encrypted direct message", comment: "Notification that the user has received a new direct message")
@@ -575,7 +603,7 @@ class HomeModel {
if !should_debounce_dms {
self.incoming_dms.append(ev)
if let notifs = handle_incoming_dms(prev_events: notification_status.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
got_new_dm(notifs: notifs, ev: ev)
}
self.incoming_dms = []
@@ -585,7 +613,7 @@ class HomeModel {
incoming_dms.append(ev)
dm_debouncer.debounce { [self] in
if let notifs = handle_incoming_dms(prev_events: notification_status.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
if let notifs = handle_incoming_dms(prev_events: self.new_events, dms: self.dms, our_pubkey: self.damus_state.pubkey, evs: self.incoming_dms) {
got_new_dm(notifs: notifs, ev: ev)
}
self.incoming_dms = []
@@ -703,7 +731,7 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P
var old_nip05: String? = nil
if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) {
old_nip05 = mprof.profile.nip05
if mprof.event.created_at > ev.created_at {
if mprof.timestamp > ev.created_at {
// skip if we already have an newer profile
return
}
@@ -720,7 +748,7 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P
print("validated nip05 for '\(nip05)'")
}
Task { @MainActor in
DispatchQueue.main.async {
profiles.validated[ev.pubkey] = validated
profiles.nip05_pubkey[nip05] = ev.pubkey
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
@@ -752,7 +780,7 @@ func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping (
switch validated {
case .unknown:
Task.detached(priority: .medium) {
Task {
let result = validate_event(ev: ev)
DispatchQueue.main.async {
@@ -773,11 +801,11 @@ func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping (
}
}
func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Profiles, ev: NostrEvent, completion: ((Profile?) -> Void)? = nil) {
func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Profiles, ev: NostrEvent, completion: (() -> Void)? = nil) {
guard_valid_event(events: events, ev: ev) {
DispatchQueue.global(qos: .background).async {
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
completion?(nil)
completion?()
return
}
@@ -785,7 +813,7 @@ func process_metadata_event(events: EventCache, our_pubkey: String, profiles: Pr
DispatchQueue.main.async {
process_metadata_profile(our_pubkey: our_pubkey, profiles: profiles, profile: profile, ev: ev)
completion?(profile)
completion?()
}
}
}
@@ -858,7 +886,6 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
if changed {
save_bootstrap_relays(pubkey: state.pubkey, relays: Array(new))
state.pool.connect()
notify(.relays_changed, ())
}
}
@@ -1076,12 +1103,13 @@ func zap_notification_title(_ zap: Zap) -> String {
}
func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) -> String {
let src = zap.request.ev
let pk = zap.is_anon ? ANON_PUBKEY : src.pubkey
let src = zap.private_request ?? zap.request.ev
let anon = event_is_anonymous(ev: src)
let pk = anon ? "anon" : src.pubkey
let profile = profiles.lookup(id: pk)
let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0))
let formattedSats = format_msats_abbrev(zap.invoice.amount)
let name = Profile.displayName(profile: profile, pubkey: pk).display_name.truncate(maxLength: 50)
let name = Profile.displayName(profile: profile, pubkey: pk).display_name
if src.content.isEmpty {
let format = localizedStringFormat(key: "zap_notification_no_message", locale: locale)
@@ -1092,28 +1120,7 @@ func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale
}
}
func create_in_app_profile_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, profile_id: String) {
let content = UNMutableNotificationContent()
content.title = zap_notification_title(zap)
content.body = zap_notification_body(profiles: profiles, zap: zap, locale: locale)
content.sound = UNNotificationSound.default
content.userInfo = LossyLocalNotification(type: .profile_zap, event_id: profile_id).to_user_info()
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
let request = UNNotificationRequest(identifier: "myZapNotification", content: content, trigger: trigger)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("Error: \(error)")
} else {
print("Local notification scheduled")
}
}
}
func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: String) {
func create_in_app_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: String) {
let content = UNMutableNotificationContent()
content.title = zap_notification_title(zap)
@@ -1165,15 +1172,13 @@ func process_local_notification(damus_state: DamusState, event ev: NostrEvent) {
create_local_notification(profiles: damus_state.profiles, notify: notify )
}
} else if type == .boost && damus_state.settings.repost_notification, let inner_ev = ev.get_inner_event(cache: damus_state.events) {
let content = NSAttributedString(render_note_content(ev: inner_ev, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content.attributed).string
let notify = LocalNotification(type: .repost, event: ev, target: inner_ev, content: content)
let notify = LocalNotification(type: .repost, event: ev, target: inner_ev, content: inner_ev.content)
create_local_notification(profiles: damus_state.profiles, notify: notify)
} else if type == .like && damus_state.settings.like_notification,
let evid = ev.referenced_ids.last?.ref_id,
let liked_event = damus_state.events.lookup(evid)
{
let content = NSAttributedString(render_note_content(ev: liked_event, profiles: damus_state.profiles, privkey: damus_state.keypair.privkey).content.attributed).string
let notify = LocalNotification(type: .like, event: ev, target: liked_event, content: content)
let notify = LocalNotification(type: .like, event: ev, target: liked_event, content: liked_event.content)
create_local_notification(profiles: damus_state.profiles, notify: notify)
}
@@ -1194,12 +1199,12 @@ func create_local_notification(profiles: Profiles, notify: LocalNotification) {
title = String(format: NSLocalizedString("Reposted by %@", comment: "Reposted by heading in local notification"), displayName)
identifier = "myBoostNotification"
case .like:
title = String(format: NSLocalizedString("%@ reacted with %@", comment: "Reacted by heading in local notification"), displayName, to_reaction_emoji(ev: notify.event) ?? "")
title = String(format: NSLocalizedString("%@ reacted with %@", comment: "Reacted by heading in local notification"), displayName, notify.event.content)
identifier = "myLikeNotification"
case .dm:
title = displayName
identifier = "myDMNotification"
case .zap, .profile_zap:
case .zap:
// not handled here
break
}
@@ -1221,75 +1226,3 @@ func create_local_notification(profiles: Profiles, notify: LocalNotification) {
}
}
enum ProcessZapResult {
case already_processed(Zap)
case done(Zap)
case failed
}
func process_zap_event(damus_state: DamusState, ev: NostrEvent, completion: @escaping (ProcessZapResult) -> Void) {
// These are zap notifications
guard let ptag = event_tag(ev, name: "p") else {
completion(.failed)
return
}
// just return the zap if we already have it
if let zap = damus_state.zaps.zaps[ev.id], case .zap(let z) = zap {
completion(.already_processed(z))
return
}
if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) {
guard let zap = process_zap_event_with_zapper(damus_state: damus_state, ev: ev, zapper: local_zapper) else {
completion(.failed)
return
}
damus_state.add_zap(zap: .zap(zap))
completion(.done(zap))
return
}
guard let profile = damus_state.profiles.lookup(id: ptag) else {
completion(.failed)
return
}
guard let lnurl = profile.lnurl else {
completion(.failed)
return
}
Task {
guard let zapper = await fetch_zapper_from_lnurl(lnurl) else {
completion(.failed)
return
}
DispatchQueue.main.async {
damus_state.profiles.zappers[ptag] = zapper
guard let zap = process_zap_event_with_zapper(damus_state: damus_state, ev: ev, zapper: zapper) else {
completion(.failed)
return
}
damus_state.add_zap(zap: .zap(zap))
completion(.done(zap))
}
}
}
fileprivate func process_zap_event_with_zapper(damus_state: DamusState, ev: NostrEvent, zapper: String) -> Zap? {
let our_keypair = damus_state.keypair
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
return nil
}
damus_state.add_zap(zap: .zap(zap))
return zap
}

View File

@@ -10,7 +10,7 @@ import Foundation
enum MentionType {
case pubkey
case event
var ref: String {
switch self {
case .pubkey:
@@ -495,17 +495,14 @@ func make_post_tags(post_blocks: [PostBlock], tags: [[String]], silent_mentions:
continue
}
if find_tag_ref(type: ref.key, id: ref.ref_id, tags: tags) != nil {
// Mention index is nil because indexed mentions from NIP-08 is deprecated.
// It has been replaced with NIP-27 text note references with nostr: prefixed URIs.
let mention = Mention(index: nil, type: mention_type, ref: ref)
if let ind = find_tag_ref(type: ref.key, id: ref.ref_id, tags: tags) {
let mention = Mention(index: ind, type: mention_type, ref: ref)
let block = Block.mention(mention)
blocks.append(block)
} else {
let ind = new_tags.count
new_tags.append(refid_to_tag(ref))
// Mention index is nil because indexed mentions from NIP-08 is deprecated.
// It has been replaced with NIP-27 text note references with nostr: prefixed URIs.
let mention = Mention(index: nil, type: mention_type, ref: ref)
let mention = Mention(index: ind, type: mention_type, ref: ref)
let block = Block.mention(mention)
blocks.append(block)
}

View File

@@ -1,12 +0,0 @@
//
// NotificationStatusModel.swift
// damus
//
// Created by William Casarin on 2023-06-23.
//
import Foundation
class NotificationStatusModel: ObservableObject {
@Published var new_events: NewEventsBits = NewEventsBits()
}

View File

@@ -21,12 +21,12 @@ class ZapGroup {
}
func zap_requests() -> [NostrEvent] {
zaps.map { z in z.request.ev }
zaps.map { z in z.request }
}
func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool {
for zap in zaps {
if !isIncluded(zap.request.ev) {
if !isIncluded(zap.request) {
return true
}
}
@@ -35,7 +35,7 @@ class ZapGroup {
}
func filter(_ isIncluded: (NostrEvent) -> Bool) -> ZapGroup? {
let new_zaps = zaps.filter { isIncluded($0.request.ev) }
let new_zaps = zaps.filter { isIncluded($0.request) }
guard new_zaps.count > 0 else {
return nil
}
@@ -60,8 +60,8 @@ class ZapGroup {
msat_total += zap.amount
if !zappers.contains(zap.request.ev.pubkey) {
zappers.insert(zap.request.ev.pubkey)
if !zappers.contains(zap.request.pubkey) {
zappers.insert(zap.request.pubkey)
}
return true

View File

@@ -150,7 +150,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
}
for zap in incoming_zaps {
pks.insert(zap.request.ev.pubkey)
pks.insert(zap.request.pubkey)
}
return Array(pks)
@@ -307,7 +307,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
changed = changed || incoming_events.count != count
count = profile_zaps.zaps.count
profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request.ev) }
profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request) }
changed = changed || profile_zaps.zaps.count != count
for el in reactions {
@@ -325,7 +325,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
for el in zaps {
count = el.value.zaps.count
el.value.zaps = el.value.zaps.filter {
isIncluded($0.request.ev)
isIncluded($0.request)
}
changed = changed || el.value.zaps.count != count
}

View File

@@ -21,6 +21,7 @@ struct NostrPost {
}
}
// TODO: parse nostr:{e,p}:pubkey uris as well
func parse_post_mention_type(_ p: Parser) -> MentionType? {
if parse_char(p, "@") {
return .pubkey

View File

@@ -69,14 +69,21 @@ class ProfileModel: ObservableObject, Equatable {
}
func subscribe() {
var text_filter = NostrFilter(kinds: [.text, .chat])
var text_filter = NostrFilter.filter_kinds([
NostrKind.text.rawValue,
NostrKind.chat.rawValue,
])
var profile_filter = NostrFilter(kinds: [.contacts, .metadata, .boost])
var profile_filter = NostrFilter.filter_kinds([
NostrKind.contacts.rawValue,
NostrKind.metadata.rawValue,
NostrKind.boost.rawValue,
])
profile_filter.authors = [pubkey]
text_filter.authors = [pubkey]
text_filter.limit = 50
text_filter.limit = 500
print("subscribing to profile \(pubkey) with sub_id \(sub_id)")
print_filters(relay_id: "profile", filters: [[text_filter], [profile_filter]])

View File

@@ -18,7 +18,6 @@ class SearchHomeModel: ObservableObject {
let base_subid = UUID().description
let profiles_subid = UUID().description
let limit: UInt32 = 250
//let multiple_events_per_pubkey: Bool = false
init(damus_state: DamusState) {
self.damus_state = damus_state
@@ -28,7 +27,7 @@ class SearchHomeModel: ObservableObject {
}
func get_base_filter() -> NostrFilter {
var filter = NostrFilter(kinds: [.text, .chat])
var filter = NostrFilter.filter_kinds([NostrKind.text.rawValue, NostrKind.chat.rawValue])
filter.limit = self.limit
filter.until = Int64(Date.now.timeIntervalSince1970)
return filter
@@ -61,7 +60,7 @@ class SearchHomeModel: ObservableObject {
return
}
if ev.is_textlike && should_show_event(contacts: damus_state.contacts, ev: ev) && !ev.is_reply(nil) {
if !damus_state.settings.multiple_events_per_pubkey && seen_pubkey.contains(ev.pubkey) {
if seen_pubkey.contains(ev.pubkey) {
return
}
seen_pubkey.insert(ev.pubkey)
@@ -91,6 +90,20 @@ class SearchHomeModel: ObservableObject {
}
}
func find_profiles_to_fetch_pk(profiles: Profiles, event_pubkeys: [String]) -> [String] {
var pubkeys = Set<String>()
for pk in event_pubkeys {
if profiles.lookup(id: pk) != nil {
continue
}
pubkeys.insert(pk)
}
return Array(pubkeys)
}
func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: EventCache) -> [String] {
switch load {
case .from_events(let events):
@@ -101,7 +114,17 @@ func find_profiles_to_fetch(profiles: Profiles, load: PubkeysToLoad, cache: Even
}
func find_profiles_to_fetch_from_keys(profiles: Profiles, pks: [String]) -> [String] {
Array(Set(pks.filter { pk in !profiles.has_fresh_profile(id: pk) }))
var pubkeys = Set<String>()
for pk in pks {
if profiles.lookup(id: pk) != nil {
continue
}
pubkeys.insert(pk)
}
return Array(pubkeys)
}
func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent], cache: EventCache) -> [String] {
@@ -109,11 +132,11 @@ func find_profiles_to_fetch_from_events(profiles: Profiles, events: [NostrEvent]
for ev in events {
// lookup profiles from boosted events
if ev.known_kind == .boost, let bev = ev.get_inner_event(cache: cache), !profiles.has_fresh_profile(id: bev.pubkey) {
if ev.known_kind == .boost, let bev = ev.get_inner_event(cache: cache), profiles.lookup(id: bev.pubkey) == nil {
pubkeys.insert(bev.pubkey)
}
if !profiles.has_fresh_profile(id: ev.pubkey) {
if profiles.lookup(id: ev.pubkey) == nil {
pubkeys.insert(ev.pubkey)
}
}
@@ -127,16 +150,16 @@ enum PubkeysToLoad {
}
func load_profiles(profiles_subid: String, relay_id: String, load: PubkeysToLoad, damus_state: DamusState) {
var filter = NostrFilter.filter_profiles
let authors = find_profiles_to_fetch(profiles: damus_state.profiles, load: load, cache: damus_state.events)
filter.authors = authors
guard !authors.isEmpty else {
return
}
print("loading \(authors.count) profiles from \(relay_id)")
let filter = NostrFilter(kinds: [.metadata],
authors: authors)
damus_state.pool.subscribe_to(sub_id: profiles_subid, filters: [filter], to: [relay_id]) { sub_id, conn_ev in
let (sid, done) = handle_subid_event(pool: damus_state.pool, relay_id: relay_id, ev: conn_ev) { sub_id, ev in
guard sub_id == profiles_subid else {

View File

@@ -34,7 +34,7 @@ class SearchModel: ObservableObject {
func subscribe() {
// since 1 month
search.limit = self.limit
search.kinds = [.text, .like]
search.kinds = [NostrKind.text.rawValue, NostrKind.like.rawValue]
//likes_filter.ids = ref_events.referenced_ids!

View File

@@ -10,21 +10,15 @@ import Foundation
/// manages the lifetime of a thread
class ThreadModel: ObservableObject {
@Published var event: NostrEvent
let original_event: NostrEvent
var event_map: Set<NostrEvent>
init(event: NostrEvent, damus_state: DamusState) {
self.damus_state = damus_state
self.event_map = Set()
self.event = event
self.original_event = event
add_event(event)
}
var is_original: Bool {
return original_event.id == event.id
}
let damus_state: DamusState
let profiles_subid = UUID().description
@@ -62,16 +56,16 @@ class ThreadModel: ObservableObject {
let thread_id = event.thread_id(privkey: nil)
ref_events.referenced_ids = [thread_id, event.id]
ref_events.kinds = [.text]
ref_events.kinds = [NostrKind.text.rawValue]
ref_events.limit = 1000
event_filter.ids = [thread_id, event.id]
meta_events.referenced_ids = [event.id]
var kinds: [NostrKind] = [.zap, .text, .boost]
var kinds = [NostrKind.zap.rawValue, NostrKind.text.rawValue, NostrKind.boost.rawValue]
if !damus_state.settings.onlyzaps_mode {
kinds.append(.like)
kinds.append(NostrKind.like.rawValue)
}
meta_events.kinds = kinds
@@ -107,10 +101,6 @@ class ThreadModel: ObservableObject {
if ev.known_kind == .metadata {
process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev)
} else if ev.known_kind == .zap {
process_zap_event(damus_state: damus_state, ev: ev) { zap in
}
} else if ev.is_textlike {
self.add_event(ev)
}
@@ -126,10 +116,3 @@ class ThreadModel: ObservableObject {
}
}
func get_top_zap(events: EventCache, evid: String) -> Zapping? {
return events.get_cache_data(evid).zaps_model.zaps.first(where: { zap in
!zap.request.marked_hidden
})
}

View File

@@ -19,11 +19,8 @@ let fallback_zap_amount = 1000
if let loaded = UserDefaults.standard.object(forKey: self.key) as? T {
self.value = loaded
} else if let loaded = UserDefaults.standard.object(forKey: key) as? T {
// If pubkey-scoped setting does not exist but the deprecated non-pubkey-scoped setting does,
// migrate the deprecated setting into the pubkey-scoped one and delete the deprecated one.
// try to load from deprecated non-pubkey-keyed setting
self.value = loaded
UserDefaults.standard.set(loaded, forKey: self.key)
UserDefaults.standard.removeObject(forKey: key)
} else {
self.value = default_value
}
@@ -51,11 +48,8 @@ let fallback_zap_amount = 1000
if let loaded = UserDefaults.standard.string(forKey: self.key), let val = T.init(from: loaded) {
self.value = val
} else if let loaded = UserDefaults.standard.string(forKey: key), let val = T.init(from: loaded) {
// If pubkey-scoped setting does not exist but the deprecated non-pubkey-scoped setting does,
// migrate the deprecated setting into the pubkey-scoped one and delete the deprecated one.
// try to load from deprecated non-pubkey-keyed setting
self.value = val
UserDefaults.standard.set(val.to_string(), forKey: self.key)
UserDefaults.standard.removeObject(forKey: key)
} else {
self.value = default_value
}
@@ -84,7 +78,7 @@ class UserSettingsStore: ObservableObject {
@StringSetting(key: "default_media_uploader", default_value: .nostrBuild)
var default_media_uploader: MediaUploader
@Setting(key: "show_wallet_selector", default_value: false)
@Setting(key: "show_wallet_selector", default_value: true)
var show_wallet_selector: Bool
@Setting(key: "left_handed", default_value: false)
@@ -126,10 +120,6 @@ class UserSettingsStore: ObservableObject {
@Setting(key: "truncate_timeline_text", default_value: false)
var truncate_timeline_text: Bool
/// Nozaps mode gimps note zapping to fit into apple's content-tipping guidelines. It can not be configurable to end-users on the app store
@Setting(key: "nozaps", default_value: true)
var nozaps: Bool
@Setting(key: "truncate_mention_text", default_value: true)
var truncate_mention_text: Bool
@@ -141,18 +131,12 @@ class UserSettingsStore: ObservableObject {
@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
@Setting(key: "onlyzaps_mode", default_value: false)
var onlyzaps_mode: Bool
@Setting(key: "disable_animation", default_value: UIAccessibility.isReduceMotionEnabled)
var disable_animation: Bool
@Setting(key: "donation_percent", default_value: 0)
var donation_percent: Int
// Helper for inverse of disable_animation.
// disable_animation was introduced as a setting first, but it's more natural for the settings UI to show the inverse.

View File

@@ -14,17 +14,14 @@ enum WalletConnectState {
}
class WalletModel: ObservableObject {
var settings: UserSettingsStore
let settings: UserSettingsStore?
private(set) var previous_state: WalletConnectState
var inital_percent: Int
@Published private(set) var connect_state: WalletConnectState
init(state: WalletConnectState, settings: UserSettingsStore) {
self.connect_state = state
init() {
self.connect_state = .none
self.previous_state = .none
self.settings = settings
self.inital_percent = settings.donation_percent
self.settings = nil
}
init(settings: UserSettingsStore) {
@@ -37,7 +34,6 @@ class WalletModel: ObservableObject {
self.previous_state = .none
self.connect_state = .none
}
self.inital_percent = settings.donation_percent
}
func cancel() {
@@ -46,7 +42,7 @@ class WalletModel: ObservableObject {
}
func disconnect() {
self.settings.nostr_wallet_connect = nil
self.settings?.nostr_wallet_connect = nil
self.connect_state = .none
self.previous_state = .none
}
@@ -56,7 +52,7 @@ class WalletModel: ObservableObject {
}
func connect(_ nwc: WalletConnectURL) {
self.settings.nostr_wallet_connect = nwc.to_url().absoluteString
self.settings?.nostr_wallet_connect = nwc.to_url().absoluteString
notify(.attached_wallet, nwc)
self.connect_state = .existing(nwc)
self.previous_state = .existing(nwc)

View File

@@ -1,13 +0,0 @@
//
// ZapButtonModel.swift
// damus
//
// Created by Terry Yiu on 6/1/23.
//
import Foundation
class ZapButtonModel: ObservableObject {
var invoice: String? = nil
@Published var zapping: String = ""
}

View File

@@ -1,29 +0,0 @@
//
// CustomizeZapModel.swift
// damus
//
// Created by William Casarin on 2023-06-22.
//
import Foundation
class CustomizeZapModel: ObservableObject {
@Published var comment: String = ""
@Published var custom_amount: String = ""
@Published var custom_amount_sats: Int? = nil
@Published var zap_type: ZapType = .pub
@Published var invoice: String = ""
@Published var error: String? = nil
@Published var zapping: Bool = false
@Published var show_zap_types: Bool = false
init() {
}
func set_defaults(settings: UserSettingsStore) {
self.zap_type = settings.default_zap_type
self.custom_amount = String(settings.default_zap_amount)
self.custom_amount_sats = settings.default_zap_amount
}
}

View File

@@ -24,7 +24,7 @@ class ZapsModel: ObservableObject {
}
func subscribe() {
var filter = NostrFilter(kinds: [.zap])
var filter = NostrFilter.filter_kinds([NostrKind.zap.rawValue])
switch target {
case .profile(let profile_id):
filter.pubkeys = [profile_id]
@@ -53,13 +53,18 @@ class ZapsModel: ObservableObject {
case .notice:
break
case .eose:
let events = state.events.lookup_zaps(target: target).map { $0.request.ev }
let events = state.events.lookup_zaps(target: target).map { $0.request }
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state)
case .event(_, let ev):
guard ev.kind == 9735 else {
return
}
if let zap = state.zaps.zaps[ev.id] {
state.events.store_zap(zap: zap)
return
}
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
return
}

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21754" systemVersion="22E261" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="PersistedProfile" representedClassName="PersistedProfile" syncable="YES">
<attribute name="about" optional="YES" attributeType="String"/>
<attribute name="banner" optional="YES" attributeType="String"/>
<attribute name="damus_donation" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="display_name" optional="YES" attributeType="String"/>
<attribute name="id" optional="YES" attributeType="String"/>
<attribute name="last_update" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="lud06" optional="YES" attributeType="String"/>
<attribute name="lud16" optional="YES" attributeType="String"/>
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="network_pull_date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="nip05" optional="YES" attributeType="String"/>
<attribute name="picture" optional="YES" attributeType="String"/>
<attribute name="website" optional="YES" attributeType="String"/>
</entity>
</model>

View File

@@ -1,39 +0,0 @@
//
// PersistedProfile.swift
// damus
//
// Created by Bryan Montz on 4/30/23.
//
import Foundation
import CoreData
@objc(PersistedProfile)
final class PersistedProfile: NSManagedObject {
@NSManaged var id: String?
@NSManaged var name: String?
@NSManaged var display_name: String?
@NSManaged var about: String?
@NSManaged var picture: String?
@NSManaged var banner: String?
@NSManaged var website: String?
@NSManaged var lud06: String?
@NSManaged var lud16: String?
@NSManaged var nip05: String?
@NSManaged var damus_donation: Int16
@NSManaged var last_update: Date? // The date that the profile was last updated by the user
@NSManaged var network_pull_date: Date? // The date we got this profile from a relay (for staleness checking)
func copyValues(from profile: Profile) {
name = profile.name
display_name = profile.display_name
about = profile.about
picture = profile.picture
banner = profile.banner
website = profile.website
lud06 = profile.lud06
lud16 = profile.lud16
nip05 = profile.nip05
damus_donation = profile.damus_donation != nil ? Int16(profile.damus_donation!) : 0
}
}

View File

@@ -10,7 +10,7 @@ import Foundation
class Profile: Codable {
var value: [String: AnyCodable]
init (name: String?, display_name: String?, about: String?, picture: String?, banner: String?, website: String?, lud06: String?, lud16: String?, nip05: String?, damus_donation: Int?) {
init (name: String?, display_name: String?, about: String?, picture: String?, banner: String?, website: String?, lud06: String?, lud16: String?, nip05: String?) {
self.value = [:]
self.name = name
self.display_name = display_name
@@ -21,30 +21,12 @@ class Profile: Codable {
self.lud06 = lud06
self.lud16 = lud16
self.nip05 = nip05
self.damus_donation = damus_donation
}
convenience init(persisted_profile: PersistedProfile) {
self.init(name: persisted_profile.name,
display_name: persisted_profile.display_name,
about: persisted_profile.about,
picture: persisted_profile.picture,
banner: persisted_profile.banner,
website: persisted_profile.website,
lud06: persisted_profile.lud06,
lud16: persisted_profile.lud16,
nip05: persisted_profile.nip05,
damus_donation: Int(persisted_profile.damus_donation))
}
private func str(_ str: String) -> String? {
return get_val(str)
}
private func int(_ key: String) -> Int? {
return get_val(key)
}
private func get_val<T>(_ v: String) -> T? {
guard let val = self.value[v] else{
return nil
@@ -70,10 +52,6 @@ class Profile: Codable {
set_val(key, val)
}
private func set_int(_ key: String, _ val: Int?) {
set_val(key, val)
}
var reactions: Bool? {
get { return get_val("reactions"); }
set(s) { set_val("reactions", s) }
@@ -99,11 +77,6 @@ class Profile: Codable {
set(s) { set_str("about", s) }
}
var damus_donation: Int? {
get { return int("damus_donation_v2"); }
set(s) { set_int("damus_donation_v2", s) }
}
var picture: String? {
get { return str("picture"); }
set(s) { set_str("picture", s) }
@@ -207,7 +180,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: "jb55@jb55.com", lud16: nil, nip05: "jb55@jb55.com")
}
func make_ln_url(_ str: String?) -> URL? {

View File

@@ -492,11 +492,11 @@ func make_boost_event(pubkey: String, privkey: String, boosted: NostrEvent) -> N
return ev
}
func make_like_event(pubkey: String, privkey: String, liked: NostrEvent, content: String = "🤙") -> NostrEvent {
func make_like_event(pubkey: String, privkey: String, liked: NostrEvent) -> NostrEvent {
var tags: [[String]] = liked.tags.filter { tag in tag.count >= 2 && (tag[0] == "e" || tag[0] == "p") }
tags.append(["e", liked.id])
tags.append(["p", liked.pubkey])
let ev = NostrEvent(content: content, pubkey: pubkey, kind: 7, tags: tags)
let ev = NostrEvent(content: "🤙", pubkey: pubkey, kind: 7, tags: tags)
ev.calculate_id()
ev.sign(privkey: privkey)
@@ -601,7 +601,7 @@ enum MakeZapRequest {
var private_inner_request: ZapRequest {
switch self {
case .priv(_, let pzr):
case .priv(let _, let pzr):
return pzr.req
case .normal(let zr):
return zr
@@ -966,28 +966,6 @@ func first_eref_mention(ev: NostrEvent, privkey: String?) -> Mention? {
return nil
}
/**
Transforms a `NostrEvent` of known kind `NostrKind.like`to a human-readable emoji.
If the known kind is not a `NostrKind.like`, it will return `nil`.
If the event content is an empty string or `+`, it will map that to a heart emoji.
If the event content is a "-", it will map that to a dislike 👎 emoji.
Otherwise, it will return the event content at face value without transforming it.
*/
func to_reaction_emoji(ev: NostrEvent) -> String? {
guard ev.known_kind == NostrKind.like else {
return nil
}
switch ev.content {
case "", "+":
return "❤️"
case "-":
return "👎"
default:
return ev.content
}
}
extension [ReferencedId] {
var pRefs: [ReferencedId] {
get {

View File

@@ -9,15 +9,15 @@ import Foundation
struct NostrFilter: Codable, Equatable {
var ids: [String]?
var kinds: [NostrKind]?
var kinds: [Int]?
var referenced_ids: [String]?
var pubkeys: [String]?
var since: Int64?
var until: Int64?
var limit: UInt32?
var authors: [String]?
var hashtag: [String]?
var parameter: [String]?
var hashtag: [String]? = nil
var parameter: [String]? = nil
private enum CodingKeys : String, CodingKey {
case ids
@@ -32,23 +32,31 @@ struct NostrFilter: Codable, Equatable {
case limit
}
init(ids: [String]? = nil, kinds: [NostrKind]? = nil, referenced_ids: [String]? = nil, pubkeys: [String]? = nil, since: Int64? = nil, until: Int64? = nil, limit: UInt32? = nil, authors: [String]? = nil, hashtag: [String]? = nil) {
self.ids = ids
self.kinds = kinds
self.referenced_ids = referenced_ids
self.pubkeys = pubkeys
self.since = since
self.until = until
self.limit = limit
self.authors = authors
self.hashtag = hashtag
}
public static func copy(from: NostrFilter) -> NostrFilter {
NostrFilter(ids: from.ids, kinds: from.kinds, referenced_ids: from.referenced_ids, pubkeys: from.pubkeys, since: from.since, until: from.until, authors: from.authors, hashtag: from.hashtag)
return NostrFilter(ids: from.ids, kinds: from.kinds, referenced_ids: from.referenced_ids, pubkeys: from.pubkeys, since: from.since, until: from.until, authors: from.authors, hashtag: from.hashtag)
}
public static func filter_hashtag(_ htags: [String]) -> NostrFilter {
NostrFilter(hashtag: htags.map { $0.lowercased() })
return NostrFilter(ids: nil, kinds: nil, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil, hashtag: htags.map { $0.lowercased() })
}
public static func filter_ids(_ ids: [String]) -> NostrFilter {
return NostrFilter(ids: ids, kinds: nil, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil, hashtag: nil)
}
public static var filter_profiles: NostrFilter {
return filter_kinds([NostrKind.metadata.rawValue])
}
public static var filter_contacts: NostrFilter {
return filter_kinds([NostrKind.contacts.rawValue])
}
public static func filter_authors(_ authors: [String]) -> NostrFilter {
return NostrFilter(ids: nil, kinds: nil, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: authors)
}
public static func filter_kinds(_ kinds: [Int]) -> NostrFilter {
return NostrFilter(ids: nil, kinds: kinds, referenced_ids: nil, pubkeys: nil, since: nil, until: nil, authors: nil)
}
}

View File

@@ -8,7 +8,7 @@
import Foundation
enum NostrKind: Int, Codable {
enum NostrKind: Int {
case metadata = 0
case text = 1
case contacts = 3

View File

@@ -60,13 +60,24 @@ func parse_nostr_ref_uri(_ p: Parser) -> ReferencedId? {
if !parse_str(p, "nostr:") {
return nil
}
guard let ref = parse_post_bech32_mention(p) else {
guard let typ = parse_nostr_ref_uri_type(p) else {
p.pos = start
return nil
}
return ref
if !parse_char(p, ":") {
p.pos = start
return nil
}
guard let pk = parse_hexstr(p, len: 64) else {
p.pos = start
return nil
}
// TODO: parse relays from nostr uris
return ReferencedId(ref_id: pk, relay_id: nil, key: typ)
}
func decode_universal_link(_ s: String) -> NostrLink? {
@@ -129,7 +140,7 @@ func decode_nostr_uri(_ s: String) -> NostrLink? {
}
if tag_is_hashtag(parts) {
return .filter(NostrFilter(hashtag: [parts[1].lowercased()]))
return .filter(NostrFilter.filter_hashtag([parts[1]]))
}
if let rid = tag_to_refid(parts) {

View File

@@ -1,181 +0,0 @@
//
// ProfileDatabase.swift
// damus
//
// Created by Bryan Montz on 4/30/23.
//
import Foundation
import CoreData
enum ProfileDatabaseError: Error {
case missing_context
case outdated_input
}
final class ProfileDatabase {
private let entity_name = "PersistedProfile"
private var persistent_container: NSPersistentContainer?
private var background_context: NSManagedObjectContext?
private let cache_url: URL
/// This queue is used to synchronize access to the network_pull_date_cache dictionary, which
/// prevents data races from crashing the app.
private var queue = DispatchQueue(label: "io.damus.profile_db",
qos: .userInteractive,
attributes: .concurrent)
private var network_pull_date_cache = [String: Date]()
init(cache_url: URL = ProfileDatabase.profile_cache_url) {
self.cache_url = cache_url
set_up()
}
private static var profile_cache_url: URL {
(FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.appendingPathComponent("profiles"))!
}
private var persistent_store_description: NSPersistentStoreDescription {
let description = NSPersistentStoreDescription(url: cache_url)
description.type = NSSQLiteStoreType
description.setOption(true as NSNumber, forKey: NSMigratePersistentStoresAutomaticallyOption)
description.setOption(true as NSNumber, forKey: NSInferMappingModelAutomaticallyOption)
description.setOption(true as NSNumber, forKey: NSSQLiteManualVacuumOption)
return description
}
private var object_model: NSManagedObjectModel? {
guard let url = Bundle.main.url(forResource: "Damus", withExtension: "momd") else {
return nil
}
return NSManagedObjectModel(contentsOf: url)
}
private func set_up() {
guard let object_model else {
print("⚠️ Warning: ProfileDatabase failed to load its object model")
return
}
persistent_container = NSPersistentContainer(name: "Damus", managedObjectModel: object_model)
persistent_container?.persistentStoreDescriptions = [persistent_store_description]
persistent_container?.loadPersistentStores { _, error in
if let error {
print("WARNING: ProfileDatabase failed to load: \(error)")
}
}
persistent_container?.viewContext.automaticallyMergesChangesFromParent = true
persistent_container?.viewContext.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
background_context = persistent_container?.newBackgroundContext()
background_context?.mergePolicy = NSMergePolicy(merge: .mergeByPropertyObjectTrumpMergePolicyType)
}
private func get_persisted(id: String, context: NSManagedObjectContext) -> PersistedProfile? {
let request = NSFetchRequest<PersistedProfile>(entityName: entity_name)
request.predicate = NSPredicate(format: "id == %@", id)
request.fetchLimit = 1
return try? context.fetch(request).first
}
func get_network_pull_date(id: String) -> Date? {
var pull_date: Date?
queue.sync {
pull_date = network_pull_date_cache[id]
}
if let pull_date {
return pull_date
}
let request = NSFetchRequest<PersistedProfile>(entityName: entity_name)
request.predicate = NSPredicate(format: "id == %@", id)
request.fetchLimit = 1
request.propertiesToFetch = ["network_pull_date"]
guard let profile = try? persistent_container?.viewContext.fetch(request).first else {
return nil
}
queue.async(flags: .barrier) {
self.network_pull_date_cache[id] = profile.network_pull_date
}
return profile.network_pull_date
}
// MARK: - Public
/// Updates or inserts a new Profile into the local database. Rejects profiles whose update date
/// is older than one we already have. Database writes occur on a background context for best performance.
/// - Parameters:
/// - id: Profile id (pubkey)
/// - profile: Profile object to be stored
/// - last_update: Date that the Profile was updated
func upsert(id: String, profile: Profile, last_update: Date) async throws {
guard let context = background_context else {
throw ProfileDatabaseError.missing_context
}
try await context.perform {
var persisted_profile: PersistedProfile?
if let profile = self.get_persisted(id: id, context: context) {
if let existing_last_update = profile.last_update, last_update < existing_last_update {
throw ProfileDatabaseError.outdated_input
} else {
persisted_profile = profile
}
} else {
persisted_profile = NSEntityDescription.insertNewObject(forEntityName: self.entity_name, into: context) as? PersistedProfile
persisted_profile?.id = id
}
persisted_profile?.copyValues(from: profile)
persisted_profile?.last_update = last_update
let pull_date = Date.now
persisted_profile?.network_pull_date = pull_date
self.queue.async(flags: .barrier) {
self.network_pull_date_cache[id] = pull_date
}
try context.save()
}
}
func get(id: String) -> Profile? {
guard let container = persistent_container,
let profile = get_persisted(id: id, context: container.viewContext) else {
return nil
}
return Profile(persisted_profile: profile)
}
var count: Int {
let request = NSFetchRequest<PersistedProfile>(entityName: entity_name)
let count = try? persistent_container?.viewContext.count(for: request)
return count ?? 0
}
func remove_all_profiles() throws {
guard let context = background_context, let container = persistent_container else {
throw ProfileDatabaseError.missing_context
}
queue.async(flags: .barrier) {
self.network_pull_date_cache.removeAll()
}
let request = NSFetchRequest<NSFetchRequestResult>(entityName: entity_name)
let batch_delete_request = NSBatchDeleteRequest(fetchRequest: request)
batch_delete_request.resultType = .resultTypeObjectIDs
let result = try container.persistentStoreCoordinator.execute(batch_delete_request, with: context) as! NSBatchDeleteResult
// NSBatchDeleteRequest is an NSPersistentStoreRequest, which operates on disk. So now we'll manually update our in-memory context.
if let object_ids = result.result as? [NSManagedObjectID] {
let changes: [AnyHashable: Any] = [
NSDeletedObjectsKey: object_ids
]
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [context])
}
}
}

View File

@@ -6,58 +6,44 @@
//
import Foundation
import UIKit
class Profiles {
static let db_freshness_threshold: TimeInterval = 24 * 60 * 60
/// This queue is used to synchronize access to the profiles dictionary, which
/// prevents data races from crashing the app.
private var queue = DispatchQueue(label: "io.damus.profiles",
qos: .userInteractive,
attributes: .concurrent)
private var profiles: [String: TimestampedProfile] = [:]
var profiles: [String: TimestampedProfile] = [:]
var validated: [String: NIP05] = [:]
var nip05_pubkey: [String: String] = [:]
var zappers: [String: String] = [:]
private let database = ProfileDatabase()
func is_validated(_ pk: String) -> NIP05? {
validated[pk]
}
func enumerated() -> EnumeratedSequence<[String: TimestampedProfile]> {
return queue.sync {
return profiles.enumerated()
}
return validated[pk]
}
func lookup_zapper(pubkey: String) -> String? {
zappers[pubkey]
if let zapper = zappers[pubkey] {
return zapper
}
return nil
}
func add(id: String, profile: TimestampedProfile) {
queue.async(flags: .barrier) {
self.profiles[id] = profile
}
Task {
do {
try await database.upsert(id: id, profile: profile.profile, last_update: Date(timeIntervalSince1970: TimeInterval(profile.timestamp)))
} catch {
print("⚠️ Warning: Profiles failed to save a profile: \(error)")
}
}
}
func lookup(id: String) -> Profile? {
var profile: Profile?
queue.sync {
profile = profiles[id]?.profile
return profiles[id]?.profile
}
return profile ?? database.get(id: id)
}
func lookup_with_timestamp(id: String) -> TimestampedProfile? {
@@ -65,23 +51,6 @@ class Profiles {
return profiles[id]
}
}
func has_fresh_profile(id: String) -> Bool {
// check memory first
var profile: Profile?
queue.sync {
profile = profiles[id]?.profile
}
if profile != nil {
return true
}
// then disk
guard let pull_date = database.get_network_pull_date(id: id) else {
return false
}
return Date.now.timeIntervalSince(pull_date) < Profiles.db_freshness_threshold
}
}

View File

@@ -37,9 +37,9 @@ public struct RelayURL: Hashable {
}
}
final class RelayConnection: ObservableObject {
@Published private(set) var isConnected = false
@Published private(set) var isConnecting = false
final class RelayConnection {
private(set) var isConnected = false
private(set) var isConnecting = false
private(set) var last_connection_attempt: TimeInterval = 0
private(set) var last_pong: Date? = nil
@@ -129,11 +129,6 @@ final class RelayConnection: ObservableObject {
}
case .error(let error):
print("⚠️ Warning: RelayConnection (\(self.url)) error: \(error)")
let nserr = error as NSError
if nserr.domain == NSPOSIXErrorDomain && nserr.code == 57 {
// ignore socket not connected?
return
}
DispatchQueue.main.async {
self.isConnected = false
self.isConnecting = false

View File

@@ -18,16 +18,11 @@ struct QueuedRequest {
let relay: String
}
struct SeenEvent: Hashable {
let relay_id: String
let evid: String
}
class RelayPool {
var relays: [Relay] = []
var handlers: [RelayHandler] = []
var request_queue: [QueuedRequest] = []
var seen: Set<SeenEvent> = Set()
var seen: Set<String> = Set()
var counts: [String: UInt64] = [:]
private let network_monitor = NWPathMonitor()
@@ -238,7 +233,7 @@ class RelayPool {
func record_seen(relay_id: String, event: NostrConnectionEvent) {
if case .nostr_event(let ev) = event {
if case .event(_, let nev) = ev {
let k = SeenEvent(relay_id: relay_id, evid: nev.id)
let k = relay_id + nev.id
if !seen.contains(k) {
seen.insert(k)
if counts[relay_id] == nil {

View File

@@ -13,19 +13,6 @@ enum WebSocketEvent {
case message(URLSessionWebSocketTask.Message)
case disconnected(URLSessionWebSocketTask.CloseCode, String?)
case error(Error)
var description: String? {
switch self {
case .connected:
return "Connected"
case .message(_):
return "Received message"
case .disconnected(let close_code, let reason):
return "Disconnected: Close code: \(close_code), reason: \(reason ?? "unknown")"
case .error(let error):
return "Error: \(error)"
}
}
}
final class WebSocket: NSObject, URLSessionWebSocketDelegate {

View File

@@ -38,7 +38,7 @@ enum DisplayName {
func parse_display_name(profile: Profile?, pubkey: String) -> DisplayName {
if pubkey == ANON_PUBKEY {
if pubkey == "anon" {
return .one(NSLocalizedString("Anonymous", comment: "Placeholder display name of anonymous user."))
}

View File

@@ -62,7 +62,7 @@ class ZapsDataModel: ObservableObject {
}
func confirm_nwc(reqid: String) {
guard let zap = zaps.first(where: { z in z.request.ev.id == reqid }),
guard let zap = zaps.first(where: { z in z.request.id == reqid }),
case .pending(let pzap) = zap
else {
return
@@ -83,16 +83,16 @@ class ZapsDataModel: ObservableObject {
}
func from(_ pubkey: String) -> [Zapping] {
return self.zaps.filter { z in z.request.ev.pubkey == pubkey }
return self.zaps.filter { z in z.request.pubkey == pubkey }
}
@discardableResult
func remove(reqid: String) -> Bool {
guard zaps.first(where: { z in z.request.ev.id == reqid }) != nil else {
guard zaps.first(where: { z in z.request.id == reqid }) != nil else {
return false
}
self.zaps = zaps.filter { z in z.request.ev.id != reqid }
self.zaps = zaps.filter { z in z.request.id != reqid }
return true
}
}
@@ -101,10 +101,6 @@ class RelativeTimeModel: ObservableObject {
@Published var value: String = ""
}
class MediaMetaModel: ObservableObject {
@Published var fill: ImageFill? = nil
}
class EventData {
var translations_model: TranslationModel
var artifacts_model: NoteArtifactsModel
@@ -112,7 +108,6 @@ class EventData {
var zaps_model : ZapsDataModel
var relative_time: RelativeTimeModel = RelativeTimeModel()
var validated: ValidationResult
var media_metadata_model: MediaMetaModel
var translations: TranslateStatus {
return translations_model.state
@@ -131,7 +126,6 @@ class EventData {
self.artifacts_model = .init(state: .not_loaded)
self.zaps_model = .init(zaps)
self.validated = .unknown
self.media_metadata_model = MediaMetaModel()
self.preview_model = .init(state: .not_loaded)
}
}
@@ -141,7 +135,6 @@ class EventCache {
private var replies = ReplyMap()
private var cancellable: AnyCancellable?
private var image_metadata: [String: ImageMetadataState] = [:]
private var video_meta: [String: VideoPlayerModel] = [:]
private var event_data: [String: EventData] = [:]
//private var thread_latest: [String: Int64]
@@ -175,9 +168,6 @@ class EventCache {
@discardableResult
func store_zap(zap: Zapping) -> Bool {
let data = get_cache_data(zap.target.id).zaps_model
if let ev = zap.event {
insert(ev)
}
return insert_uniq_sorted_zap_by_amount(zaps: &data.zaps, new_zap: zap)
}
@@ -185,7 +175,7 @@ class EventCache {
switch zap.target {
case .note(let note_target):
let zaps = get_cache_data(note_target.note_id).zaps_model
zaps.remove(reqid: zap.request.ev.id)
zaps.remove(reqid: zap.request.id)
case .profile:
// these aren't stored anywhere yet
break
@@ -204,30 +194,6 @@ class EventCache {
return image_metadata[url.absoluteString.lowercased()]
}
@MainActor
func lookup_media_size(url: URL) -> CGSize? {
if let img_meta = lookup_img_metadata(url: url) {
return img_meta.meta.dim?.size
}
return get_video_player_model(url: url).size
}
func store_video_player_model(url: URL, meta: VideoPlayerModel) {
video_meta[url.absoluteString] = meta
}
@MainActor
func get_video_player_model(url: URL) -> VideoPlayerModel {
if let model = video_meta[url.absoluteString] {
return model
}
let model = VideoPlayerModel()
video_meta[url.absoluteString] = model
return model
}
func parent_events(event: NostrEvent) -> [NostrEvent] {
var parents: [NostrEvent] = []
@@ -291,7 +257,6 @@ class EventCache {
private func prune() {
events = [:]
video_meta = [:]
event_data = [:]
replies.replies = [:]
}
@@ -400,19 +365,11 @@ func preload_image(url: URL) {
print("Preloading image \(url.absoluteString)")
KingfisherManager.shared.retrieveImage(with: Kingfisher.ImageResource(downloadURL: url)) { val in
KingfisherManager.shared.retrieveImage(with: ImageResource(downloadURL: url)) { val in
print("Preloaded image \(url.absoluteString)")
}
}
func is_animated_image(url: URL) -> Bool {
guard let ext = url.pathComponents.last?.split(separator: ".").last?.lowercased() else {
return false
}
return ext == "gif"
}
func preload_event(plan: PreloadPlan, state: DamusState) async {
var artifacts: NoteArtifacts? = plan.data.artifacts.artifacts
let settings = state.settings
@@ -431,13 +388,6 @@ func preload_event(plan: PreloadPlan, state: DamusState) async {
}
for url in arts.images {
guard !is_animated_image(url: url) else {
// jb55: I have a theory that animated images are not working with the preloader due
// to some disk-cache write race condition. normal images need not apply
continue
}
preload_image(url: url)
}
}

View File

@@ -11,7 +11,7 @@ import Foundation
class EventHolder: ObservableObject, ScrollQueue {
private var has_event: Set<String>
@Published var events: [NostrEvent]
var incoming: [NostrEvent]
@Published var incoming: [NostrEvent]
var should_queue: Bool
var on_queue: ((NostrEvent) -> Void)?

View File

@@ -40,7 +40,7 @@ extension KFOptionSetter {
func onFailure(fallbackUrl: URL?, cacheKey: String?) -> Self {
guard let url = fallbackUrl, let key = cacheKey else { return self }
let imageResource = Kingfisher.ImageResource(downloadURL: url, cacheKey: key)
let imageResource = ImageResource(downloadURL: url, cacheKey: key)
let source = imageResource.convertToSource()
options.alternativeSources = [source]

View File

@@ -60,7 +60,7 @@ func hashtag_str(_ htag: String) -> CompatibleText {
}
text = Text(attributedString)
let img = Image("\(name)-hashtag")
text = text + Text(img).baselineOffset(custom_hashtag.offset ?? 0.0)
text = text + Text("\(img)").baselineOffset(custom_hashtag.offset ?? 0.0)
} else {
attributedString.foregroundColor = DamusColors.purple
}

View File

@@ -59,15 +59,17 @@ struct ImageMetadata: Equatable {
}
func process_blurhash(blurhash: String, size: CGSize?) async -> UIImage? {
let res = Task.detached(priority: .low) {
let res = Task.init {
let size = get_blurhash_size(img_size: size ?? CGSize(width: 100.0, height: 100.0))
guard let img = UIImage.init(blurHash: blurhash, size: size) else {
let noimg: UIImage? = nil
return noimg
}
return img
}
return await res.value
}
@@ -144,7 +146,7 @@ func calculate_blurhash(img: UIImage) async -> String? {
return nil
}
let res = Task.detached(priority: .low) {
let res = Task.init {
let bhs = get_blurhash_size(img_size: img.size)
let smaller = img.resized(to: bhs)

View File

@@ -11,10 +11,9 @@ func insert_uniq_sorted_zap(zaps: inout [Zapping], new_zap: Zapping, cmp: (Zappi
var i: Int = 0
for zap in zaps {
if new_zap.request.ev.id == zap.request.ev.id {
if new_zap.request.id == zap.request.id {
// replace pending
if !new_zap.is_pending && zap.is_pending {
print("nwc: replacing pending with real zap \(new_zap.request.ev.id)")
zaps[i] = new_zap
return true
}

View File

@@ -9,7 +9,6 @@ import Foundation
import secp256k1
let PUBKEY_HRP = "npub"
let ANON_PUBKEY = "anon"
struct FullKeypair: Equatable {
let pubkey: String

View File

@@ -44,5 +44,4 @@ enum LocalNotificationType: String {
case mention
case repost
case zap
case profile_zap
}

View File

@@ -77,9 +77,6 @@ extension Notification.Name {
static var update_stats: Notification.Name {
return Notification.Name("update_stats")
}
static var present_sheet: Notification.Name {
return Notification.Name("present_sheet")
}
static var zapping: Notification.Name {
return Notification.Name("zapping")
}

View File

@@ -22,27 +22,18 @@ class Relayer {
}
}
enum OnFlush {
case once((PostedEvent) -> Void)
case all((PostedEvent) -> Void)
}
class PostedEvent {
let event: NostrEvent
let skip_ephemeral: Bool
var remaining: [Relayer]
let flush_after: Date?
var flushed_once: Bool
let on_flush: OnFlush?
init(event: NostrEvent, remaining: [String], skip_ephemeral: Bool, flush_after: Date?, on_flush: OnFlush?) {
init(event: NostrEvent, remaining: [String], skip_ephemeral: Bool, flush_after: Date? = nil) {
self.event = event
self.skip_ephemeral = skip_ephemeral
self.flush_after = flush_after
self.on_flush = on_flush
self.flushed_once = false
self.remaining = remaining.map {
Relayer(relay: $0, attempts: 0, retry_after: 10.0)
Relayer(relay: $0, attempts: 0, retry_after: 2.0)
}
}
}
@@ -118,19 +109,6 @@ class PostBox {
guard let ev = self.events[event_id] else {
return false
}
if let on_flush = ev.on_flush {
switch on_flush {
case .once(let cb):
if !ev.flushed_once {
ev.flushed_once = true
cb(ev)
}
case .all(let cb):
cb(ev)
}
}
let prev_count = ev.remaining.count
ev.remaining = ev.remaining.filter { $0.relay != relay_id }
let after_count = ev.remaining.count
@@ -150,16 +128,11 @@ class PostBox {
relayer.attempts += 1
relayer.last_attempt = Int64(Date().timeIntervalSince1970)
relayer.retry_after *= 1.5
if pool.get_relay(relayer.relay) != nil {
print("flushing event \(event.event.id) to \(relayer.relay)")
} else {
print("could not find relay when flushing: \(relayer.relay)")
}
pool.send(.event(event.event), to: [relayer.relay], skip_ephemeral: event.skip_ephemeral)
}
}
func send(_ event: NostrEvent, to: [String]? = nil, skip_ephemeral: Bool = true, delay: TimeInterval? = nil, on_flush: OnFlush? = nil) {
func send(_ event: NostrEvent, to: [String]? = nil, skip_ephemeral: Bool = true, delay: TimeInterval? = nil) {
// Don't add event if we already have it
if events[event.id] != nil {
return
@@ -167,7 +140,7 @@ class PostBox {
let remaining = to ?? pool.our_descriptors.map { $0.url.id }
let after = delay.map { d in Date.now.addingTimeInterval(d) }
let posted_ev = PostedEvent(event: event, remaining: remaining, skip_ephemeral: skip_ephemeral, flush_after: after, on_flush: on_flush)
let posted_ev = PostedEvent(event: event, remaining: remaining, skip_ephemeral: skip_ephemeral, flush_after: after)
events[event.id] = posted_ev

View File

@@ -66,12 +66,22 @@ enum PreviewState {
class PreviewCache {
private var previews: [String: Preview]
private var image_meta: [String: ImageFill]
func lookup(_ evid: String) -> Preview? {
return previews[evid]
}
func lookup_image_meta(_ evid: String) -> ImageFill? {
return image_meta[evid]
}
func cache_image_meta(evid: String, image_fill: ImageFill) {
self.image_meta[evid] = image_fill
}
init() {
self.previews = [:]
self.image_meta = [:]
}
}

View File

@@ -1,276 +0,0 @@
//
// Router.swift
// damus
//
// Created by Scott Penrose on 5/7/23.
//
import SwiftUI
enum Route: Hashable {
case ProfileByKey(pubkey: String)
case Profile(profile: ProfileModel, followers: FollowersModel)
case Followers(followers: FollowersModel)
case Relay(relay: String, showActionButtons: Binding<Bool>)
case RelayDetail(relay: String, metadata: RelayMetadata)
case Following(following: FollowingModel)
case MuteList(users: [String])
case RelayConfig
case Bookmarks
case Config
case EditMetadata
case DMChat(dms: DirectMessageModel)
case UserRelays(relays: [String])
case KeySettings(keypair: Keypair)
case AppearanceSettings(settings: UserSettingsStore)
case NotificationSettings(settings: UserSettingsStore)
case ZapSettings(settings: UserSettingsStore)
case TranslationSettings(settings: UserSettingsStore)
case SearchSettings(settings: UserSettingsStore)
case Thread(thread: ThreadModel)
case Reposts(reposts: RepostsModel)
case Reactions(reactions: ReactionsModel)
case Zaps(target: ZapTarget)
case Search(search: SearchModel)
case EULA
case Login
case CreateAccount
case SaveKeys(account: CreateAccountModel)
case Wallet(wallet: WalletModel)
case WalletScanner(result: Binding<WalletScanResult>)
case FollowersYouKnow(friendedFollowers: [String], followers: FollowersModel)
@ViewBuilder
func view(navigationCordinator: NavigationCoordinator, damusState: DamusState) -> some View {
switch self {
case .ProfileByKey(let pubkey):
ProfileView(damus_state: damusState, pubkey: pubkey)
case .Profile(let profile, let followers):
ProfileView(damus_state: damusState, profile: profile, followers: followers)
case .Followers(let followers):
FollowersView(damus_state: damusState, followers: followers)
case .Relay(let relay, let showActionButtons):
RelayView(state: damusState, relay: relay, showActionButtons: showActionButtons)
case .RelayDetail(let relay, let metadata):
RelayDetailView(state: damusState, relay: relay, nip11: metadata)
case .Following(let following):
FollowingView(damus_state: damusState, following: following)
case .MuteList(let users):
MutelistView(damus_state: damusState, users: users)
case .RelayConfig:
RelayConfigView(state: damusState)
case .Bookmarks:
BookmarksView(state: damusState)
case .Config:
ConfigView(state: damusState)
case .EditMetadata:
EditMetadataView(damus_state: damusState)
case .DMChat(let dms):
DMChatView(damus_state: damusState, dms: dms)
case .UserRelays(let relays):
UserRelaysView(state: damusState, relays: relays)
case .KeySettings(let keypair):
KeySettingsView(keypair: keypair)
case .AppearanceSettings(let settings):
AppearanceSettingsView(settings: settings)
case .NotificationSettings(let settings):
NotificationSettingsView(settings: settings)
case .ZapSettings(let settings):
ZapSettingsView(settings: settings)
case .TranslationSettings(let settings):
NotificationSettingsView(settings: settings)
case .SearchSettings(let settings):
SearchSettingsView(settings: settings)
case .Thread(let thread):
ThreadView(state: damusState, thread: thread)
case .Reposts(let reposts):
RepostsView(damus_state: damusState, model: reposts)
case .Reactions(let reactions):
ReactionsView(damus_state: damusState, model: reactions)
case .Zaps(let target):
ZapsView(state: damusState, target: target)
case .Search(let search):
SearchView(appstate: damusState, search: search)
case .EULA:
EULAView(nav: navigationCordinator)
case .Login:
LoginView(nav: navigationCordinator)
case .CreateAccount:
CreateAccountView(nav: navigationCordinator)
case .SaveKeys(let account):
SaveKeysView(account: account)
case .Wallet(let walletModel):
WalletView(damus_state: damusState, model: walletModel)
case .WalletScanner(let walletScanResult):
WalletScannerView(result: walletScanResult)
case .FollowersYouKnow(let friendedFollowers, let followers):
FollowersYouKnowView(damus_state: damusState, friended_followers: friendedFollowers, followers: followers)
}
}
static func == (lhs: Route, rhs: Route) -> Bool {
switch (lhs, rhs) {
case (.ProfileByKey (let lhs_pubkey), .ProfileByKey(let rhs_pubkey)):
return lhs_pubkey == rhs_pubkey
case (.Profile (let lhs_profile, _), .Profile(let rhs_profile, _)):
return lhs_profile == rhs_profile
case (.Followers (_), .Followers (_)):
return true
case (.Relay (let lhs_relay, _), .Relay (let rhs_relay, _)):
return lhs_relay == rhs_relay
case (.RelayDetail(let lhs_relay, _), .RelayDetail(let rhs_relay, _)):
return lhs_relay == rhs_relay
case (.Following(_), .Following(_)):
return true
case (.MuteList(let lhs_users), .MuteList(let rhs_users)):
return lhs_users == rhs_users
case (.RelayConfig, .RelayConfig):
return true
case (.Bookmarks, .Bookmarks):
return true
case (.Config, .Config):
return true
case (.EditMetadata, .EditMetadata):
return true
case (.DMChat(let lhs_dms), .DMChat(let rhs_dms)):
return lhs_dms.our_pubkey == rhs_dms.our_pubkey
case (.UserRelays(let lhs_relays), .UserRelays(let rhs_relays)):
return lhs_relays == rhs_relays
case (.KeySettings(let lhs_keypair), .KeySettings(let rhs_keypair)):
return lhs_keypair.pubkey == rhs_keypair.pubkey
case (.AppearanceSettings(_), .AppearanceSettings(_)):
return true
case (.NotificationSettings(_), .NotificationSettings(_)):
return true
case (.ZapSettings(_), .ZapSettings(_)):
return true
case (.TranslationSettings(_), .TranslationSettings(_)):
return true
case (.SearchSettings(_), .SearchSettings(_)):
return true
case (.Thread(let lhs_threadModel), .Thread(thread: let rhs_threadModel)):
return lhs_threadModel.event.id == rhs_threadModel.event.id
case (.Reposts(let lhs_reposts), .Reposts(let rhs_reposts)):
return lhs_reposts.target == rhs_reposts.target
case (.Reactions(let lhs_reactions), .Reactions(let rhs_reactions)):
return lhs_reactions.target == rhs_reactions.target
case (.Zaps(let lhs_target), .Zaps(let rhs_target)):
return lhs_target == rhs_target
case (.Search(let lhs_search), .Search(let rhs_search)):
return lhs_search.sub_id == rhs_search.sub_id && lhs_search.profiles_subid == rhs_search.profiles_subid
case (.EULA, .EULA):
return true
case (.Login, .Login):
return true
case (.CreateAccount, .CreateAccount):
return true
case (.SaveKeys(let lhs_account), .SaveKeys(let rhs_account)):
return lhs_account.pubkey == rhs_account.pubkey
case (.Wallet(_), .Wallet(_)):
return true
case (.WalletScanner(_), .WalletScanner(_)):
return true
case (.FollowersYouKnow(_, _), .FollowersYouKnow(_, _)):
return true
default:
return false
}
}
func hash(into hasher: inout Hasher) {
switch self {
case .ProfileByKey(let pubkey):
hasher.combine("profilebykey")
hasher.combine(pubkey)
case .Profile(let profile, _):
hasher.combine("profile")
hasher.combine(profile.pubkey)
case .Followers(_):
hasher.combine("followers")
case .Relay(let relay, _):
hasher.combine("relay")
hasher.combine(relay)
case .RelayDetail(let relay, _):
hasher.combine("relayDetail")
hasher.combine(relay)
case .Following(_):
hasher.combine("following")
case .MuteList(let users):
hasher.combine("muteList")
hasher.combine(users)
case .RelayConfig:
hasher.combine("relayConfig")
case .Bookmarks:
hasher.combine("bookmarks")
case .Config:
hasher.combine("config")
case .EditMetadata:
hasher.combine("editMetadata")
case .DMChat(let dms):
hasher.combine("dms")
hasher.combine(dms.our_pubkey)
case .UserRelays(let relays):
hasher.combine("userRelays")
hasher.combine(relays)
case .KeySettings(let keypair):
hasher.combine("keySettings")
hasher.combine(keypair.pubkey)
case .AppearanceSettings(_):
hasher.combine("appearanceSettings")
case .NotificationSettings(_):
hasher.combine("notificationSettings")
case .ZapSettings(_):
hasher.combine("zapSettings")
case .TranslationSettings(_):
hasher.combine("translationSettings")
case .SearchSettings(_):
hasher.combine("searchSettings")
case .Thread(let threadModel):
hasher.combine("thread")
hasher.combine(threadModel.event.id)
case .Reposts(let reposts):
hasher.combine("reposts")
hasher.combine(reposts.target)
case .Zaps(let target):
hasher.combine("zaps")
hasher.combine(target.id)
hasher.combine(target.pubkey)
case .Reactions(let reactions):
hasher.combine("reactions")
hasher.combine(reactions.target)
case .Search(let search):
hasher.combine("search")
hasher.combine(search.sub_id)
hasher.combine(search.profiles_subid)
case .EULA:
hasher.combine("eula")
case .Login:
hasher.combine("login")
case .CreateAccount:
hasher.combine("createAccount")
case .SaveKeys(let account):
hasher.combine("saveKeys")
hasher.combine(account.pubkey)
case .Wallet(_):
hasher.combine("wallet")
case .WalletScanner(_):
hasher.combine("walletScanner")
case .FollowersYouKnow(let friendedFollowers, let followers):
hasher.combine("followersYouKnow")
hasher.combine(friendedFollowers)
hasher.combine(followers.sub_id)
}
}
}
class NavigationCoordinator: ObservableObject {
@Published var path = [Route]()
func push(route: Route) {
path.append(route)
}
func popToRoot() {
path = []
}
}

View File

@@ -1,34 +0,0 @@
//
// StringUtil.swift
// damus
//
// Created by Terry Yiu on 6/4/23.
//
import Foundation
extension String {
/// Returns a copy of the String truncated to maxLength and "..." ellipsis appended to the end,
/// or if the String does not exceed maxLength, the String itself is returned without truncation or added ellipsis.
func truncate(maxLength: Int) -> String {
guard count > maxLength else {
return self
}
return self[...self.index(self.startIndex, offsetBy: maxLength - 1)] + "..."
}
}
extension AttributedString {
/// Returns a copy of the AttributedString truncated to maxLength and "..." ellipsis appended to the end,
/// or if the AttributedString does not exceed maxLength, nil is returned.
func truncateOrNil(maxLength: Int) -> AttributedString? {
let nsAttributedString = NSAttributedString(self)
if nsAttributedString.length < maxLength { return nil }
let range = NSRange(location: 0, length: maxLength)
let truncatedAttributedString = nsAttributedString.attributedSubstring(from: range)
return AttributedString(truncatedAttributedString) + "..."
}
}

View File

@@ -36,8 +36,7 @@ struct WalletConnectURL: Equatable {
}
init?(str: String) {
guard let url = URL(string: str),
url.scheme == "nostrwalletconnect" || url.scheme == "nostr+walletconnect",
guard let url = URL(string: str), url.scheme == "nostrwalletconnect",
let pk = url.host, pk.utf8.count == 64,
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
let items = components.queryItems,
@@ -175,16 +174,15 @@ func make_wallet_connect_request<T>(req: WalletRequest<T>, to_pk: String, keypai
}
func subscribe_to_nwc(url: WalletConnectURL, pool: RelayPool) {
var filter = NostrFilter(kinds: [.nwc_response])
var filter: NostrFilter = .filter_kinds([NostrKind.nwc_response.rawValue])
filter.authors = [url.pubkey]
filter.limit = 0
let sub = NostrSubscribe(filters: [filter], sub_id: "nwc")
pool.send(.subscribe(sub), to: [url.relay.id], skip_ephemeral: false)
pool.send(.subscribe(sub), to: [url.relay.id])
}
@discardableResult
func nwc_pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: String, delay: TimeInterval? = 5.0, on_flush: OnFlush? = nil) -> NostrEvent? {
func nwc_pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: String) -> NostrEvent? {
let req = make_wallet_pay_invoice_request(invoice: invoice)
guard let ev = make_wallet_connect_request(req: req, to_pk: url.pubkey, keypair: url.keypair) else {
return nil
@@ -192,14 +190,14 @@ func nwc_pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: Str
try? pool.add_relay(.nwc(url: url.relay))
subscribe_to_nwc(url: url, pool: pool)
post.send(ev, to: [url.relay.id], skip_ephemeral: false, delay: delay, on_flush: on_flush)
post.send(ev, to: [url.relay.id], skip_ephemeral: false, delay: 5.0)
return ev
}
func nwc_success(state: DamusState, resp: FullWalletResponse) {
func nwc_success(zapcache: Zaps, evcache: EventCache, resp: FullWalletResponse) {
// find the pending zap and mark it as pending-confirmed
for kv in state.zaps.our_zaps {
for kv in zapcache.our_zaps {
let zaps = kv.value
for zap in zaps {
@@ -213,30 +211,14 @@ func nwc_success(state: DamusState, resp: FullWalletResponse) {
if nwc_state.update_state(state: .confirmed) {
// notify the zaps model of an update so it can mark them as paid
state.events.get_cache_data(pzap.target.id).zaps_model.objectWillChange.send()
evcache.get_cache_data(pzap.target.id).zaps_model.objectWillChange.send()
print("NWC success confirmed")
}
return
}
}
}
func send_donation_zap(pool: RelayPool, postbox: PostBox, nwc: WalletConnectURL, percent: Int, base_msats: Int64) async {
let percent_f = Double(percent) / 100.0
let donations_msats = Int64(percent_f * Double(base_msats))
let payreq = LNUrlPayRequest(allowsNostr: true, commentAllowed: nil, nostrPubkey: "", callback: "https://sendsats.lol/@damus")
guard let invoice = await fetch_zap_invoice(payreq, zapreq: nil, msats: donations_msats, zap_type: .non_zap, comment: nil) else {
// we failed... oh well. no donation for us.
print("damus-donation failed to fetch invoice")
return
}
print("damus-donation donating...")
nwc_pay(url: nwc, pool: pool, post: postbox, invoice: invoice, delay: nil)
}
func nwc_error(zapcache: Zaps, evcache: EventCache, resp: FullWalletResponse) {
// find a pending zap with the nwc request id associated with this response and remove it
for kv in zapcache.our_zaps {

View File

@@ -41,16 +41,7 @@ public enum ZapTarget: Equatable {
struct ZapRequest {
let ev: NostrEvent
let marked_hidden: Bool
var is_in_thread: Bool {
return !self.ev.content.isEmpty && !marked_hidden
}
init(ev: NostrEvent) {
self.ev = ev
self.marked_hidden = ev.tags.first(where: { t in t.count > 0 && t[0] == "hidden" }) != nil
}
}
enum ExtPendingZapStateType {
@@ -138,7 +129,7 @@ struct ZapRequestId: Equatable {
let reqid: String
init(from_zap: Zapping) {
self.reqid = from_zap.request.ev.id
self.reqid = from_zap.request.id
}
init(from_makezap: MakeZapRequest) {
@@ -207,12 +198,12 @@ enum Zapping {
}
}
var request: ZapRequest {
var request: NostrEvent {
switch self {
case .zap(let zap):
return zap.request
return zap.request_ev
case .pending(let pzap):
return pzap.request
return pzap.request.ev
}
}
@@ -236,15 +227,6 @@ enum Zapping {
}
}
var is_in_thread: Bool {
switch self {
case .zap(let zap):
return zap.request.is_in_thread
case .pending(let pzap):
return pzap.request.is_in_thread
}
}
var is_anon: Bool {
switch self {
case .zap(let zap):
@@ -260,12 +242,12 @@ struct Zap {
public let invoice: ZapInvoice
public let zapper: String /// zap authorizer
public let target: ZapTarget
public let raw_request: ZapRequest
public let request: ZapRequest
public let is_anon: Bool
public let private_request: ZapRequest?
public let private_request: NostrEvent?
var request: ZapRequest {
return private_request ?? self.raw_request
var request_ev: NostrEvent {
return private_request ?? self.request.ev
}
public static func from_zap_event(zap_ev: NostrEvent, zapper: String, our_privkey: String?) -> Zap? {
@@ -313,9 +295,8 @@ struct Zap {
}
let is_anon = private_request == nil && event_is_anonymous(ev: zap_req)
let preq = private_request.map { pr in ZapRequest(ev: pr) }
return Zap(event: zap_ev, invoice: zap_invoice, zapper: zapper, target: target, raw_request: ZapRequest(ev: zap_req), is_anon: is_anon, private_request: preq)
return Zap(event: zap_ev, invoice: zap_invoice, zapper: zapper, target: target, request: ZapRequest(ev: zap_req), is_anon: is_anon, private_request: private_request)
}
}
@@ -459,14 +440,15 @@ func fetch_static_payreq(_ lnurl: String) async -> LNUrlPayRequest? {
return endpoint
}
func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent?, msats: Int64, zap_type: ZapType, comment: String?) async -> String? {
func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int, zap_type: ZapType, comment: String?) async -> String? {
guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else {
return nil
}
let zappable = payreq.allowsNostr ?? false
let amount: Int64 = Int64(sats) * 1000
var query = [URLQueryItem(name: "amount", value: "\(msats)")]
var query = [URLQueryItem(name: "amount", value: "\(amount)")]
if zappable && zap_type != .non_zap, let json = encode_json(zapreq) {
print("zapreq json: \(json)")
@@ -507,7 +489,7 @@ func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent?, msats: In
// make sure it's the correct amount
guard let bolt11 = decode_bolt11(result.pr),
.specific(msats) == bolt11.amount
.specific(amount) == bolt11.amount
else {
return nil
}

View File

@@ -12,8 +12,8 @@ class Zaps {
let our_pubkey: String
var our_zaps: [String: [Zapping]]
private(set) var event_counts: [String: Int]
private(set) var event_totals: [String: Int64]
var event_counts: [String: Int]
var event_totals: [String: Int64]
init(our_pubkey: String) {
self.zaps = [:]
@@ -22,18 +22,18 @@ class Zaps {
self.event_counts = [:]
self.event_totals = [:]
}
func remove_zap(reqid: String) -> Zapping? {
var res: Zapping? = nil
for kv in our_zaps {
let ours = kv.value
guard let zap = ours.first(where: { z in z.request.ev.id == reqid }) else {
guard let zap = ours.first(where: { z in z.request.id == reqid }) else {
continue
}
res = zap
our_zaps[kv.key] = ours.filter { z in z.request.ev.id != reqid }
our_zaps[kv.key] = ours.filter { z in z.request.id != reqid }
if let count = event_counts[zap.target.id] {
event_counts[zap.target.id] = count - 1
@@ -51,16 +51,13 @@ class Zaps {
}
func add_zap(zap: Zapping) {
if zaps[zap.request.ev.id] != nil {
if zaps[zap.request.id] != nil {
return
}
self.zaps[zap.request.ev.id] = zap
if let zap_id = zap.event?.id {
self.zaps[zap_id] = zap
}
self.zaps[zap.request.id] = zap
// record our zaps for an event
if zap.request.ev.pubkey == our_pubkey {
if zap.request.pubkey == our_pubkey {
switch zap.target {
case .note(let note_target):
if our_zaps[note_target.note_id] == nil {
@@ -74,7 +71,7 @@ class Zaps {
}
// don't count tips to self. lame.
guard zap.request.ev.pubkey != zap.target.pubkey else {
guard zap.request.pubkey != zap.target.pubkey else {
return
}

View File

@@ -43,7 +43,7 @@ struct EventActionBar: View {
HStack {
if damus_state.keypair.privkey != nil {
HStack(spacing: 4) {
EventActionButton(img: "bubble2", col: bar.replied ? DamusColors.purple : Color.gray) {
EventActionButton(img: "bubble.left", col: bar.replied ? DamusColors.purple : Color.gray) {
notify(.compose, PostAction.replying_to(event))
}
.accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button"))
@@ -55,14 +55,14 @@ struct EventActionBar: View {
Spacer()
HStack(spacing: 4) {
EventActionButton(img: "repost", col: bar.boosted ? Color.green : nil) {
EventActionButton(img: "arrow.2.squarepath", col: bar.boosted ? Color.green : nil) {
if bar.boosted {
notify(.delete, bar.our_boost)
} else {
self.show_repost_action = true
}
}
.accessibilityLabel(NSLocalizedString("Reposts", comment: "Accessibility label for boosts button"))
.accessibilityLabel(NSLocalizedString("Boosts", comment: "Accessibility label for boosts button"))
Text(verbatim: "\(bar.boosts > 0 ? "\(bar.boosts)" : "")")
.font(.footnote.weight(.medium))
.foregroundColor(bar.boosted ? Color.green : Color.gray)
@@ -88,14 +88,14 @@ struct EventActionBar: View {
if let lnurl = self.lnurl {
Spacer()
ZapButton(damus_state: damus_state, target: ZapTarget.note(id: event.id, author: event.pubkey), lnurl: lnurl, zaps: self.damus_state.events.get_cache_data(self.event.id).zaps_model)
ZapButton(damus_state: damus_state, event: event, lnurl: lnurl, zaps: self.damus_state.events.get_cache_data(self.event.id).zaps_model)
}
Spacer()
EventActionButton(img: "upload", col: Color.gray) {
EventActionButton(img: "square.and.arrow.up", col: Color.gray) {
show_share_action = true
}
.accessibilityLabel(NSLocalizedString("Share", comment: "Button to share a note"))
.accessibilityLabel(NSLocalizedString("Share", comment: "Button to share a post"))
}
.onAppear {
self.bar.update(damus: damus_state, evid: self.event.id)
@@ -161,12 +161,9 @@ struct EventActionBar: View {
func EventActionButton(img: String, col: Color?, action: @escaping () -> ()) -> some View {
Button(action: action) {
Image(img)
.resizable()
Image(systemName: img)
.foregroundColor(col == nil ? Color.gray : col!)
.font(.footnote.weight(.medium))
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
}
}
@@ -191,16 +188,11 @@ struct LikeButton: View {
}) {
if liked {
LINEAR_GRADIENT
.mask(Image("shaka.fill")
.mask(Image("shaka-full")
.resizable()
.aspectRatio(contentMode: .fit)
)
.frame(width: 20, height: 20)
).frame(width: 14, height: 14)
} else {
Image("shaka")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
Image("shaka-line")
.foregroundColor(.gray)
}
}

View File

@@ -25,7 +25,7 @@ struct EventDetailBar: View {
var body: some View {
HStack {
if bar.boosts > 0 {
NavigationLink(value: Route.Reposts(reposts: RepostsModel(state: state, target: target))) {
NavigationLink(destination: RepostsView(damus_state: state, model: RepostsModel(state: state, target: target))) {
let noun = Text(verbatim: repostsCountString(bar.boosts)).foregroundColor(.gray)
Text("\(Text(verbatim: bar.boosts.formatted()).font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.")
}
@@ -33,7 +33,7 @@ struct EventDetailBar: View {
}
if bar.likes > 0 && !state.settings.onlyzaps_mode {
NavigationLink(value: Route.Reactions(reactions: ReactionsModel(state: state, target: target))) {
NavigationLink(destination: ReactionsView(damus_state: state, model: ReactionsModel(state: state, target: target))) {
let noun = Text(verbatim: reactionsCountString(bar.likes)).foregroundColor(.gray)
Text("\(Text(verbatim: bar.likes.formatted()).font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.")
}
@@ -41,7 +41,8 @@ struct EventDetailBar: View {
}
if bar.zaps > 0 {
NavigationLink(value: Route.Zaps(target: .note(id: target, author: target_pk))) {
let dst = ZapsView(state: state, target: .note(id: target, author: target_pk))
NavigationLink(destination: dst) {
let noun = Text(verbatim: zapsCountString(bar.zaps)).foregroundColor(.gray)
Text("\(Text(verbatim: bar.zaps.formatted()).font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.")
}

View File

@@ -28,7 +28,7 @@ struct RepostAction: View {
damus_state.postbox.send(boost)
} label: {
Label(NSLocalizedString("Repost", comment: "Button to repost a note"), image: "repost")
Label(NSLocalizedString("Repost", comment: "Button to repost a note"), systemImage: "arrow.2.squarepath")
.frame(maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .leading)
}

View File

@@ -41,7 +41,7 @@ struct ShareAction: View {
UIPasteboard.general.string = "https://damus.io/" + (bech32_note_id(event.id) ?? event.id)
}
let bookmarkImg = isBookmarked ? "bookmark.fill" : "bookmark"
let bookmarkImg = isBookmarked ? "bookmark.slash" : "bookmark"
let bookmarkTxt = isBookmarked ? NSLocalizedString("Remove Bookmark", comment: "Button text to remove bookmark from a note.") : NSLocalizedString("Add Bookmark", comment: "Button text to add bookmark to a note.")
ShareActionButton(img: bookmarkImg, text: bookmarkTxt) {
dismiss()
@@ -54,7 +54,7 @@ struct ShareAction: View {
NotificationCenter.default.post(name: .broadcast_event, object: event)
}
ShareActionButton(img: "upload", text: NSLocalizedString("Share Via...", comment: "Button to present iOS share sheet")) {
ShareActionButton(img: "square.and.arrow.up", text: NSLocalizedString("Share Via...", comment: "Button to present iOS share sheet")) {
show_share = true
dismiss()
}

View File

@@ -27,7 +27,7 @@ struct ShareActionButton: View {
var body: some View {
Button(action: action) {
VStack() {
Image(img)
Image(systemName: img)
.foregroundColor(col)
.font(.system(size: 23, weight: .bold))
.overlay {
@@ -48,6 +48,6 @@ struct ShareActionButton: View {
struct ShareActionButton_Previews: PreviewProvider {
static var previews: some View {
ShareActionButton(img: "link", text: "Stretch", action: {})
ShareActionButton(img: "figure.flexibility", text: "Stretch", action: {})
}
}

View File

@@ -19,7 +19,7 @@ struct AddRelayView: View {
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
Label("", image: "close-circle")
Label("", systemImage: "xmark.circle.fill")
.foregroundColor(.accentColor)
.padding(.trailing, -25.0)
.opacity((relay == "") ? 0.0 : 1.0)
@@ -28,7 +28,7 @@ struct AddRelayView: View {
}
}
Label("", image: "copy2")
Label("", systemImage: "doc.on.clipboard")
.padding(.leading, -10)
.onTapGesture {
if let pastedrelay = UIPasteboard.general.string {

View File

@@ -29,7 +29,7 @@ struct BookmarksView: View {
Group {
if bookmarks.isEmpty {
VStack {
Image("bookmark")
Image(systemName: "bookmark")
.resizable()
.scaledToFit()
.frame(width: 32.0, height: 32.0)
@@ -38,6 +38,7 @@ struct BookmarksView: View {
} else {
ScrollView {
InnerTimelineView(events: EventHolder(events: bookmarks, incoming: []), damus: state, filter: noneFilter)
}
}
}

View File

@@ -23,7 +23,7 @@ struct AlbyButton: View {
HStack {
Image("alby")
Text("Attach 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.")
Text("Connect to Alby")
}
.offset(x: -25)
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .center)

View File

@@ -21,13 +21,13 @@ struct FriendsButton: View {
}) {
if filter == .friends {
LINEAR_GRADIENT
.mask(Image("user-added")
.mask(Image(systemName: "person.2.fill")
.resizable()
).frame(width: 28, height: 28)
).frame(width: 30, height: 20)
} else {
Image("user-added")
Image(systemName: "person.2.fill")
.resizable()
.frame(width: 28, height: 28)
.frame(width: 30, height: 20)
.foregroundColor(DamusColors.adaptableGrey)
}
}

View File

@@ -21,7 +21,7 @@ let carousel_items = [
CarouselItem(image: Image("undercover"),
text: Text("\(Text("Private", comment: "Heading indicating that this application keeps personally identifiable information private. A sentence describing what is done to keep data private comes after this heading.").bold()). Creating an account doesn't require a phone number, email or name. Get started right away with zero friction.", comment: "Explanation of what is done to keep personally identifiable information private. There is a heading that precedes this explanation which is a variable to this string.")),
CarouselItem(image: Image("bitcoin-p2p"),
text: Text("\(Text("Earn Money", comment: "Heading indicating that this application allows users to earn money.").bold()). Tip your friends and stack sats with Bitcoin⚡, the native currency of the internet.", comment: "Explanation of what can be done by users to earn money. There is a heading that precedes this explanation which is a variable to this string."))
text: Text("\(Text("Earn Money", comment: "Heading indicating that this application allows users to earn money.").bold()). Tip your friend's posts and stack sats with Bitcoin⚡, the native currency of the internet.", comment: "Explanation of what can be done by users to earn money. There is a heading that precedes this explanation which is a variable to this string."))
]
struct CarouselView: View {

Some files were not shown because too many files have changed in this diff Show More