Compare commits
100 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
1110ffa8af
|
|||
| 84cfeb1604 | |||
| 4c37bfc128 | |||
| 548af2bf9d | |||
| 692146fe00 | |||
| 40134b4365 | |||
| 9a547077c1 | |||
| 0d71cc18ad | |||
| d10554ab6c | |||
| 2656c30832 | |||
| 39b6dfb47e | |||
| 5ca5420ce2 | |||
| 4703ed80a7 | |||
| f7e407e030 | |||
| e547e26d99 | |||
| f6044a9eea | |||
| 26bd50c948 | |||
| 6e0af0ba10 | |||
| 44b7ae2054 | |||
| 9cc21fc860 | |||
| c9526b7aa6 | |||
| 055b7af1a3 | |||
| de0b1dbda2 | |||
| 7605af84b5 | |||
| d45eadef35 | |||
| 9cae934062 | |||
| 7fb5cdf6c0 | |||
| 49bbe62d2a | |||
| 705accd309 | |||
| 3375ccc4fa | |||
| a9fecc3047 | |||
| 31b3ad9825 | |||
| 2bde3a9217 | |||
| 6050116314 | |||
| a2cac142c0 | |||
| 8a20e5845e | |||
| 641e2564fb | |||
| 50810033c0 | |||
| fab4e231b6 | |||
| 42ff49a803 | |||
| 8c878cbc4c | |||
| face4268bf | |||
| fed6c47835 | |||
| 8e9fb308f9 | |||
| 89b48db92d | |||
| 9581cc994d | |||
| 34e32bc930 | |||
| dfcef0ba95 | |||
| 3c11ba53ce | |||
| 9759787c95 | |||
| eef428ce4f | |||
| d69647e071 | |||
| c22f5e90a3 | |||
| f2fe02032e | |||
| da2bdad18d | |||
| c7cc8df5ba | |||
| 92df446d72 | |||
| 7ea2af6172 | |||
| 184eea6e68 | |||
| 82372d1bf5 | |||
| 39f59eb798 | |||
| 639deec1a2 | |||
| 18780002bb | |||
| 722180bb9a | |||
| 366a584934 | |||
| 9ee09c3b59 | |||
| e8caf3a7f4 | |||
| b4ff6ee614 | |||
| ed652db3d3 | |||
| 3d01c29148 | |||
| 1c1bb599ed | |||
| 25e6c77d9b | |||
| 5aae81c47d | |||
| 6b2fd4cec1 | |||
| d486af6704 | |||
| 323f920848 | |||
| c58c200acb | |||
| c3786bf849 | |||
| a0e882db64 | |||
| eedf734dae | |||
| cfa06797b7 | |||
| 824279742c | |||
| 2cdbadd09d | |||
| 1ea70c8427 | |||
| 09876c06d0 | |||
| 7a063f8aa0 | |||
| b8ac026a3d | |||
| c18853c957 | |||
| 0a09dbfe1c | |||
| 78e840734a | |||
| 1ccb300dd1 | |||
| 049a32db41 | |||
| d82add1080 | |||
|
9d42715d76
|
|||
|
14fd06c052
|
|||
|
e2ab3a41b4
|
|||
|
6d7c2af504
|
|||
|
f5fbd1d3c1
|
|||
|
c4333280dd
|
|||
|
6b6a98b71f
|
@@ -4,3 +4,4 @@ Suhail Saqan <suhail.saqan@gmail.com> <43693074+suhailsaqan@users.noreply.github
|
||||
cr0bar <cr0bar@cr0.bar> <cr0bar@users.noreply.github.com>
|
||||
Swift <scoder1747@gmail.com> <120697811+scoder1747@users.noreply.github.com>
|
||||
Daniel D'Aquino <daniel@daquino.me> <patches@damus.io>
|
||||
Transifex <transifex@transifex.com> <43880903+transifex-integration[bot]@users.noreply.github.com>
|
||||
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"identifier" : "64C21A2D",
|
||||
"nonRenewingSubscriptions" : [
|
||||
|
||||
],
|
||||
"products" : [
|
||||
|
||||
],
|
||||
"settings" : {
|
||||
"_applicationInternalID" : "1628663131",
|
||||
"_developerTeamID" : "XK7H4JAB3D",
|
||||
"_failTransactionsEnabled" : false,
|
||||
"_lastSynchronizedDate" : 704848066.26849198,
|
||||
"_locale" : "en_US",
|
||||
"_storefront" : "USA",
|
||||
"_storeKitErrors" : [
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "Load Products"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "Purchase"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "Verification"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "App Store Sync"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "Subscription Status"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "App Transaction"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "Manage Subscriptions Sheet"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "Refund Request Sheet"
|
||||
},
|
||||
{
|
||||
"current" : null,
|
||||
"enabled" : false,
|
||||
"name" : "Offer Code Redeem Sheet"
|
||||
}
|
||||
]
|
||||
},
|
||||
"subscriptionGroups" : [
|
||||
{
|
||||
"id" : "21283177",
|
||||
"localizations" : [
|
||||
|
||||
],
|
||||
"name" : "Purple",
|
||||
"subscriptions" : [
|
||||
{
|
||||
"adHocOffers" : [
|
||||
|
||||
],
|
||||
"codeOffers" : [
|
||||
|
||||
],
|
||||
"displayPrice" : "6.99",
|
||||
"familyShareable" : false,
|
||||
"groupNumber" : 1,
|
||||
"internalID" : "6446591615",
|
||||
"introductoryOffer" : null,
|
||||
"localizations" : [
|
||||
{
|
||||
"description" : "Support damus development with Damus Purple!",
|
||||
"displayName" : "Damus Purple",
|
||||
"locale" : "en_CA"
|
||||
}
|
||||
],
|
||||
"productID" : "purple",
|
||||
"recurringSubscriptionPeriod" : "P1M",
|
||||
"referenceName" : "Purple",
|
||||
"subscriptionGroupID" : "21283177",
|
||||
"type" : "RecurringSubscription"
|
||||
},
|
||||
{
|
||||
"adHocOffers" : [
|
||||
|
||||
],
|
||||
"codeOffers" : [
|
||||
|
||||
],
|
||||
"displayPrice" : "69.99",
|
||||
"familyShareable" : false,
|
||||
"groupNumber" : 2,
|
||||
"internalID" : "6448764101",
|
||||
"introductoryOffer" : null,
|
||||
"localizations" : [
|
||||
|
||||
],
|
||||
"productID" : "purpleyearly",
|
||||
"recurringSubscriptionPeriod" : "P1Y",
|
||||
"referenceName" : "Purple Yearly",
|
||||
"subscriptionGroupID" : "21283177",
|
||||
"type" : "RecurringSubscription"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"version" : {
|
||||
"major" : 3,
|
||||
"minor" : 0
|
||||
}
|
||||
}
|
||||
@@ -16,12 +16,14 @@ damus implements the following [Nostr Implementation Possibilities][nips]
|
||||
- [NIP-08: Mentions][nip08]
|
||||
- [NIP-10: Reply conventions][nip10]
|
||||
- [NIP-12: Generic tag queries (hashtags)][nip12]
|
||||
- [NIP-42: Authentication of clients to relays][nip42]
|
||||
|
||||
[nips]: https://github.com/nostr-protocol/nips
|
||||
[nip01]: https://github.com/nostr-protocol/nips/blob/master/01.md
|
||||
[nip08]: https://github.com/nostr-protocol/nips/blob/master/08.md
|
||||
[nip10]: https://github.com/nostr-protocol/nips/blob/master/10.md
|
||||
[nip12]: https://github.com/nostr-protocol/nips/blob/master/12.md
|
||||
[nip42]: https://github.com/nostr-protocol/nips/blob/master/42.md
|
||||
|
||||
## Getting Started on Damus
|
||||
|
||||
|
||||
+45
-2
@@ -485,11 +485,37 @@ static inline int parse_str(struct cursor *cur, const char *str) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static inline int is_whitespace(char c) {
|
||||
static inline int is_whitespace(int c) {
|
||||
return c == ' ' || c == '\t' || c == '\n' || c == '\v' || c == '\f' || c == '\r';
|
||||
}
|
||||
|
||||
static inline int is_underscore(char c) {
|
||||
|
||||
static inline int next_char_is_whitespace(unsigned char *curChar, unsigned char *endChar) {
|
||||
unsigned char * next = curChar + 1;
|
||||
if(next > endChar) return 0;
|
||||
else if(next == endChar) return 1;
|
||||
return is_whitespace(*next);
|
||||
}
|
||||
|
||||
static int char_disallowed_at_end_url(char c){
|
||||
return c == '.' || c == ',';
|
||||
}
|
||||
|
||||
static inline int is_final_url_char(unsigned char *curChar, unsigned char *endChar){
|
||||
if(is_whitespace(*curChar)){
|
||||
return 1;
|
||||
}
|
||||
else if(next_char_is_whitespace(curChar, endChar)) {
|
||||
// next char is whitespace so this char could be the final char in the url
|
||||
return char_disallowed_at_end_url(*curChar);
|
||||
}
|
||||
else{
|
||||
// next char isn't whitespace so it can't be a final char
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static inline int is_underscore(int c) {
|
||||
return c == '_';
|
||||
}
|
||||
|
||||
@@ -670,6 +696,23 @@ static inline int consume_until_whitespace(struct cursor *cur, int or_end) {
|
||||
return or_end;
|
||||
}
|
||||
|
||||
static inline int consume_until_end_url(struct cursor *cur, int or_end) {
|
||||
char c;
|
||||
int consumedAtLeastOne = 0;
|
||||
|
||||
while (cur->p < cur->end) {
|
||||
c = *cur->p;
|
||||
|
||||
if (is_final_url_char(cur->p, cur->end))
|
||||
return consumedAtLeastOne;
|
||||
|
||||
cur->p++;
|
||||
consumedAtLeastOne = 1;
|
||||
}
|
||||
|
||||
return or_end;
|
||||
}
|
||||
|
||||
static inline int consume_until_non_alphanumeric(struct cursor *cur, int or_end) {
|
||||
char c;
|
||||
int consumedAtLeastOne = 0;
|
||||
|
||||
+3
-3
@@ -117,7 +117,7 @@ static int consume_url_fragment(struct cursor *cur)
|
||||
|
||||
cur->p++;
|
||||
|
||||
return consume_until_whitespace(cur, 1);
|
||||
return consume_until_end_url(cur, 1);
|
||||
}
|
||||
|
||||
static int consume_url_path(struct cursor *cur)
|
||||
@@ -134,7 +134,7 @@ static int consume_url_path(struct cursor *cur)
|
||||
while (cur->p < cur->end) {
|
||||
c = *cur->p;
|
||||
|
||||
if (c == '?' || c == '#' || is_whitespace(c)) {
|
||||
if (c == '?' || c == '#' || is_final_url_char(cur->p, cur->end)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ static int consume_url_host(struct cursor *cur)
|
||||
while (cur->p < cur->end) {
|
||||
c = *cur->p;
|
||||
// TODO: handle IDNs
|
||||
if (is_alphanumeric(c) || c == '.' || c == '-')
|
||||
if ((is_alphanumeric(c) || c == '.' || c == '-') && !is_final_url_char(cur->p, cur->end))
|
||||
{
|
||||
count++;
|
||||
cur->p++;
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; };
|
||||
3A90B1812A4EA3AF00000D94 /* UserSearchCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A90B1802A4EA3AF00000D94 /* UserSearchCache.swift */; };
|
||||
3A90B1832A4EA3C600000D94 /* UserSearchCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A90B1822A4EA3C600000D94 /* UserSearchCacheTests.swift */; };
|
||||
3A9ADA302B4CB5F400756AA0 /* TranslatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9ADA2F2B4CB5F400756AA0 /* TranslatorTests.swift */; };
|
||||
3A9ADA322B4CBFD000756AA0 /* BlocksExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A9ADA312B4CBFD000756AA0 /* BlocksExtensionTests.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 */; };
|
||||
@@ -260,6 +262,7 @@
|
||||
4C9B0DF32A65C46800CBDA21 /* ProfileEditButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9B0DF22A65C46800CBDA21 /* ProfileEditButton.swift */; };
|
||||
4C9BB83129C0ED4F00FC4E37 /* DisplayName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */; };
|
||||
4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9BB83329C12D9900FC4E37 /* EventProfileName.swift */; };
|
||||
4C9D6D162B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9D6D152B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift */; };
|
||||
4C9D6D1B2B1D35D7004E5CD9 /* PullDownSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9D6D1A2B1D35D7004E5CD9 /* PullDownSearch.swift */; };
|
||||
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */; };
|
||||
4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */; };
|
||||
@@ -374,6 +377,7 @@
|
||||
4CF0ABF62985CD5500D66079 /* UserSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF0ABF52985CD5500D66079 /* UserSearch.swift */; };
|
||||
4CF38C882A9442DC00BE01B6 /* UserStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CF38C872A9442DC00BE01B6 /* UserStatusView.swift */; };
|
||||
4CFD502F2A2DA45800A229DB /* MediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFD502E2A2DA45800A229DB /* MediaView.swift */; };
|
||||
4CFF8F5929C9FD1E008DB934 /* DamusPurpleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F5829C9FD1E008DB934 /* DamusPurpleView.swift */; };
|
||||
4CFF8F6329CC9AD7008DB934 /* ImageContextMenuModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */; };
|
||||
4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6629CC9E3A008DB934 /* ImageView.swift */; };
|
||||
4CFF8F6929CC9ED1008DB934 /* ImageContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */; };
|
||||
@@ -418,6 +422,11 @@
|
||||
9C83F89329A937B900136C08 /* TextViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C83F89229A937B900136C08 /* TextViewWrapper.swift */; };
|
||||
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; };
|
||||
ADFE73552AD4793100EC7326 /* QRScanNSECView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADFE73542AD4793100EC7326 /* QRScanNSECView.swift */; };
|
||||
B501062D2B363036003874F5 /* AuthIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B501062C2B363036003874F5 /* AuthIntegrationTests.swift */; };
|
||||
B57B4C622B312BD700A232C0 /* ReconnectRelaysNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57B4C612B312BD700A232C0 /* ReconnectRelaysNotify.swift */; };
|
||||
B57B4C642B312BFA00A232C0 /* RelayAuthenticationDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57B4C632B312BFA00A232C0 /* RelayAuthenticationDetail.swift */; };
|
||||
B57B4C662B312C3700A232C0 /* NostrAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57B4C652B312C3700A232C0 /* NostrAuth.swift */; };
|
||||
B5B4D1432B37D47600844320 /* NdbExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B4D1422B37D47600844320 /* NdbExtensions.swift */; };
|
||||
BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; };
|
||||
BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; };
|
||||
BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; };
|
||||
@@ -439,6 +448,9 @@
|
||||
D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */; };
|
||||
D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; };
|
||||
D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; };
|
||||
D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F43092B23F0BE00425B75 /* DamusPurple.swift */; };
|
||||
D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74F430B2B23FB9B00425B75 /* StoreObserver.swift */; };
|
||||
D76556D62B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */; };
|
||||
D76874F32AE3632B00FB0F68 /* ProfileZapLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */; };
|
||||
D77BFA0B2AE3051200621634 /* ProfileActionSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */; };
|
||||
D783A63F2AD4E53D00658DDA /* SuggestedHashtagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */; };
|
||||
@@ -467,6 +479,7 @@
|
||||
D79C4C1B2AFEB061003A41B4 /* DamusNotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D79C4C142AFEB061003A41B4 /* DamusNotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
D7A343EE2AD0D77C00CED48B /* InlineSnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D7A343ED2AD0D77C00CED48B /* InlineSnapshotTesting */; };
|
||||
D7A343F02AD0D77C00CED48B /* SnapshotTesting in Frameworks */ = {isa = PBXBuildFile; productRef = D7A343EF2AD0D77C00CED48B /* SnapshotTesting */; };
|
||||
D7C6787E2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7C6787D2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift */; };
|
||||
D7CCFC072B05833200323D86 /* NdbNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90548A2A6AEDEE00811EEC /* NdbNote.swift */; };
|
||||
D7CCFC082B05834500323D86 /* NoteId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC14FF42A740BB7007AEB17 /* NoteId.swift */; };
|
||||
D7CCFC0B2B0585EA00323D86 /* nostrdb.c in Sources */ = {isa = PBXBuildFile; fileRef = 4CE9FBB82A6B3B26007E485C /* nostrdb.c */; settings = {COMPILER_FLAGS = "-w"; }; };
|
||||
@@ -658,6 +671,8 @@
|
||||
3A96D41A298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
3A96D41B298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
3A96D41C298DA94500388A2A /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = nl; path = nl.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||
3A9ADA2F2B4CB5F400756AA0 /* TranslatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranslatorTests.swift; sourceTree = "<group>"; };
|
||||
3A9ADA312B4CBFD000756AA0 /* BlocksExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlocksExtensionTests.swift; sourceTree = "<group>"; };
|
||||
3AA247FC297E3CFF0090C62D /* RepostsModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepostsModel.swift; sourceTree = "<group>"; };
|
||||
3AA247FE297E3D900090C62D /* RepostsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostsView.swift; sourceTree = "<group>"; };
|
||||
3AA24801297E3DC20090C62D /* RepostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RepostView.swift; sourceTree = "<group>"; };
|
||||
@@ -1033,6 +1048,7 @@
|
||||
4C8682862814DE470026224F /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
|
||||
4C86F7C32A76C44C00EC0817 /* ZappingNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZappingNotify.swift; sourceTree = "<group>"; };
|
||||
4C86F7C52A76C51100EC0817 /* AttachedWalletNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachedWalletNotify.swift; sourceTree = "<group>"; };
|
||||
4C8AE1182A0320BE00B944E6 /* Purple.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Purple.storekit; sourceTree = "<group>"; };
|
||||
4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompatibleAttribute.swift; sourceTree = "<group>"; };
|
||||
4C8D00C929DF80350036AF10 /* TruncatedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TruncatedText.swift; sourceTree = "<group>"; };
|
||||
4C8D00CB29DF92DF0036AF10 /* Hashtags.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hashtags.swift; sourceTree = "<group>"; };
|
||||
@@ -1058,6 +1074,7 @@
|
||||
4C9B0DF22A65C46800CBDA21 /* ProfileEditButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileEditButton.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>"; };
|
||||
4C9D6D152B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayTabBarNotify.swift; sourceTree = "<group>"; };
|
||||
4C9D6D1A2B1D35D7004E5CD9 /* PullDownSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullDownSearch.swift; sourceTree = "<group>"; };
|
||||
4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = "<group>"; };
|
||||
4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeAnonPfpView.swift; sourceTree = "<group>"; };
|
||||
@@ -1184,6 +1201,7 @@
|
||||
4CF0ABF52985CD5500D66079 /* UserSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSearch.swift; sourceTree = "<group>"; };
|
||||
4CF38C872A9442DC00BE01B6 /* UserStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserStatusView.swift; sourceTree = "<group>"; };
|
||||
4CFD502E2A2DA45800A229DB /* MediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaView.swift; sourceTree = "<group>"; };
|
||||
4CFF8F5829C9FD1E008DB934 /* DamusPurpleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleView.swift; sourceTree = "<group>"; };
|
||||
4CFF8F6229CC9AD7008DB934 /* ImageContextMenuModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContextMenuModifier.swift; sourceTree = "<group>"; };
|
||||
4CFF8F6629CC9E3A008DB934 /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = "<group>"; };
|
||||
4CFF8F6829CC9ED1008DB934 /* ImageContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageContainerView.swift; sourceTree = "<group>"; };
|
||||
@@ -1228,6 +1246,11 @@
|
||||
9C83F89229A937B900136C08 /* TextViewWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextViewWrapper.swift; sourceTree = "<group>"; };
|
||||
9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachMediaUtility.swift; sourceTree = "<group>"; };
|
||||
ADFE73542AD4793100EC7326 /* QRScanNSECView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRScanNSECView.swift; sourceTree = "<group>"; };
|
||||
B501062C2B363036003874F5 /* AuthIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthIntegrationTests.swift; sourceTree = "<group>"; usesTabs = 0; };
|
||||
B57B4C612B312BD700A232C0 /* ReconnectRelaysNotify.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReconnectRelaysNotify.swift; sourceTree = "<group>"; };
|
||||
B57B4C632B312BFA00A232C0 /* RelayAuthenticationDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelayAuthenticationDetail.swift; sourceTree = "<group>"; };
|
||||
B57B4C652B312C3700A232C0 /* NostrAuth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NostrAuth.swift; sourceTree = "<group>"; };
|
||||
B5B4D1422B37D47600844320 /* NdbExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NdbExtensions.swift; sourceTree = "<group>"; usesTabs = 0; };
|
||||
BA3759892ABCCDE30018D73B /* ImageResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageResizer.swift; sourceTree = "<group>"; };
|
||||
BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureProcessor.swift; sourceTree = "<group>"; };
|
||||
BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCaptureProcessor.swift; sourceTree = "<group>"; };
|
||||
@@ -1249,6 +1272,9 @@
|
||||
D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProfiles.swift; sourceTree = "<group>"; };
|
||||
D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = "<group>"; };
|
||||
D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = "<group>"; };
|
||||
D74F43092B23F0BE00425B75 /* DamusPurple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurple.swift; sourceTree = "<group>"; };
|
||||
D74F430B2B23FB9B00425B75 /* StoreObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreObserver.swift; sourceTree = "<group>"; };
|
||||
D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusPurpleWelcomeView.swift; sourceTree = "<group>"; };
|
||||
D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZapLinkView.swift; sourceTree = "<group>"; };
|
||||
D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileActionSheetView.swift; sourceTree = "<group>"; };
|
||||
D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedHashtagsView.swift; sourceTree = "<group>"; };
|
||||
@@ -1262,6 +1288,7 @@
|
||||
D79C4C162AFEB061003A41B4 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||
D79C4C182AFEB061003A41B4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
D79C4C1C2AFEB061003A41B4 /* DamusNotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DamusNotificationService.entitlements; sourceTree = "<group>"; };
|
||||
D7C6787D2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP98AuthenticatedRequest.swift; sourceTree = "<group>"; };
|
||||
D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = "<group>"; };
|
||||
D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayURL.swift; sourceTree = "<group>"; };
|
||||
E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = "<group>"; };
|
||||
@@ -1410,6 +1437,7 @@
|
||||
4C0A3F8D280F63FF000448DE /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D74F43082B23F09300425B75 /* Purple */,
|
||||
BA3759882ABCCDE30018D73B /* Camera */,
|
||||
4C190F1E2A535FC200027FD5 /* Zaps */,
|
||||
4C54AA0829A55416003E4487 /* Notifications */,
|
||||
@@ -1799,6 +1827,7 @@
|
||||
4C1A9A2829DDF53B00516EAC /* Video */,
|
||||
4C1A9A1B29DDCF8B00516EAC /* Settings */,
|
||||
4CFF8F6129CC9A80008DB934 /* Images */,
|
||||
4CFF8F5729C9FD07008DB934 /* Purple */,
|
||||
4CCEB7AC29B53D180078AA28 /* Search */,
|
||||
4C30AC7029A5676F00E2BD5A /* Notifications */,
|
||||
4CE0E2B029A3DF4700DB4CA2 /* Timeline */,
|
||||
@@ -1883,6 +1912,8 @@
|
||||
4C2B7BF12A71B6540049DEE7 /* Id.swift */,
|
||||
D7FF93FF2AC7AC5200FD969D /* RelayURL.swift */,
|
||||
D798D22B2B086C7400234419 /* NostrEvent+.swift */,
|
||||
D7C6787D2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift */,
|
||||
B57B4C652B312C3700A232C0 /* NostrAuth.swift */,
|
||||
);
|
||||
path = Nostr;
|
||||
sourceTree = "<group>";
|
||||
@@ -2024,6 +2055,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C9B0DED2A65A75F00CBDA21 /* AttrStringTestExtensions.swift */,
|
||||
B5B4D1422B37D47600844320 /* NdbExtensions.swift */,
|
||||
3A9ADA2F2B4CB5F400756AA0 /* TranslatorTests.swift */,
|
||||
);
|
||||
path = Util;
|
||||
sourceTree = "<group>";
|
||||
@@ -2032,6 +2065,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C86F7C52A76C51100EC0817 /* AttachedWalletNotify.swift */,
|
||||
4C9D6D152B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift */,
|
||||
4C1253552A76C8C60004F4B8 /* BroadcastNotify.swift */,
|
||||
4C1253512A76C6130004F4B8 /* ComposeNotify.swift */,
|
||||
4CA352AD2A76C1AC003BB08B /* FollowedNotify.swift */,
|
||||
@@ -2059,6 +2093,7 @@
|
||||
4C12536B2A76D4B00004F4B8 /* RepostedNotify.swift */,
|
||||
4C4E137A2A76D5FB00BDD832 /* MuteThreadNotify.swift */,
|
||||
4C4E137C2A76D63600BDD832 /* UnmuteThreadNotify.swift */,
|
||||
B57B4C612B312BD700A232C0 /* ReconnectRelaysNotify.swift */,
|
||||
);
|
||||
path = Notify;
|
||||
sourceTree = "<group>";
|
||||
@@ -2241,6 +2276,7 @@
|
||||
4C32B9362A9AD44700DC3548 /* flatbuffers */,
|
||||
4C9054862A6AEB4500811EEC /* nostrdb */,
|
||||
4C19AE4A2A5CEF7C00C90DB7 /* nostrscript */,
|
||||
4C8AE1182A0320BE00B944E6 /* Purple.storekit */,
|
||||
4C06670728FDE62900038D2A /* damus-c */,
|
||||
4CE6DEE527F7A08100C66700 /* damus */,
|
||||
4CE6DEF627F7A08200C66700 /* damusTests */,
|
||||
@@ -2335,6 +2371,8 @@
|
||||
D71DC1EB2A9129C3006E207C /* PostViewTests.swift */,
|
||||
D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */,
|
||||
D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */,
|
||||
B501062C2B363036003874F5 /* AuthIntegrationTests.swift */,
|
||||
3A9ADA312B4CBFD000756AA0 /* BlocksExtensionTests.swift */,
|
||||
);
|
||||
path = damusTests;
|
||||
sourceTree = "<group>";
|
||||
@@ -2363,6 +2401,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CE879542996BAB900F758CC /* RelayPaidDetail.swift */,
|
||||
B57B4C632B312BFA00A232C0 /* RelayAuthenticationDetail.swift */,
|
||||
);
|
||||
path = Detail;
|
||||
sourceTree = "<group>";
|
||||
@@ -2421,6 +2460,15 @@
|
||||
path = Posting;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4CFF8F5729C9FD07008DB934 /* Purple */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4CFF8F5829C9FD1E008DB934 /* DamusPurpleView.swift */,
|
||||
D76556D52B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift */,
|
||||
);
|
||||
path = Purple;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4CFF8F6129CC9A80008DB934 /* Images */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -2472,6 +2520,15 @@
|
||||
path = Mocking;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D74F43082B23F09300425B75 /* Purple */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D74F43092B23F0BE00425B75 /* DamusPurple.swift */,
|
||||
D74F430B2B23FB9B00425B75 /* StoreObserver.swift */,
|
||||
);
|
||||
path = Purple;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D79C4C152AFEB061003A41B4 /* DamusNotificationService */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -2795,6 +2852,7 @@
|
||||
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
|
||||
F757933A29D7AECD007DEAC1 /* ImagePicker.swift in Sources */,
|
||||
4CF0ABEE29844B5500D66079 /* AnyEncodable.swift in Sources */,
|
||||
B57B4C662B312C3700A232C0 /* NostrAuth.swift in Sources */,
|
||||
4CB8838D296F710400DC99E7 /* Reposted.swift in Sources */,
|
||||
4C3EA67728FF7A9800C48A62 /* talstr.c in Sources */,
|
||||
4CE6DEE927F7A08100C66700 /* ContentView.swift in Sources */,
|
||||
@@ -2848,6 +2906,7 @@
|
||||
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
|
||||
4C8D1A6F29F31E5000ACDF75 /* FriendsButton.swift in Sources */,
|
||||
3A5E47C52A4A6CF400C0D090 /* Trie.swift in Sources */,
|
||||
B57B4C642B312BFA00A232C0 /* RelayAuthenticationDetail.swift in Sources */,
|
||||
4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */,
|
||||
BA3759972ABCCF360018D73B /* CameraPreview.swift in Sources */,
|
||||
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
|
||||
@@ -2875,6 +2934,7 @@
|
||||
3AAA95CA298DF87B00F3D526 /* TranslationService.swift in Sources */,
|
||||
4CE4F9E328528C5200C00DD9 /* AddRelayView.swift in Sources */,
|
||||
BA3759922ABCCEBA0018D73B /* CameraService+Extensions.swift in Sources */,
|
||||
D74F430C2B23FB9B00425B75 /* StoreObserver.swift in Sources */,
|
||||
4C363A9A28283854006E126D /* Reply.swift in Sources */,
|
||||
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */,
|
||||
4CFF8F6729CC9E3A008DB934 /* ImageView.swift in Sources */,
|
||||
@@ -2970,9 +3030,11 @@
|
||||
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
|
||||
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
|
||||
4C12535E2A76CA870004F4B8 /* SwitchedTimelineNotify.swift in Sources */,
|
||||
D74F430A2B23F0BE00425B75 /* DamusPurple.swift in Sources */,
|
||||
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */,
|
||||
4C4E137D2A76D63600BDD832 /* UnmuteThreadNotify.swift in Sources */,
|
||||
4CE4F0F829DB7399005914DB /* ThiccDivider.swift in Sources */,
|
||||
4CFF8F5929C9FD1E008DB934 /* DamusPurpleView.swift in Sources */,
|
||||
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */,
|
||||
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
|
||||
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
|
||||
@@ -2992,6 +3054,7 @@
|
||||
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
||||
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
|
||||
4CA9276C2A2910D10098A105 /* ReplyPart.swift in Sources */,
|
||||
D7C6787E2B2D34CC00BCEAFB /* NIP98AuthenticatedRequest.swift in Sources */,
|
||||
4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */,
|
||||
4CE1399229F0666100AC6A0B /* ShareActionButton.swift in Sources */,
|
||||
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */,
|
||||
@@ -3038,6 +3101,7 @@
|
||||
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */,
|
||||
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */,
|
||||
3A90B1812A4EA3AF00000D94 /* UserSearchCache.swift in Sources */,
|
||||
4C9D6D162B1AA9C6004E5CD9 /* DisplayTabBarNotify.swift in Sources */,
|
||||
4CC14FF92A741939007AEB17 /* Referenced.swift in Sources */,
|
||||
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
|
||||
4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */,
|
||||
@@ -3054,6 +3118,7 @@
|
||||
4C7D096F2A0AEA0400943473 /* ScannerViewController.swift in Sources */,
|
||||
4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */,
|
||||
4CC7AAF6297F1A6A00430951 /* EventBody.swift in Sources */,
|
||||
D76556D62B1E6C08001B0CCC /* DamusPurpleWelcomeView.swift in Sources */,
|
||||
3165648B295B70D500C64604 /* LinkView.swift in Sources */,
|
||||
4C8D00CF29E38B950036AF10 /* nostr_bech32.c in Sources */,
|
||||
4C1253562A76C8C60004F4B8 /* BroadcastNotify.swift in Sources */,
|
||||
@@ -3145,6 +3210,7 @@
|
||||
4C75EFB528049D790006080F /* Relay.swift in Sources */,
|
||||
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */,
|
||||
4CC7AAFA297F64AC00430951 /* EventMenu.swift in Sources */,
|
||||
B57B4C622B312BD700A232C0 /* ReconnectRelaysNotify.swift in Sources */,
|
||||
E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */,
|
||||
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */,
|
||||
4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */,
|
||||
@@ -3178,12 +3244,14 @@
|
||||
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */,
|
||||
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */,
|
||||
501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */,
|
||||
B5B4D1432B37D47600844320 /* NdbExtensions.swift in Sources */,
|
||||
3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */,
|
||||
D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */,
|
||||
4C4F14A72A2A61A30045A0B9 /* NostrScriptTests.swift in Sources */,
|
||||
D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */,
|
||||
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
|
||||
4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */,
|
||||
3A9ADA322B4CBFD000756AA0 /* BlocksExtensionTests.swift in Sources */,
|
||||
4C7D097E2A0C58B900943473 /* WalletConnectTests.swift in Sources */,
|
||||
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */,
|
||||
D72A2D022AD9C136002AFF62 /* EventViewTests.swift in Sources */,
|
||||
@@ -3193,6 +3261,7 @@
|
||||
75AD872B2AA23A460085EF2C /* Block+Tests.swift in Sources */,
|
||||
F944F56E29EA9CCC0067B3BF /* DamusParseContentTests.swift in Sources */,
|
||||
3A5E47C72A4A76C800C0D090 /* TrieTests.swift in Sources */,
|
||||
B501062D2B363036003874F5 /* AuthIntegrationTests.swift in Sources */,
|
||||
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */,
|
||||
D72A2D052AD9C1B5002AFF62 /* MockDamusState.swift in Sources */,
|
||||
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */,
|
||||
@@ -3202,6 +3271,7 @@
|
||||
3A3040F329A91366008A0F29 /* ProfileViewTests.swift in Sources */,
|
||||
4CF0ABDC2981A19E00D66079 /* ListTests.swift in Sources */,
|
||||
4C684A552A7E91FE005E6031 /* LongPostTests.swift in Sources */,
|
||||
3A9ADA302B4CB5F400756AA0 /* TranslatorTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -71,6 +71,9 @@
|
||||
ReferencedContainer = "container:damus.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<StoreKitConfigurationFileReference
|
||||
identifier = "../../Purple.storekit">
|
||||
</StoreKitConfigurationFileReference>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Damus dark-gray.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Damus dark.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "special-features.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "special-features.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "special-features.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "stars-bg.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 262 KiB |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "shadow-2.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
+21
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "shadow.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 511 KiB |
@@ -41,5 +41,8 @@ class DamusColors {
|
||||
static let neutral1 = Color("DamusNeutral1")
|
||||
static let neutral3 = Color("DamusNeutral3")
|
||||
static let neutral6 = Color("DamusNeutral6")
|
||||
static let pink = Color(red: 211/255.0, green: 76/255.0, blue: 217/255.0)
|
||||
static let lighterPink = Color(red: 248/255.0, green: 105/255.0, blue: 182/255.0)
|
||||
static let lightBackgroundPink = Color(red: 0xF8/255.0, green: 0xE7/255.0, blue: 0xF8/255.0)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,37 +7,49 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum NeutralButtonShape {
|
||||
case rounded, capsule, circle
|
||||
|
||||
var style: NeutralButtonStyle {
|
||||
switch self {
|
||||
case .rounded:
|
||||
return NeutralButtonStyle(padding: EdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10), cornerRadius: 12)
|
||||
case .capsule:
|
||||
return NeutralButtonStyle(padding: EdgeInsets(top: 5, leading: 15, bottom: 5, trailing: 15), cornerRadius: 20)
|
||||
case .circle:
|
||||
return NeutralButtonStyle(padding: EdgeInsets(top: 20, leading: 20, bottom: 20, trailing: 20), cornerRadius: 9999)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct NeutralButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Self.Configuration) -> some View {
|
||||
return configuration.label
|
||||
let padding: EdgeInsets
|
||||
let cornerRadius: CGFloat
|
||||
let scaleEffect: CGFloat
|
||||
|
||||
init(padding: EdgeInsets = EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0), cornerRadius: CGFloat = 15, scaleEffect: CGFloat = 0.95) {
|
||||
self.padding = padding
|
||||
self.cornerRadius = cornerRadius
|
||||
self.scaleEffect = scaleEffect
|
||||
}
|
||||
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
configuration.label
|
||||
.padding(padding)
|
||||
.background(DamusColors.neutral1)
|
||||
.cornerRadius(12)
|
||||
.cornerRadius(cornerRadius)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.stroke(DamusColors.neutral3, lineWidth: 1)
|
||||
)
|
||||
.scaleEffect(configuration.isPressed ? 0.95 : 1)
|
||||
.scaleEffect(configuration.isPressed ? scaleEffect : 1)
|
||||
}
|
||||
}
|
||||
|
||||
struct NeutralCircleButtonStyle: ButtonStyle {
|
||||
func makeBody(configuration: Self.Configuration) -> some View {
|
||||
return configuration.label
|
||||
.padding(20)
|
||||
.background(DamusColors.neutral1)
|
||||
.cornerRadius(9999)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 9999)
|
||||
.stroke(DamusColors.neutral3, lineWidth: 1)
|
||||
)
|
||||
.scaleEffect(configuration.isPressed ? 0.95 : 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct NeutralButtonStyle_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
|
||||
Button(action: {
|
||||
print("dynamic size")
|
||||
}) {
|
||||
@@ -45,8 +57,7 @@ struct NeutralButtonStyle_Previews: PreviewProvider {
|
||||
.padding()
|
||||
}
|
||||
.buttonStyle(NeutralButtonStyle())
|
||||
|
||||
|
||||
|
||||
Button(action: {
|
||||
print("infinite width")
|
||||
}) {
|
||||
@@ -58,6 +69,17 @@ struct NeutralButtonStyle_Previews: PreviewProvider {
|
||||
}
|
||||
.buttonStyle(NeutralButtonStyle())
|
||||
.padding()
|
||||
|
||||
Button("Rounded Button", action: {})
|
||||
.buttonStyle(NeutralButtonShape.rounded.style)
|
||||
.padding()
|
||||
|
||||
Button("Capsule Button", action: {})
|
||||
.buttonStyle(NeutralButtonShape.capsule.style)
|
||||
.padding()
|
||||
|
||||
Button(action: {}, label: {Image("messages")})
|
||||
.buttonStyle(NeutralButtonShape.circle.style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ struct NonImageAvatar<Content: View>: View {
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color(red: 0xF8/255.0, green: 0xE7/255.0, blue: 0xF8/255.0))
|
||||
.fill(DamusColors.lightBackgroundPink)
|
||||
.frame(width: 54, height: 54)
|
||||
|
||||
content
|
||||
|
||||
@@ -64,7 +64,7 @@ struct TranslateView: View {
|
||||
guard let note_language = translations_model.note_language else {
|
||||
return
|
||||
}
|
||||
let res = await translate_note(profiles: damus_state.profiles, keypair: damus_state.keypair, event: event, settings: damus_state.settings, note_lang: note_language)
|
||||
let res = await translate_note(profiles: damus_state.profiles, keypair: damus_state.keypair, event: event, settings: damus_state.settings, note_lang: note_language, purple: damus_state.purple)
|
||||
DispatchQueue.main.async {
|
||||
self.translations_model.state = res
|
||||
}
|
||||
@@ -125,10 +125,10 @@ struct TranslateView_Previews: PreviewProvider {
|
||||
}
|
||||
}
|
||||
|
||||
func translate_note(profiles: Profiles, keypair: Keypair, event: NostrEvent, settings: UserSettingsStore, note_lang: String) async -> TranslateStatus {
|
||||
func translate_note(profiles: Profiles, keypair: Keypair, event: NostrEvent, settings: UserSettingsStore, note_lang: String, purple: DamusPurple) async -> TranslateStatus {
|
||||
|
||||
// If the note language is different from our preferred languages, send a translation request.
|
||||
let translator = Translator(settings)
|
||||
let translator = Translator(settings, purple: purple)
|
||||
let originalContent = event.get_content(keypair)
|
||||
let translated_note = try? await translator.translate(originalContent, from: note_lang, to: current_language())
|
||||
|
||||
|
||||
+47
-22
@@ -71,6 +71,7 @@ struct ContentView: View {
|
||||
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home
|
||||
@State var muting: Pubkey? = nil
|
||||
@State var confirm_mute: Bool = false
|
||||
@State var hide_bar: 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
|
||||
@@ -153,25 +154,26 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
func MainContent(damus: DamusState) -> some View {
|
||||
ZStack {
|
||||
if #available(iOS 16.0, *) {
|
||||
SearchHomeView(damus_state: damus_state!, model: SearchHomeModel(damus_state: damus_state!))
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
.opacity(selected_timeline == .search ? 1 : 0)
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
SearchHomeView(damus_state: damus_state!, model: SearchHomeModel(damus_state: damus_state!))
|
||||
.opacity(selected_timeline == .search ? 1 : 0)
|
||||
VStack {
|
||||
switch selected_timeline {
|
||||
case .search:
|
||||
if #available(iOS 16.0, *) {
|
||||
SearchHomeView(damus_state: damus_state!, model: SearchHomeModel(damus_state: damus_state!))
|
||||
.scrollDismissesKeyboard(.immediately)
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
SearchHomeView(damus_state: damus_state!, model: SearchHomeModel(damus_state: damus_state!))
|
||||
}
|
||||
|
||||
case .home:
|
||||
PostingTimelineView
|
||||
|
||||
case .notifications:
|
||||
NotificationsView(state: damus, notifications: home.notifications)
|
||||
|
||||
case .dms:
|
||||
DirectMessagesView(damus_state: damus_state!, model: damus_state!.dms, settings: damus_state!.settings)
|
||||
}
|
||||
|
||||
PostingTimelineView
|
||||
.opacity(selected_timeline == .home ? 1 : 0)
|
||||
|
||||
NotificationsView(state: damus, notifications: home.notifications)
|
||||
.opacity(selected_timeline == .notifications ? 1 : 0)
|
||||
|
||||
DirectMessagesView(damus_state: damus_state!, model: damus_state!.dms, settings: damus_state!.settings)
|
||||
.opacity(selected_timeline == .dms ? 1 : 0)
|
||||
}
|
||||
.navigationBarTitle(timeline_name(selected_timeline), displayMode: .inline)
|
||||
.toolbar {
|
||||
@@ -283,12 +285,17 @@ struct ContentView: View {
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
|
||||
TabBar(nstatus: home.notification_status, selected: $selected_timeline, settings: damus.settings, action: switch_timeline)
|
||||
.padding([.bottom], 8)
|
||||
.background(Color(uiColor: .systemBackground).ignoresSafeArea())
|
||||
if !hide_bar {
|
||||
TabBar(nstatus: home.notification_status, selected: $selected_timeline, settings: damus.settings, action: switch_timeline)
|
||||
.padding([.bottom], 8)
|
||||
.background(Color(uiColor: .systemBackground).ignoresSafeArea())
|
||||
} else {
|
||||
Text("")
|
||||
}
|
||||
}
|
||||
}
|
||||
.ignoresSafeArea(.keyboard)
|
||||
.edgesIgnoringSafeArea(hide_bar ? [.bottom] : [])
|
||||
.onAppear() {
|
||||
self.connect()
|
||||
try? AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playback, mode: .default, options: .mixWithOthers)
|
||||
@@ -343,6 +350,10 @@ struct ContentView: View {
|
||||
.onReceive(handle_notify(.compose)) { action in
|
||||
self.active_sheet = .post(action)
|
||||
}
|
||||
.onReceive(handle_notify(.display_tabbar)) { display in
|
||||
let show = display
|
||||
self.hide_bar = !show
|
||||
}
|
||||
.onReceive(timer) { n in
|
||||
self.damus_state?.postbox.try_flushing_events()
|
||||
self.damus_state!.profiles.profile_data(self.damus_state!.pubkey).status.try_expire()
|
||||
@@ -447,6 +458,9 @@ struct ContentView: View {
|
||||
break
|
||||
}
|
||||
}
|
||||
.onReceive(handle_notify(.disconnect_relays)) { () in
|
||||
damus_state.pool.disconnect()
|
||||
}
|
||||
.onChange(of: scenePhase) { (phase: ScenePhase) in
|
||||
switch phase {
|
||||
case .background:
|
||||
@@ -616,7 +630,7 @@ struct ContentView: View {
|
||||
|
||||
guard let ndb = mndb else { return }
|
||||
|
||||
let pool = RelayPool(ndb: ndb)
|
||||
let pool = RelayPool(ndb: ndb, keypair: keypair)
|
||||
let model_cache = RelayModelCache()
|
||||
let relay_filters = RelayFilters(our_pubkey: pubkey)
|
||||
let bootstrap_relays = load_bootstrap_relays(pubkey: pubkey)
|
||||
@@ -667,8 +681,17 @@ struct ContentView: View {
|
||||
video: VideoController(),
|
||||
ndb: ndb
|
||||
)
|
||||
|
||||
home.damus_state = self.damus_state!
|
||||
|
||||
if let damus_state, damus_state.settings.enable_experimental_purple_api {
|
||||
// Assign delegate so that we can send receipts to the Purple API server as soon as we get updates from user's purchases
|
||||
StoreObserver.standard.delegate = damus_state.purple
|
||||
}
|
||||
else {
|
||||
// Purple API is an experimental feature. If not enabled, do not connect `StoreObserver` with Purple API to avoid leaking receipts
|
||||
}
|
||||
|
||||
pool.connect()
|
||||
}
|
||||
|
||||
@@ -894,6 +917,8 @@ func find_event_with_subid(state: DamusState, query query_: FindEvent, subid: St
|
||||
}
|
||||
case .notice:
|
||||
break
|
||||
case .auth:
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict/>
|
||||
</array>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
||||
@@ -34,6 +34,39 @@ struct DamusState {
|
||||
let music: MusicController?
|
||||
let video: VideoController
|
||||
let ndb: Ndb
|
||||
var purple: DamusPurple
|
||||
|
||||
init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [String], replies: ReplyCounter, muted_threads: MutedThreadsManager, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil) {
|
||||
self.pool = pool
|
||||
self.keypair = keypair
|
||||
self.likes = likes
|
||||
self.boosts = boosts
|
||||
self.contacts = contacts
|
||||
self.profiles = profiles
|
||||
self.dms = dms
|
||||
self.previews = previews
|
||||
self.zaps = zaps
|
||||
self.lnurls = lnurls
|
||||
self.settings = settings
|
||||
self.relay_filters = relay_filters
|
||||
self.relay_model_cache = relay_model_cache
|
||||
self.drafts = drafts
|
||||
self.events = events
|
||||
self.bookmarks = bookmarks
|
||||
self.postbox = postbox
|
||||
self.bootstrap_relays = bootstrap_relays
|
||||
self.replies = replies
|
||||
self.muted_threads = muted_threads
|
||||
self.wallet = wallet
|
||||
self.nav = nav
|
||||
self.music = music
|
||||
self.video = video
|
||||
self.ndb = ndb
|
||||
self.purple = purple ?? DamusPurple(
|
||||
environment: settings.purple_api_local_test_mode ? .local_test : .production,
|
||||
keypair: keypair
|
||||
)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func add_zap(zap: Zapping) -> Bool {
|
||||
|
||||
@@ -63,6 +63,8 @@ class EventsModel: ObservableObject {
|
||||
break
|
||||
case .ok:
|
||||
break
|
||||
case .auth:
|
||||
break
|
||||
case .eose:
|
||||
let txn = NdbTxn(ndb: self.state.ndb)
|
||||
load_profiles(context: "events_model", profiles_subid: profiles_id, relay_id: relay_id, load: .from_events(events), damus_state: state, txn: txn)
|
||||
|
||||
@@ -91,6 +91,8 @@ class FollowersModel: ObservableObject {
|
||||
|
||||
case .ok:
|
||||
break
|
||||
case .auth:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,6 +446,8 @@ class HomeModel {
|
||||
|
||||
case .ok:
|
||||
break
|
||||
case .auth:
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -118,6 +118,11 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
case .ok:
|
||||
break
|
||||
case .event(_, let ev):
|
||||
// Ensure the event public key matches this profiles public key
|
||||
// This is done to protect against a relay not properly filtering events by the pubkey
|
||||
// See https://github.com/damus-io/damus/issues/1846 for more information
|
||||
guard self.pubkey == ev.pubkey else { break }
|
||||
|
||||
add_event(ev)
|
||||
case .notice:
|
||||
break
|
||||
@@ -129,6 +134,8 @@ class ProfileModel: ObservableObject, Equatable {
|
||||
}
|
||||
progress += 1
|
||||
break
|
||||
case .auth:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
//
|
||||
// DamusPurple.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2023-12-08.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class DamusPurple: StoreObserverDelegate {
|
||||
let environment: ServerEnvironment
|
||||
let keypair: Keypair
|
||||
var starred_profiles_cache: [Pubkey: Bool]
|
||||
|
||||
init(environment: ServerEnvironment, keypair: Keypair) {
|
||||
self.environment = environment
|
||||
self.keypair = keypair
|
||||
self.starred_profiles_cache = [:]
|
||||
}
|
||||
|
||||
// MARK: Functions
|
||||
func is_profile_subscribed_to_purple(pubkey: Pubkey) async -> Bool? {
|
||||
if let cached_result = self.starred_profiles_cache[pubkey] {
|
||||
return cached_result
|
||||
}
|
||||
|
||||
guard let data = await self.get_account_data(pubkey: pubkey) else { return nil }
|
||||
|
||||
if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
|
||||
let active = json["active"] as? Bool {
|
||||
self.starred_profiles_cache[pubkey] = active
|
||||
return active
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func account_exists(pubkey: Pubkey) async -> Bool? {
|
||||
guard let account_data = await self.get_account_data(pubkey: pubkey) else { return nil }
|
||||
|
||||
if let account_info = try? JSONDecoder().decode(AccountInfo.self, from: account_data) {
|
||||
return account_info.pubkey == pubkey.hex()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func get_account_data(pubkey: Pubkey) async -> Data? {
|
||||
let url = environment.get_base_url().appendingPathComponent("accounts/\(pubkey.hex())")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(for: request)
|
||||
return data
|
||||
} catch {
|
||||
print("Failed to fetch data: \(error)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func create_account(pubkey: Pubkey) async throws {
|
||||
let url = environment.get_base_url().appendingPathComponent("accounts")
|
||||
|
||||
Log.info("Creating account with Damus Purple server", for: .damus_purple)
|
||||
|
||||
let (data, response) = try await make_nip98_authenticated_request(
|
||||
method: .post,
|
||||
url: url,
|
||||
payload: nil,
|
||||
payload_type: nil,
|
||||
auth_keypair: self.keypair
|
||||
)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
switch httpResponse.statusCode {
|
||||
case 200:
|
||||
Log.info("Created an account with Damus Purple server", for: .damus_purple)
|
||||
default:
|
||||
Log.error("Error in creating account with Damus Purple. HTTP status code: %d; Response: %s", for: .damus_purple, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func create_account_if_not_existing(pubkey: Pubkey) async throws {
|
||||
guard await !(self.account_exists(pubkey: pubkey) ?? false) else { return }
|
||||
try await self.create_account(pubkey: pubkey)
|
||||
}
|
||||
|
||||
func send_receipt() async {
|
||||
// Get the receipt if it's available.
|
||||
if let appStoreReceiptURL = Bundle.main.appStoreReceiptURL,
|
||||
FileManager.default.fileExists(atPath: appStoreReceiptURL.path) {
|
||||
|
||||
try? await create_account_if_not_existing(pubkey: keypair.pubkey)
|
||||
|
||||
do {
|
||||
let receiptData = try Data(contentsOf: appStoreReceiptURL, options: .alwaysMapped)
|
||||
let url = environment.get_base_url().appendingPathComponent("accounts/\(keypair.pubkey.hex())/app-store-receipt")
|
||||
|
||||
Log.info("Sending in-app purchase receipt to Damus Purple server", for: .damus_purple)
|
||||
|
||||
let (data, response) = try await make_nip98_authenticated_request(
|
||||
method: .post,
|
||||
url: url,
|
||||
payload: receiptData,
|
||||
payload_type: .binary,
|
||||
auth_keypair: self.keypair
|
||||
)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
switch httpResponse.statusCode {
|
||||
case 200:
|
||||
Log.info("Sent in-app purchase receipt to Damus Purple server successfully", for: .damus_purple)
|
||||
default:
|
||||
Log.error("Error in sending in-app purchase receipt to Damus Purple. HTTP status code: %d; Response: %s", for: .damus_purple, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
catch {
|
||||
Log.error("Couldn't read receipt data with error: %s", for: .damus_purple, error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func translate(text: String, source source_language: String, target target_language: String) async throws -> String {
|
||||
var url = environment.get_base_url()
|
||||
url.append(path: "/translate")
|
||||
url.append(queryItems: [
|
||||
.init(name: "source", value: source_language),
|
||||
.init(name: "target", value: target_language),
|
||||
.init(name: "q", value: text)
|
||||
])
|
||||
let (data, response) = try await make_nip98_authenticated_request(
|
||||
method: .get,
|
||||
url: url,
|
||||
payload: nil,
|
||||
payload_type: nil,
|
||||
auth_keypair: self.keypair
|
||||
)
|
||||
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
switch httpResponse.statusCode {
|
||||
case 200:
|
||||
return try JSONDecoder().decode(TranslationResult.self, from: data).text
|
||||
default:
|
||||
Log.error("Translation error with Damus Purple. HTTP status code: %d; Response: %s", for: .damus_purple, httpResponse.statusCode, String(data: data, encoding: .utf8) ?? "Unknown")
|
||||
throw PurpleError.translation_error(status_code: httpResponse.statusCode, response: data)
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw PurpleError.translation_no_response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: API types
|
||||
|
||||
extension DamusPurple {
|
||||
fileprivate struct AccountInfo: Codable {
|
||||
let pubkey: String
|
||||
let created_at: UInt64
|
||||
let expiry: UInt64?
|
||||
let active: Bool
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Helper structures
|
||||
|
||||
extension DamusPurple {
|
||||
enum ServerEnvironment {
|
||||
case local_test
|
||||
case production
|
||||
|
||||
func get_base_url() -> URL {
|
||||
switch self {
|
||||
case .local_test:
|
||||
Constants.PURPLE_API_TEST_BASE_URL
|
||||
case .production:
|
||||
Constants.PURPLE_API_PRODUCTION_BASE_URL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum PurpleError: Error {
|
||||
case translation_error(status_code: Int, response: Data)
|
||||
case translation_no_response
|
||||
}
|
||||
|
||||
struct TranslationResult: Codable {
|
||||
let text: String
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// StoreObserver.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2023-12-08.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import StoreKit
|
||||
|
||||
class StoreObserver: NSObject, SKPaymentTransactionObserver {
|
||||
static let standard = StoreObserver()
|
||||
|
||||
var delegate: StoreObserverDelegate?
|
||||
|
||||
init(delegate: StoreObserverDelegate? = nil) {
|
||||
self.delegate = delegate
|
||||
super.init()
|
||||
}
|
||||
|
||||
//Observe transaction updates.
|
||||
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
|
||||
//Handle transaction states here.
|
||||
|
||||
Task {
|
||||
await self.delegate?.send_receipt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protocol StoreObserverDelegate {
|
||||
func send_receipt() async
|
||||
}
|
||||
@@ -87,6 +87,8 @@ class SearchHomeModel: ObservableObject {
|
||||
load_profiles(context: "universe", profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events.all_events), damus_state: damus_state, txn: txn)
|
||||
}
|
||||
|
||||
break
|
||||
case .auth:
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -159,6 +161,8 @@ func load_profiles<Y>(context: String, profiles_subid: String, relay_id: String,
|
||||
break
|
||||
case .notice:
|
||||
break
|
||||
case .auth:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -130,6 +130,9 @@ func handle_subid_event(pool: RelayPool, relay_id: String, ev: NostrConnectionEv
|
||||
|
||||
case .eose(let subid):
|
||||
return (subid, true)
|
||||
|
||||
case .auth:
|
||||
return (nil, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
|
||||
}
|
||||
|
||||
case none
|
||||
case purple
|
||||
case libretranslate
|
||||
case deepl
|
||||
case nokyctranslate
|
||||
@@ -38,6 +39,8 @@ enum TranslationService: String, CaseIterable, Identifiable, StringCodable {
|
||||
switch self {
|
||||
case .none:
|
||||
return .init(tag: self.rawValue, displayName: NSLocalizedString("none_translation_service", value: "None", comment: "Dropdown option for selecting no translation service."))
|
||||
case .purple:
|
||||
return .init(tag: self.rawValue, displayName: NSLocalizedString("Damus Purple", comment: "Dropdown option for selecting Damus Purple as a translation service."))
|
||||
case .libretranslate:
|
||||
return .init(tag: self.rawValue, displayName: NSLocalizedString("LibreTranslate (Open Source)", comment: "Dropdown option for selecting LibreTranslate as the translation service."))
|
||||
case .deepl:
|
||||
|
||||
@@ -107,8 +107,8 @@ class UserSettingsStore: ObservableObject {
|
||||
@Setting(key: "left_handed", default_value: false)
|
||||
var left_handed: Bool
|
||||
|
||||
@Setting(key: "always_show_images", default_value: false)
|
||||
var always_show_images: Bool
|
||||
@Setting(key: "blur_images", default_value: true)
|
||||
var blur_images: Bool
|
||||
|
||||
@Setting(key: "media_previews", default_value: true)
|
||||
var media_previews: Bool
|
||||
@@ -201,6 +201,12 @@ class UserSettingsStore: ObservableObject {
|
||||
@Setting(key: "send_device_token_to_localhost", default_value: false)
|
||||
var send_device_token_to_localhost: Bool
|
||||
|
||||
@Setting(key: "enable_experimental_purple_api", default_value: false)
|
||||
var enable_experimental_purple_api: Bool
|
||||
|
||||
@Setting(key: "purple_api_local_test_mode", default_value: false)
|
||||
var purple_api_local_test_mode: Bool
|
||||
|
||||
@Setting(key: "emoji_reactions", default_value: default_emoji_reactions)
|
||||
var emoji_reactions: [String]
|
||||
|
||||
@@ -290,6 +296,8 @@ class UserSettingsStore: ObservableObject {
|
||||
switch translation_service {
|
||||
case .none:
|
||||
return false
|
||||
case .purple:
|
||||
return true
|
||||
case .libretranslate:
|
||||
return URLComponents(string: libretranslate_url) != nil
|
||||
case .deepl:
|
||||
|
||||
@@ -66,6 +66,8 @@ class ZapsModel: ObservableObject {
|
||||
}
|
||||
|
||||
self.state.add_zap(zap: .zap(zap))
|
||||
case .auth:
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// NIP98AuthenticatedRequest.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2023-12-15.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum HTTPMethod: String {
|
||||
case get = "GET"
|
||||
case post = "POST"
|
||||
case put = "PUT"
|
||||
case delete = "DELETE"
|
||||
}
|
||||
|
||||
enum HTTPPayloadType: String {
|
||||
case json = "application/json"
|
||||
case binary = "application/octet-stream"
|
||||
}
|
||||
|
||||
func make_nip98_authenticated_request(method: HTTPMethod, url: URL, payload: Data?, payload_type: HTTPPayloadType?, auth_keypair: Keypair) async throws -> (data: Data, response: URLResponse) {
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method.rawValue
|
||||
request.httpBody = payload
|
||||
|
||||
var tag_pairs = [
|
||||
["u", url.absoluteString],
|
||||
["method", method.rawValue],
|
||||
]
|
||||
|
||||
if let payload {
|
||||
let payload_hash = sha256(payload)
|
||||
let payload_hash_hex = hex_encode(payload_hash)
|
||||
tag_pairs.append(["payload", payload_hash_hex])
|
||||
}
|
||||
|
||||
let auth_note = NdbNote(
|
||||
content: "",
|
||||
keypair: auth_keypair,
|
||||
kind: 27235,
|
||||
tags: tag_pairs,
|
||||
createdAt: UInt32(Date().timeIntervalSince1970)
|
||||
)
|
||||
|
||||
let auth_note_json_data: Data = try JSONEncoder().encode(auth_note)
|
||||
let auth_note_base64: String = base64_encode(auth_note_json_data.bytes)
|
||||
|
||||
request.setValue("Nostr " + auth_note_base64, forHTTPHeaderField: "Authorization")
|
||||
if let payload_type {
|
||||
request.setValue(payload_type.rawValue, forHTTPHeaderField: "Content-Type")
|
||||
}
|
||||
return try await URLSession.shared.data(for: request)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// NostrAuth.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Charlie Fish on 12/18/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
func make_auth_request(keypair: FullKeypair, challenge_string: String, relay: Relay) -> NostrEvent? {
|
||||
let tags: [[String]] = [["relay", relay.descriptor.url.id],["challenge", challenge_string]]
|
||||
let event = NostrEvent(content: "", keypair: keypair.to_keypair(), kind: 22242, tags: tags)
|
||||
return event
|
||||
}
|
||||
@@ -38,7 +38,8 @@ enum NostrRequest {
|
||||
case subscribe(NostrSubscribe)
|
||||
case unsubscribe(String)
|
||||
case event(NostrEvent)
|
||||
|
||||
case auth(NostrEvent)
|
||||
|
||||
var is_write: Bool {
|
||||
switch self {
|
||||
case .subscribe:
|
||||
@@ -47,6 +48,8 @@ enum NostrRequest {
|
||||
return false
|
||||
case .event:
|
||||
return true
|
||||
case .auth:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,11 @@ enum NostrResponse {
|
||||
case notice(String)
|
||||
case eose(String)
|
||||
case ok(CommandResult)
|
||||
|
||||
/// An [NIP-42](https://github.com/nostr-protocol/nips/blob/master/42.md) `auth` challenge.
|
||||
///
|
||||
/// The associated type of this case is the challenge string sent by the server.
|
||||
case auth(String)
|
||||
|
||||
var subid: String? {
|
||||
switch self {
|
||||
case .ok:
|
||||
@@ -34,6 +38,8 @@ enum NostrResponse {
|
||||
return sub_id
|
||||
case .notice:
|
||||
return nil
|
||||
case .auth(let challenge_string):
|
||||
return challenge_string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +100,13 @@ enum NostrResponse {
|
||||
case NDB_TCE_NOTICE:
|
||||
free(data)
|
||||
return .notice("")
|
||||
case NDB_TCE_AUTH:
|
||||
defer { free(data) }
|
||||
|
||||
guard let challenge_string = sized_cstr(cstr: tce.subid, len: tce.subid_len) else {
|
||||
return nil
|
||||
}
|
||||
return .auth(challenge_string)
|
||||
default:
|
||||
free(data)
|
||||
return nil
|
||||
|
||||
+31
-2
@@ -57,6 +57,25 @@ enum RelayFlags: Int {
|
||||
case broken = 1
|
||||
}
|
||||
|
||||
enum RelayAuthenticationError {
|
||||
/// Only a public key was provided in keypair to sign challenge.
|
||||
///
|
||||
/// A private key is required to sign `auth` challenge.
|
||||
case no_private_key
|
||||
/// No keypair was provided to sign challenge.
|
||||
case no_key
|
||||
}
|
||||
enum RelayAuthenticationState: Equatable {
|
||||
/// No `auth` request has been made from this relay
|
||||
case none
|
||||
/// We have received an `auth` challenge, but have not yet replied to the challenge
|
||||
case pending
|
||||
/// We have received an `auth` challenge and replied with an `auth` event
|
||||
case verified
|
||||
/// We received an `auth` challenge but failed to reply to the challenge
|
||||
case error(RelayAuthenticationError)
|
||||
}
|
||||
|
||||
struct Limitations: Codable {
|
||||
let payment_required: Bool?
|
||||
|
||||
@@ -85,13 +104,15 @@ struct RelayMetadata: Codable {
|
||||
class Relay: Identifiable {
|
||||
let descriptor: RelayDescriptor
|
||||
let connection: RelayConnection
|
||||
|
||||
var authentication_state: RelayAuthenticationState
|
||||
|
||||
var flags: Int
|
||||
|
||||
init(descriptor: RelayDescriptor, connection: RelayConnection) {
|
||||
self.flags = 0
|
||||
self.descriptor = descriptor
|
||||
self.connection = connection
|
||||
self.authentication_state = RelayAuthenticationState.none
|
||||
}
|
||||
|
||||
var is_broken: Bool {
|
||||
@@ -109,5 +130,13 @@ enum RelayError: Error {
|
||||
}
|
||||
|
||||
func get_relay_id(_ url: RelayURL) -> String {
|
||||
return url.url.absoluteString
|
||||
let trimTrailingSlashes: (String) -> String = { url in
|
||||
var trimmedUrl = url
|
||||
while trimmedUrl.hasSuffix("/") {
|
||||
trimmedUrl.removeLast()
|
||||
}
|
||||
return trimmedUrl
|
||||
}
|
||||
|
||||
return trimTrailingSlashes(url.url.absoluteString)
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ final class RelayConnection: ObservableObject {
|
||||
socket.send(.string(req))
|
||||
}
|
||||
|
||||
func send(_ req: NostrRequestType) {
|
||||
func send(_ req: NostrRequestType, callback: ((String) -> Void)? = nil) {
|
||||
switch req {
|
||||
case .typical(let req):
|
||||
guard let req = make_nostr_req(req) else {
|
||||
@@ -105,9 +105,11 @@ final class RelayConnection: ObservableObject {
|
||||
return
|
||||
}
|
||||
send_raw(req)
|
||||
callback?(req)
|
||||
|
||||
case .custom(let req):
|
||||
send_raw(req)
|
||||
callback?(req)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,9 +203,20 @@ func make_nostr_req(_ req: NostrRequest) -> String? {
|
||||
return make_nostr_unsubscribe_req(sub_id)
|
||||
case .event(let ev):
|
||||
return make_nostr_push_event(ev: ev)
|
||||
case .auth(let ev):
|
||||
return make_nostr_auth_event(ev: ev)
|
||||
}
|
||||
}
|
||||
|
||||
func make_nostr_auth_event(ev: NostrEvent) -> String? {
|
||||
guard let event = encode_json(ev) else {
|
||||
return nil
|
||||
}
|
||||
let encoded = "[\"AUTH\",\(event)]"
|
||||
print(encoded)
|
||||
return encoded
|
||||
}
|
||||
|
||||
func make_nostr_push_event(ev: NostrEvent) -> String? {
|
||||
guard let event = encode_json(ev) else {
|
||||
return nil
|
||||
|
||||
@@ -31,13 +31,17 @@ class RelayPool {
|
||||
var seen: Set<SeenEvent> = Set()
|
||||
var counts: [String: UInt64] = [:]
|
||||
var ndb: Ndb
|
||||
var keypair: Keypair?
|
||||
var message_received_function: (((String, RelayDescriptor)) -> Void)?
|
||||
var message_sent_function: (((String, Relay)) -> Void)?
|
||||
|
||||
private let network_monitor = NWPathMonitor()
|
||||
private let network_monitor_queue = DispatchQueue(label: "io.damus.network_monitor")
|
||||
private var last_network_status: NWPath.Status = .unsatisfied
|
||||
|
||||
init(ndb: Ndb) {
|
||||
init(ndb: Ndb, keypair: Keypair? = nil) {
|
||||
self.ndb = ndb
|
||||
self.keypair = keypair
|
||||
|
||||
network_monitor.pathUpdateHandler = { [weak self] path in
|
||||
if (path.status == .satisfied || path.status == .requiresConnection) && self?.last_network_status != path.status {
|
||||
@@ -121,6 +125,7 @@ class RelayPool {
|
||||
else { return }
|
||||
|
||||
let _ = self.ndb.process_event(str)
|
||||
self.message_received_function?((str, desc))
|
||||
})
|
||||
let relay = Relay(descriptor: desc, connection: conn)
|
||||
self.relays.append(relay)
|
||||
@@ -244,7 +249,9 @@ class RelayPool {
|
||||
continue
|
||||
}
|
||||
|
||||
relay.connection.send(req)
|
||||
relay.connection.send(req, callback: { str in
|
||||
self.message_sent_function?((str, relay))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,7 +305,34 @@ class RelayPool {
|
||||
run_queue(relay_id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Handle auth
|
||||
if case let .nostr_event(nostrResponse) = event,
|
||||
case let .auth(challenge_string) = nostrResponse {
|
||||
if let relay = get_relay(relay_id) {
|
||||
print("received auth request from \(relay.descriptor.url.id)")
|
||||
relay.authentication_state = .pending
|
||||
if let keypair {
|
||||
if let fullKeypair = keypair.to_full() {
|
||||
if let authRequest = make_auth_request(keypair: fullKeypair, challenge_string: challenge_string, relay: relay) {
|
||||
send(.auth(authRequest), to: [relay_id], skip_ephemeral: false)
|
||||
relay.authentication_state = .verified
|
||||
} else {
|
||||
print("failed to make auth request")
|
||||
}
|
||||
} else {
|
||||
print("keypair provided did not contain private key, can not sign auth request")
|
||||
relay.authentication_state = .error(.no_private_key)
|
||||
}
|
||||
} else {
|
||||
print("no keypair to reply to auth request")
|
||||
relay.authentication_state = .error(.no_key)
|
||||
}
|
||||
} else {
|
||||
print("no relay found for \(relay_id)")
|
||||
}
|
||||
}
|
||||
|
||||
for handler in handlers {
|
||||
handler.callback(relay_id, event)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// DisplayTabBarNotify.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-12-01.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct DisplayTabBarNotify: Notify {
|
||||
typealias Payload = Bool
|
||||
var payload: Payload
|
||||
}
|
||||
|
||||
extension NotifyHandler {
|
||||
static var display_tabbar: NotifyHandler<DisplayTabBarNotify> {
|
||||
.init()
|
||||
}
|
||||
}
|
||||
|
||||
extension Notifications {
|
||||
static func display_tabbar(_ payload: Bool) -> Notifications<DisplayTabBarNotify> {
|
||||
.init(.init(payload: payload))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// ReconnectRelaysNotify.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Charlie Fish on 12/18/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ReconnectRelaysNotify: Notify {
|
||||
typealias Payload = ()
|
||||
var payload: ()
|
||||
}
|
||||
|
||||
extension NotifyHandler {
|
||||
static var disconnect_relays: NotifyHandler<ReconnectRelaysNotify> {
|
||||
.init()
|
||||
}
|
||||
}
|
||||
|
||||
extension Notifications {
|
||||
/// Reconnects all relays.
|
||||
static var disconnect_relays: Notifications<ReconnectRelaysNotify> {
|
||||
.init(.init(payload: ()))
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
+49
-1
@@ -6,7 +6,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import NaturalLanguage
|
||||
|
||||
fileprivate extension String {
|
||||
/// Failable initializer to build a Swift.String from a C-backed `str_block_t`.
|
||||
@@ -218,3 +218,51 @@ extension Block {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Blocks {
|
||||
|
||||
/// Returns a language hypothesis represented as an ``NLLanguage`` (determined by ``NLLanguageRecognizer``),
|
||||
/// which is the most likely language detected using the combination of blocks. If it cannot determine one, `nil` is returned.
|
||||
var languageHypothesis: NLLanguage? {
|
||||
// Rely on Apple's NLLanguageRecognizer to tell us which language it thinks the blocks are in
|
||||
// and filter on only the text portions of the content as URLs, hashtags, and anything else confuse the language recognizer.
|
||||
let originalOnlyText = blocks.compactMap {
|
||||
if case .text(let txt) = $0 {
|
||||
// Replacing right single quotation marks (’) with "typewriter or ASCII apostrophes" (')
|
||||
// as a workaround to get Apple's language recognizer to predict language the correctly.
|
||||
// It is important to add this workaround to get the language right because it wastes users' money to send translation requests.
|
||||
// Until Apple fixes their language model, this workaround will be kept in place.
|
||||
// See https://en.wikipedia.org/wiki/Apostrophe#Unicode for an explanation of the differences between the two characters.
|
||||
//
|
||||
// For example,
|
||||
// "nevent1qqs0wsknetaju06xk39cv8sttd064amkykqalvfue7ydtg3p0lyfksqzyrhxagf6h8l9cjngatumrg60uq22v66qz979pm32v985ek54ndh8gj42wtp"
|
||||
// has the note content "It’s a meme".
|
||||
// Without the character replacement, it is 61% confident that the text is in Turkish (tr) and 8% confident that the text is in English (en),
|
||||
// which is a wildly incorrect hypothesis.
|
||||
// With the character replacement, it is 65% confident that the text is in English (en) and 24% confident that the text is in Turkish (tr), which is more accurate.
|
||||
//
|
||||
// Similarly,
|
||||
// "nevent1qqspjqlln6wvxrqg6kzl2p7gk0rgr5stc7zz5sstl34cxlw55gvtylgpp4mhxue69uhkummn9ekx7mqpr4mhxue69uhkummnw3ez6ur4vgh8wetvd3hhyer9wghxuet5qy28wumn8ghj7un9d3shjtnwdaehgu3wvfnsygpx6655ve67vqlcme9ld7ww73pqx7msclhwzu8lqmkhvuluxnyc7yhf3xut"
|
||||
// has the note content "You’re funner".
|
||||
// Without the character replacement, it is 52% confident that the text is in Norwegian Bokmål (nb) and 41% confident that the text is in English (en).
|
||||
// With the character replacement, it is 93% confident that the text is in English (en) and 4% confident that the text is in Norwegian Bokmål (nb).
|
||||
return txt.replacingOccurrences(of: "’", with: "'")
|
||||
}
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
.joined(separator: " ")
|
||||
|
||||
// If there is no text, there's nothing to use to detect language.
|
||||
guard !originalOnlyText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let languageRecognizer = NLLanguageRecognizer()
|
||||
languageRecognizer.processString(originalOnlyText)
|
||||
|
||||
// Only accept language recognition hypothesis if there's at least a 50% probability that it's accurate.
|
||||
return languageRecognizer.languageHypotheses(withMaximum: 1).first(where: { $0.value >= 0.5 })?.key
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,37 +8,96 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
// Concatening too many `Text` objects can cause crashes (See https://github.com/damus-io/damus/issues/1826)
|
||||
fileprivate let MAX_TEXT_ITEMS = 100
|
||||
|
||||
class CompatibleText: Equatable {
|
||||
var text: Text
|
||||
var attributed: AttributedString
|
||||
|
||||
var text: some View {
|
||||
if items.count > MAX_TEXT_ITEMS {
|
||||
return AnyView(
|
||||
VStack {
|
||||
Image("warning")
|
||||
Text(NSLocalizedString("This note contains too many items and cannot be rendered", comment: "Error message indicating that a note is too big and cannot be rendered"))
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
)
|
||||
}
|
||||
return AnyView(
|
||||
items.reduce(Text(""), { (accumulated, item) in
|
||||
return accumulated + item.render_to_text()
|
||||
})
|
||||
)
|
||||
}
|
||||
var attributed: AttributedString {
|
||||
return items.reduce(AttributedString(stringLiteral: ""), { (accumulated, item) in
|
||||
guard let item_attributed_string = item.attributed_string() else { return accumulated }
|
||||
return accumulated + item_attributed_string
|
||||
})
|
||||
}
|
||||
var items: [Item]
|
||||
|
||||
init() {
|
||||
self.text = Text("")
|
||||
self.attributed = AttributedString(stringLiteral: "")
|
||||
self.items = [.attributed_string(AttributedString(stringLiteral: ""))]
|
||||
}
|
||||
|
||||
|
||||
init(stringLiteral: String) {
|
||||
self.text = Text(stringLiteral)
|
||||
self.attributed = AttributedString(stringLiteral: stringLiteral)
|
||||
self.items = [.attributed_string(AttributedString(stringLiteral: stringLiteral))]
|
||||
}
|
||||
|
||||
init(text: Text, attributed: AttributedString) {
|
||||
self.text = text
|
||||
self.attributed = attributed
|
||||
}
|
||||
|
||||
|
||||
init(attributed: AttributedString) {
|
||||
self.text = Text(attributed)
|
||||
self.attributed = attributed
|
||||
self.items = [.attributed_string(attributed)]
|
||||
}
|
||||
|
||||
|
||||
init(items: [Item]) {
|
||||
self.items = items
|
||||
}
|
||||
|
||||
static func == (lhs: CompatibleText, rhs: CompatibleText) -> Bool {
|
||||
return lhs.attributed == rhs.attributed
|
||||
return lhs.items == rhs.items
|
||||
}
|
||||
|
||||
|
||||
static func +(lhs: CompatibleText, rhs: CompatibleText) -> CompatibleText {
|
||||
let combinedText = lhs.text + rhs.text
|
||||
let combinedAttributes = lhs.attributed + rhs.attributed
|
||||
return CompatibleText(text: combinedText, attributed: combinedAttributes)
|
||||
if case .attributed_string(let las) = lhs.items.last,
|
||||
case .attributed_string(let ras) = rhs.items.first
|
||||
{
|
||||
// Concatenate attributed strings whenever possible to reduce item count
|
||||
let combined_attributed_string = las + ras
|
||||
return CompatibleText(items:
|
||||
Array(lhs.items.prefix(upTo: lhs.items.count - 1)) +
|
||||
[.attributed_string(combined_attributed_string)] +
|
||||
Array(rhs.items.suffix(from: 1))
|
||||
)
|
||||
}
|
||||
else {
|
||||
return CompatibleText(items: lhs.items + rhs.items)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension CompatibleText {
|
||||
enum Item: Equatable {
|
||||
case attributed_string(AttributedString)
|
||||
case icon(named: String, offset: CGFloat)
|
||||
|
||||
func render_to_text() -> Text {
|
||||
switch self {
|
||||
case .attributed_string(let attributed_string):
|
||||
return Text(attributed_string)
|
||||
case .icon(named: let image_name, offset: let offset):
|
||||
return Text(Image(image_name)).baselineOffset(offset)
|
||||
}
|
||||
}
|
||||
|
||||
func attributed_string() -> AttributedString? {
|
||||
switch self {
|
||||
case .attributed_string(let attributed_string):
|
||||
return attributed_string
|
||||
case .icon(named: let name, offset: _):
|
||||
guard let img = UIImage(named: name) else { return nil }
|
||||
return icon_attributed_string(img: img)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,9 @@
|
||||
import Foundation
|
||||
|
||||
class Constants {
|
||||
static let PURPLE_API_PRODUCTION_BASE_URL: URL = URL(string: "https://purple.damus.io")!
|
||||
static let PURPLE_API_TEST_BASE_URL: URL = URL(string: "http://127.0.0.1:8989")!
|
||||
static let DEVICE_TOKEN_RECEIVER_PRODUCTION_URL: URL = URL(string: "https://notify.damus.io:8000/user-info")!
|
||||
static let DEVICE_TOKEN_RECEIVER_TEST_URL: URL = URL(string: "http://localhost:8000/user-info")!
|
||||
static let EXAMPLE_DEMOS: DamusState = .empty
|
||||
}
|
||||
|
||||
@@ -306,7 +306,6 @@ func should_translate(event: NostrEvent, our_keypair: Keypair, settings: UserSet
|
||||
}
|
||||
|
||||
func should_preload_translation(event: NostrEvent, our_keypair: Keypair, current_status: TranslateStatus, settings: UserSettingsStore, note_lang: String?) -> Bool {
|
||||
|
||||
switch current_status {
|
||||
case .havent_tried:
|
||||
return should_translate(event: event, our_keypair: our_keypair, settings: settings, note_lang: note_lang) && settings.auto_translate
|
||||
@@ -445,7 +444,7 @@ func preload_event(plan: PreloadPlan, state: DamusState) async {
|
||||
// We have to recheck should_translate here now that we have note_language
|
||||
if plan.load_translations && should_translate(event: plan.event, our_keypair: our_keypair, settings: settings, note_lang: note_language) && settings.auto_translate
|
||||
{
|
||||
translations = await translate_note(profiles: profiles, keypair: our_keypair, event: plan.event, settings: settings, note_lang: note_language)
|
||||
translations = await translate_note(profiles: profiles, keypair: our_keypair, event: plan.event, settings: settings, note_lang: note_language, purple: state.purple)
|
||||
}
|
||||
|
||||
let ts = translations
|
||||
|
||||
@@ -43,28 +43,20 @@ let custom_hashtags: [String: CustomHashtag] = [
|
||||
func hashtag_str(_ htag: String) -> CompatibleText {
|
||||
var attributedString = AttributedString(stringLiteral: "#\(htag)")
|
||||
attributedString.link = URL(string: "damus:t:\(htag.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed) ?? htag)")
|
||||
|
||||
|
||||
let lowertag = htag.lowercased()
|
||||
|
||||
var text = Text(attributedString)
|
||||
|
||||
if let custom_hashtag = custom_hashtags[lowertag] {
|
||||
if let col = custom_hashtag.color {
|
||||
attributedString.foregroundColor = col
|
||||
}
|
||||
|
||||
|
||||
let name = custom_hashtag.name
|
||||
|
||||
if let img = UIImage(named: "\(name)-hashtag") {
|
||||
attributedString = attributedString + " "
|
||||
attributed_string_attach_icon(&attributedString, img: img)
|
||||
}
|
||||
text = Text(attributedString)
|
||||
let img = Image("\(name)-hashtag")
|
||||
text = text + Text(img).baselineOffset(custom_hashtag.offset ?? 0.0)
|
||||
|
||||
attributedString = attributedString + " "
|
||||
return CompatibleText(items: [.attributed_string(attributedString), .icon(named: "\(name)-hashtag", offset: custom_hashtag.offset ?? 0.0)])
|
||||
} else {
|
||||
attributedString.foregroundColor = DamusColors.purple
|
||||
return CompatibleText(items: [.attributed_string(attributedString)])
|
||||
}
|
||||
|
||||
return CompatibleText(text: text, attributed: attributedString)
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ enum LogCategory: String {
|
||||
case render
|
||||
case storage
|
||||
case push_notifications
|
||||
case damus_purple
|
||||
}
|
||||
|
||||
/// Damus structured logger
|
||||
|
||||
@@ -7,14 +7,15 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
let BOOTSTRAP_RELAYS = [
|
||||
// This is `fileprivate` because external code should use the `get_default_bootstrap_relays` instead.
|
||||
fileprivate let BOOTSTRAP_RELAYS = [
|
||||
"wss://relay.damus.io",
|
||||
"wss://eden.nostr.land",
|
||||
"wss://nostr.wine",
|
||||
"wss://nos.lol",
|
||||
]
|
||||
|
||||
let REGION_SPECIFIC_BOOTSTRAP_RELAYS: [Locale.Region: [String]] = [
|
||||
fileprivate let REGION_SPECIFIC_BOOTSTRAP_RELAYS: [Locale.Region: [String]] = [
|
||||
Locale.Region.japan: [
|
||||
"wss://relay-jp.nostr.wirednet.jp",
|
||||
"wss://yabu.me",
|
||||
|
||||
@@ -81,7 +81,7 @@ enum Route: Hashable {
|
||||
case .ZapSettings(let settings):
|
||||
ZapSettingsView(settings: settings)
|
||||
case .TranslationSettings(let settings):
|
||||
TranslationSettingsView(settings: settings)
|
||||
TranslationSettingsView(settings: settings, damus_state: damusState)
|
||||
case .ReactionsSettings(let settings):
|
||||
ReactionsSettingsView(settings: settings)
|
||||
case .SearchSettings(let settings):
|
||||
|
||||
@@ -12,16 +12,30 @@ import FoundationNetworking
|
||||
|
||||
public struct Translator {
|
||||
private let userSettingsStore: UserSettingsStore
|
||||
private let purple: DamusPurple
|
||||
private let session = URLSession.shared
|
||||
private let encoder = JSONEncoder()
|
||||
private let decoder = JSONDecoder()
|
||||
|
||||
init(_ userSettingsStore: UserSettingsStore) {
|
||||
init(_ userSettingsStore: UserSettingsStore, purple: DamusPurple) {
|
||||
self.userSettingsStore = userSettingsStore
|
||||
self.purple = purple
|
||||
}
|
||||
|
||||
/// Returns true if the translator should attempt to translate from the source language to the target language and valid translation service settings are configured.
|
||||
func shouldTranslate(from sourceLanguage: String, to targetLanguage: String) -> Bool {
|
||||
return sourceLanguage != targetLanguage && userSettingsStore.can_translate
|
||||
}
|
||||
|
||||
public func translate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
|
||||
// Do not attempt to translate if the source and target languages are the same.
|
||||
guard shouldTranslate(from: sourceLanguage, to: targetLanguage) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch userSettingsStore.translation_service {
|
||||
case .purple:
|
||||
return try await translateWithPurple(text, from: sourceLanguage, to: targetLanguage)
|
||||
case .libretranslate:
|
||||
return try await translateWithLibreTranslate(text, from: sourceLanguage, to: targetLanguage)
|
||||
case .nokyctranslate:
|
||||
@@ -31,7 +45,7 @@ public struct Translator {
|
||||
case .deepl:
|
||||
return try await translateWithDeepL(text, from: sourceLanguage, to: targetLanguage)
|
||||
case .none:
|
||||
return text
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +104,10 @@ public struct Translator {
|
||||
return response.translations.map { $0.text }.joined(separator: " ")
|
||||
}
|
||||
|
||||
private func translateWithPurple(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
|
||||
return try await self.purple.translate(text: text, source: sourceLanguage, target: targetLanguage)
|
||||
}
|
||||
|
||||
private func translateWithNoKYCTranslate(_ text: String, from sourceLanguage: String, to targetLanguage: String) async throws -> String? {
|
||||
let url = try makeURL("https://translate.nokyctranslate.com", path: "/translate")
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import Combine
|
||||
struct DMChatView: View, KeyboardReadable {
|
||||
let damus_state: DamusState
|
||||
@ObservedObject var dms: DirectMessageModel
|
||||
@State var showPrivateKeyWarning: Bool = false
|
||||
|
||||
var pubkey: Pubkey {
|
||||
dms.pubkey
|
||||
@@ -106,11 +105,7 @@ struct DMChatView: View, KeyboardReadable {
|
||||
Button(
|
||||
role: .none,
|
||||
action: {
|
||||
showPrivateKeyWarning = contentContainsPrivateKey(dms.draft)
|
||||
|
||||
if !showPrivateKeyWarning {
|
||||
send_message()
|
||||
}
|
||||
send_message()
|
||||
}
|
||||
) {
|
||||
Label("", image: "send")
|
||||
@@ -165,14 +160,6 @@ struct DMChatView: View, KeyboardReadable {
|
||||
dms.draft = ""
|
||||
}
|
||||
}
|
||||
.alert(NSLocalizedString("Note contains \"nsec1\" private key. Are you sure?", comment: "Alert user that they might be attempting to paste a private key and ask them to confirm."), isPresented: $showPrivateKeyWarning, actions: {
|
||||
Button(NSLocalizedString("No", comment: "Button to cancel out of posting a note after being alerted that it looks like they might be posting a private key."), role: .cancel) {
|
||||
showPrivateKeyWarning = false
|
||||
}
|
||||
Button(NSLocalizedString("Yes, Post with Private Key", comment: "Button to proceed with posting a note even though it looks like they might be posting a private key."), role: .destructive) {
|
||||
send_message()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -39,13 +39,13 @@ struct DMView: View {
|
||||
Spacer(minLength: UIScreen.main.bounds.width * 0.2)
|
||||
}
|
||||
|
||||
let should_show_img = should_show_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
|
||||
let should_blur_img = should_blur_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
|
||||
|
||||
VStack(alignment: .trailing) {
|
||||
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: .normal, options: dm_options)
|
||||
NoteContentView(damus_state: damus_state, event: event, blur_images: should_blur_img, size: .normal, options: dm_options)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.padding([.top, .leading, .trailing], 10)
|
||||
.padding([.bottom], 25)
|
||||
.padding([.bottom], 10)
|
||||
.background(VisualEffectView(effect: UIBlurEffect(style: .prominent))
|
||||
.background(is_ours ? Color.accentColor.opacity(0.9) : Color.secondary.opacity(0.15))
|
||||
)
|
||||
|
||||
@@ -18,7 +18,6 @@ struct DirectMessagesView: View {
|
||||
@State var dm_type: DMType = .friend
|
||||
@ObservedObject var model: DirectMessagesModel
|
||||
@ObservedObject var settings: UserSettingsStore
|
||||
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home
|
||||
|
||||
func MainContent(requests: Bool) -> some View {
|
||||
ScrollView {
|
||||
@@ -91,12 +90,10 @@ struct DirectMessagesView: View {
|
||||
.tabViewStyle(.page(indexDisplayMode: .never))
|
||||
}
|
||||
.toolbar {
|
||||
if selected_timeline == .dms {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
if would_filter_non_friends_from_dms(contacts: damus_state.contacts, dms: self.model.dms) {
|
||||
|
||||
FriendsButton(filter: $settings.friend_filter)
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
if would_filter_non_friends_from_dms(contacts: damus_state.contacts, dms: self.model.dms) {
|
||||
|
||||
FriendsButton(filter: $settings.friend_filter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,21 +54,21 @@ struct EventView: View {
|
||||
}
|
||||
|
||||
// blame the porn bots for this code
|
||||
func should_show_images(settings: UserSettingsStore, contacts: Contacts, ev: NostrEvent, our_pubkey: Pubkey, booster_pubkey: Pubkey? = nil) -> Bool {
|
||||
if settings.always_show_images {
|
||||
return true
|
||||
func should_blur_images(settings: UserSettingsStore, contacts: Contacts, ev: NostrEvent, our_pubkey: Pubkey, booster_pubkey: Pubkey? = nil) -> Bool {
|
||||
if !settings.blur_images {
|
||||
return false
|
||||
}
|
||||
|
||||
if ev.pubkey == our_pubkey {
|
||||
return true
|
||||
return false
|
||||
}
|
||||
if contacts.is_in_friendosphere(ev.pubkey) {
|
||||
return true
|
||||
return false
|
||||
}
|
||||
if let boost_key = booster_pubkey, contacts.is_in_friendosphere(boost_key) {
|
||||
return true
|
||||
return false
|
||||
}
|
||||
return false
|
||||
return true
|
||||
}
|
||||
|
||||
func format_relative_time(_ created_at: UInt32) -> String
|
||||
|
||||
@@ -11,19 +11,19 @@ struct EventBody: View {
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
let size: EventViewKind
|
||||
let should_show_img: Bool
|
||||
let should_blur_img: Bool
|
||||
let options: EventViewOptions
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind, should_show_img: Bool? = nil, options: EventViewOptions) {
|
||||
init(damus_state: DamusState, event: NostrEvent, size: EventViewKind, should_blur_img: Bool? = nil, options: EventViewOptions) {
|
||||
self.damus_state = damus_state
|
||||
self.event = event
|
||||
self.size = size
|
||||
self.options = options
|
||||
self.should_show_img = should_show_img ?? should_show_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
|
||||
self.should_blur_img = should_blur_img ?? should_blur_images(settings: damus_state.settings, contacts: damus_state.contacts, ev: event, our_pubkey: damus_state.pubkey)
|
||||
}
|
||||
|
||||
var note_content: some View {
|
||||
NoteContentView(damus_state: damus_state, event: event, show_images: should_show_img, size: size, options: options)
|
||||
NoteContentView(damus_state: damus_state, event: event, blur_images: should_blur_img, size: size, options: options)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ struct LongformView: View {
|
||||
EventShell(state: state, event: event.event, options: options) {
|
||||
SelectableText(attributedString: AttributedString(stringLiteral: event.title ?? "Untitled"), size: .title)
|
||||
|
||||
NoteContentView(damus_state: state, event: event.event, show_images: true, size: .selected, options: options)
|
||||
NoteContentView(damus_state: state, event: event.event, blur_images: false, size: .selected, options: options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,11 +45,11 @@ struct TextEvent: View {
|
||||
}
|
||||
|
||||
func EvBody(options: EventViewOptions) -> some View {
|
||||
let show_imgs = should_show_images(settings: damus.settings, contacts: damus.contacts, ev: event, our_pubkey: damus.pubkey)
|
||||
let blur_imgs = should_blur_images(settings: damus.settings, contacts: damus.contacts, ev: event, our_pubkey: damus.pubkey)
|
||||
return NoteContentView(
|
||||
damus_state: damus,
|
||||
event: event,
|
||||
show_images: show_imgs,
|
||||
blur_images: blur_imgs,
|
||||
size: .normal,
|
||||
options: options
|
||||
)
|
||||
|
||||
@@ -87,7 +87,6 @@ struct LoginView: View {
|
||||
}
|
||||
|
||||
if let p = parsed {
|
||||
|
||||
Button(action: {
|
||||
Task {
|
||||
do {
|
||||
@@ -361,7 +360,7 @@ struct SignInEntry: View {
|
||||
shouldSaveKey: shouldSaveKey,
|
||||
privKeyFound: $privKeyFound)
|
||||
if privKeyFound {
|
||||
Toggle("Save Key in Secure Keychain", isOn: shouldSaveKey)
|
||||
Toggle(NSLocalizedString("Save Key in Secure Keychain", comment: "Toggle to save private key to the Apple secure keychain."), isOn: shouldSaveKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ struct NoteContentView: View {
|
||||
|
||||
let damus_state: DamusState
|
||||
let event: NostrEvent
|
||||
@State var show_images: Bool
|
||||
@State var blur_images: Bool
|
||||
@State var load_media: Bool = false
|
||||
let size: EventViewKind
|
||||
let preview_height: CGFloat?
|
||||
@@ -40,10 +40,10 @@ struct NoteContentView: View {
|
||||
return self.artifacts_model.state.artifacts ?? .separated(.just_content(event.get_content(damus_state.keypair)))
|
||||
}
|
||||
|
||||
init(damus_state: DamusState, event: NostrEvent, show_images: Bool, size: EventViewKind, options: EventViewOptions) {
|
||||
init(damus_state: DamusState, event: NostrEvent, blur_images: Bool, size: EventViewKind, options: EventViewOptions) {
|
||||
self.damus_state = damus_state
|
||||
self.event = event
|
||||
self.show_images = show_images
|
||||
self.blur_images = blur_images
|
||||
self.size = size
|
||||
self.options = options
|
||||
self.preview_height = lookup_cached_preview_size(previews: damus_state.previews, evid: event.id)
|
||||
@@ -62,7 +62,7 @@ struct NoteContentView: View {
|
||||
}
|
||||
|
||||
var preview: LinkViewRepresentable? {
|
||||
guard show_images,
|
||||
guard blur_images,
|
||||
case .loaded(let preview) = preview_model.state,
|
||||
case .value(let cached) = preview else {
|
||||
return nil
|
||||
@@ -93,7 +93,7 @@ struct NoteContentView: View {
|
||||
|
||||
func previewView(links: [URL]) -> some View {
|
||||
Group {
|
||||
if let preview = self.preview, show_images {
|
||||
if let preview = self.preview, blur_images {
|
||||
if let preview_height {
|
||||
preview
|
||||
.frame(height: preview_height)
|
||||
@@ -133,18 +133,18 @@ struct NoteContentView: View {
|
||||
translateView
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if artifacts.media.count > 0 {
|
||||
if !damus_state.settings.media_previews && !load_media {
|
||||
loadMediaButton(artifacts: artifacts)
|
||||
} else if show_images || (show_images && !damus_state.settings.media_previews && load_media) {
|
||||
} else if !blur_images || (!blur_images && !damus_state.settings.media_previews && load_media) {
|
||||
ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.media)
|
||||
} else if !show_images || (!show_images && !damus_state.settings.media_previews && load_media) {
|
||||
} else if blur_images || (blur_images && !damus_state.settings.media_previews && load_media) {
|
||||
ZStack {
|
||||
ImageCarousel(state: damus_state, evid: event.id, urls: artifacts.media)
|
||||
Blur()
|
||||
.onTapGesture {
|
||||
show_images = true
|
||||
blur_images = false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,7 +188,7 @@ struct NoteContentView: View {
|
||||
.frame(height: 1)
|
||||
switch artifacts.media[index] {
|
||||
case .image(let url), .video(let url):
|
||||
Text("\(url)")
|
||||
Text(url.absoluteString)
|
||||
.font(eventviewsize_to_font(size, font_size: damus_state.settings.font_size))
|
||||
.foregroundStyle(DamusColors.neutral6)
|
||||
.multilineTextAlignment(.leading)
|
||||
@@ -208,6 +208,10 @@ struct NoteContentView: View {
|
||||
}
|
||||
|
||||
func load(force_artifacts: Bool = false) {
|
||||
if case .loading = damus_state.events.get_cache_data(event.id).artifacts_model.state {
|
||||
return
|
||||
}
|
||||
|
||||
// always reload artifacts on load
|
||||
let plan = get_preload_plan(evcache: damus_state.events, ev: event, our_keypair: damus_state.keypair, settings: damus_state.settings)
|
||||
|
||||
@@ -297,11 +301,15 @@ struct NoteContentView: View {
|
||||
}
|
||||
|
||||
func attributed_string_attach_icon(_ astr: inout AttributedString, img: UIImage) {
|
||||
let wrapped = icon_attributed_string(img: img)
|
||||
astr.append(wrapped)
|
||||
}
|
||||
|
||||
func icon_attributed_string(img: UIImage) -> AttributedString {
|
||||
let attachment = NSTextAttachment()
|
||||
attachment.image = img
|
||||
let attachmentString = NSAttributedString(attachment: attachment)
|
||||
let wrapped = AttributedString(attachmentString)
|
||||
astr.append(wrapped)
|
||||
return AttributedString(attachmentString)
|
||||
}
|
||||
|
||||
func url_str(_ url: URL) -> CompatibleText {
|
||||
@@ -661,17 +669,17 @@ struct NoteContentView_Previews: PreviewProvider {
|
||||
|
||||
Group {
|
||||
VStack {
|
||||
NoteContentView(damus_state: state, event: test_note, show_images: true, size: .normal, options: [])
|
||||
NoteContentView(damus_state: state, event: test_note, blur_images: false, size: .normal, options: [])
|
||||
}
|
||||
.previewDisplayName("Short note")
|
||||
|
||||
VStack {
|
||||
NoteContentView(damus_state: state, event: test_encoded_note_with_image!, show_images: true, size: .normal, options: [])
|
||||
NoteContentView(damus_state: state, event: test_encoded_note_with_image!, blur_images: false, size: .normal, options: [])
|
||||
}
|
||||
.previewDisplayName("Note with image")
|
||||
|
||||
VStack {
|
||||
NoteContentView(damus_state: state2, event: test_longform_event.event, show_images: true, size: .normal, options: [.wide])
|
||||
NoteContentView(damus_state: state2, event: test_longform_event.event, blur_images: false, size: .normal, options: [.wide])
|
||||
.border(Color.red)
|
||||
}
|
||||
.previewDisplayName("Long-form note")
|
||||
|
||||
@@ -82,8 +82,7 @@ struct NotificationsView: View {
|
||||
@ObservedObject var notifications: NotificationsModel
|
||||
@StateObject var filter = NotificationFilter()
|
||||
@SceneStorage("NotificationsView.filter_state") var filter_state: NotificationFilterState = .all
|
||||
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home
|
||||
|
||||
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
var mystery: some View {
|
||||
|
||||
@@ -97,6 +97,9 @@ class SuggestedUsersViewModel: ObservableObject {
|
||||
|
||||
case .ok:
|
||||
break
|
||||
|
||||
case .auth:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@ enum PostAction {
|
||||
struct PostView: View {
|
||||
@State var post: NSMutableAttributedString = NSMutableAttributedString()
|
||||
@FocusState var focus: Bool
|
||||
@State var showPrivateKeyWarning: Bool = false
|
||||
@State var attach_media: Bool = false
|
||||
@State var attach_camera: Bool = false
|
||||
@State var error: String? = nil
|
||||
@@ -159,11 +158,7 @@ struct PostView: View {
|
||||
|
||||
var PostButton: some View {
|
||||
Button(NSLocalizedString("Post", comment: "Button to post a note.")) {
|
||||
showPrivateKeyWarning = contentContainsPrivateKey(self.post.string)
|
||||
|
||||
if !showPrivateKeyWarning {
|
||||
self.send_post()
|
||||
}
|
||||
self.send_post()
|
||||
}
|
||||
.disabled(posting_disabled)
|
||||
.opacity(posting_disabled ? 0.5 : 1.0)
|
||||
@@ -480,14 +475,6 @@ struct PostView: View {
|
||||
clear_draft()
|
||||
}
|
||||
}
|
||||
.alert(NSLocalizedString("Note contains \"nsec1\" private key. Are you sure?", comment: "Alert user that they might be attempting to paste a private key and ask them to confirm."), isPresented: $showPrivateKeyWarning, actions: {
|
||||
Button(NSLocalizedString("No", comment: "Button to cancel out of posting a note after being alerted that it looks like they might be posting a private key."), role: .cancel) {
|
||||
showPrivateKeyWarning = false
|
||||
}
|
||||
Button(NSLocalizedString("Yes, Post with Private Key", comment: "Button to proceed with posting a note even though it looks like they might be posting a private key."), role: .destructive) {
|
||||
self.send_post()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,12 +10,13 @@ import SwiftUI
|
||||
/// Profile Name used when displaying an event in the timeline
|
||||
@MainActor
|
||||
struct EventProfileName: View {
|
||||
let damus_state: DamusState
|
||||
var damus_state: DamusState
|
||||
let pubkey: Pubkey
|
||||
|
||||
@State var display_name: DisplayName?
|
||||
@State var nip05: NIP05?
|
||||
@State var donation: Int?
|
||||
@State var is_purple_user: Bool?
|
||||
|
||||
let size: EventViewKind
|
||||
|
||||
@@ -25,6 +26,7 @@ struct EventProfileName: View {
|
||||
self.size = size
|
||||
let donation = damus.ndb.lookup_profile(pubkey).map({ p in p?.profile?.damus_donation }).value
|
||||
self._donation = State(wrappedValue: donation)
|
||||
is_purple_user = nil
|
||||
}
|
||||
|
||||
var friend_type: FriendType? {
|
||||
@@ -47,7 +49,12 @@ struct EventProfileName: View {
|
||||
return profile.reactions == false
|
||||
}
|
||||
|
||||
var supporter: Int? {
|
||||
func supporter_percentage() -> Int? {
|
||||
if damus_state.settings.enable_experimental_purple_api,
|
||||
is_purple_user == true {
|
||||
return 100
|
||||
}
|
||||
|
||||
guard let donation, donation > 0
|
||||
else {
|
||||
return nil
|
||||
@@ -92,7 +99,7 @@ struct EventProfileName: View {
|
||||
.frame(width: 14, height: 14)
|
||||
}
|
||||
|
||||
if let supporter {
|
||||
if let supporter = self.supporter_percentage() {
|
||||
SupporterBadge(percent: supporter)
|
||||
}
|
||||
}
|
||||
@@ -119,6 +126,13 @@ struct EventProfileName: View {
|
||||
donation = profile.damus_donation
|
||||
}
|
||||
}
|
||||
.onAppear(perform: {
|
||||
Task {
|
||||
if damus_state.settings.enable_experimental_purple_api {
|
||||
is_purple_user = await damus_state.purple.is_profile_subscribed_to_purple(pubkey: self.pubkey) ?? false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ struct ProfileActionSheetView: View {
|
||||
.profile_button_style(scheme: colorScheme)
|
||||
}
|
||||
)
|
||||
.buttonStyle(NeutralCircleButtonStyle())
|
||||
.buttonStyle(NeutralButtonShape.circle.style)
|
||||
Text(NSLocalizedString("Message", comment: "Button label that allows the user to start a direct message conversation with the user shown on-screen"))
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.caption)
|
||||
@@ -121,8 +121,7 @@ struct ProfileActionSheetView: View {
|
||||
|
||||
}
|
||||
)
|
||||
|
||||
.buttonStyle(NeutralCircleButtonStyle())
|
||||
.buttonStyle(NeutralButtonShape.circle.style)
|
||||
}
|
||||
.padding()
|
||||
.padding(.top, 20)
|
||||
@@ -165,7 +164,7 @@ fileprivate struct ProfileActionSheetFollowButton: View {
|
||||
|
||||
}
|
||||
)
|
||||
.buttonStyle(NeutralCircleButtonStyle())
|
||||
.buttonStyle(NeutralButtonShape.circle.style)
|
||||
|
||||
Text(verbatim: "\(follow_btn_txt(follow_state, follows_you: follows_you))")
|
||||
.foregroundStyle(.secondary)
|
||||
@@ -292,7 +291,7 @@ fileprivate struct ProfileActionSheetZapButton: View {
|
||||
return true
|
||||
}
|
||||
}())
|
||||
.buttonStyle(NeutralCircleButtonStyle())
|
||||
.buttonStyle(NeutralButtonShape.circle.style)
|
||||
|
||||
Text(button_label)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
//
|
||||
// DamusPurpleView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by William Casarin on 2023-03-21.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import StoreKit
|
||||
|
||||
fileprivate let damus_products = ["purpleyearly","purple"]
|
||||
|
||||
enum ProductState {
|
||||
case loading
|
||||
case loaded([Product])
|
||||
case failed
|
||||
|
||||
var products: [Product]? {
|
||||
switch self {
|
||||
case .loading:
|
||||
return nil
|
||||
case .loaded(let ps):
|
||||
return ps
|
||||
case .failed:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func non_discounted_price(_ product: Product) -> String {
|
||||
return (product.price * 1.1984569224).formatted(product.priceFormatStyle)
|
||||
}
|
||||
|
||||
enum DamusPurpleType: String {
|
||||
case yearly = "purpleyearly"
|
||||
case monthly = "purple"
|
||||
}
|
||||
|
||||
struct PurchasedProduct {
|
||||
let tx: StoreKit.Transaction
|
||||
let product: Product
|
||||
}
|
||||
|
||||
struct DamusPurpleView: View {
|
||||
let damus_state: DamusState
|
||||
let keypair: Keypair
|
||||
|
||||
@State var products: ProductState
|
||||
@State var purchased: PurchasedProduct? = nil
|
||||
@State var selection: DamusPurpleType = .yearly
|
||||
@State var show_welcome_sheet: Bool = false
|
||||
@State var show_manage_subscriptions = false
|
||||
@State var show_settings_change_confirmation_dialog = false
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
init(damus_state: DamusState) {
|
||||
self._products = State(wrappedValue: .loading)
|
||||
self.damus_state = damus_state
|
||||
self.keypair = damus_state.keypair
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.background(.black)
|
||||
|
||||
ScrollView {
|
||||
MainContent
|
||||
.padding(.top, 75)
|
||||
.background(content: {
|
||||
ZStack {
|
||||
Image("purple-blue-gradient-1")
|
||||
.offset(CGSize(width: 300.0, height: -0.0))
|
||||
Image("purple-blue-gradient-1")
|
||||
.offset(CGSize(width: 300.0, height: -0.0))
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarBackButtonHidden(true)
|
||||
.navigationBarItems(leading: BackNav())
|
||||
.onReceive(handle_notify(.switched_timeline)) { _ in
|
||||
dismiss()
|
||||
}
|
||||
.onAppear {
|
||||
notify(.display_tabbar(false))
|
||||
}
|
||||
.onDisappear {
|
||||
notify(.display_tabbar(true))
|
||||
}
|
||||
.task {
|
||||
await load_products()
|
||||
}
|
||||
.ignoresSafeArea(.all)
|
||||
.sheet(isPresented: $show_welcome_sheet, onDismiss: {
|
||||
update_user_settings_to_purple()
|
||||
}, content: {
|
||||
DamusPurpleWelcomeView()
|
||||
})
|
||||
.confirmationDialog(
|
||||
NSLocalizedString("It seems that you already have a translation service configured. Would you like to switch to Damus Purple as your translator?", comment: "Confirmation dialog question asking users if they want their translation settings to be automatically switched to the Damus Purple translation service"),
|
||||
isPresented: $show_settings_change_confirmation_dialog,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button(NSLocalizedString("Yes", comment: "User confirm Yes")) {
|
||||
set_translation_settings_to_purple()
|
||||
}.keyboardShortcut(.defaultAction)
|
||||
Button(NSLocalizedString("No", comment: "User confirm No"), role: .cancel) {}
|
||||
}
|
||||
.manageSubscriptionsSheet(isPresented: $show_manage_subscriptions)
|
||||
}
|
||||
|
||||
func update_user_settings_to_purple() {
|
||||
if damus_state.settings.translation_service == .none {
|
||||
set_translation_settings_to_purple()
|
||||
}
|
||||
else {
|
||||
show_settings_change_confirmation_dialog = true
|
||||
}
|
||||
}
|
||||
|
||||
func set_translation_settings_to_purple() {
|
||||
damus_state.settings.translation_service = .purple
|
||||
damus_state.settings.auto_translate = true
|
||||
}
|
||||
|
||||
func handle_transactions(products: [Product]) async {
|
||||
for await update in StoreKit.Transaction.updates {
|
||||
switch update {
|
||||
case .verified(let tx):
|
||||
let prod = products.filter({ prod in tx.productID == prod.id }).first
|
||||
|
||||
if let prod,
|
||||
let expiration = tx.expirationDate,
|
||||
Date.now < expiration
|
||||
{
|
||||
self.purchased = PurchasedProduct(tx: tx, product: prod)
|
||||
break
|
||||
}
|
||||
case .unverified:
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func load_products() async {
|
||||
do {
|
||||
let products = try await Product.products(for: damus_products)
|
||||
self.products = .loaded(products)
|
||||
await handle_transactions(products: products)
|
||||
|
||||
print("loaded products", products)
|
||||
} catch {
|
||||
self.products = .failed
|
||||
print("Failed to fetch products: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
func IconOnBox(_ name: String) -> some View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 20.0)
|
||||
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 20.0))
|
||||
.frame(width: 80, height: 80)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.stroke(LinearGradient(
|
||||
colors: [DamusColors.pink, .white.opacity(0), .white.opacity(0.5), .white.opacity(0)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing), lineWidth: 1)
|
||||
)
|
||||
|
||||
Image(name)
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
|
||||
func Icon(_ name: String) -> some View {
|
||||
Image(name)
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
func Title(_ txt: String) -> some View {
|
||||
Text(txt)
|
||||
.font(.title3)
|
||||
.bold()
|
||||
.foregroundColor(.white)
|
||||
.padding(.bottom, 3)
|
||||
}
|
||||
|
||||
func Subtitle(_ txt: String) -> some View {
|
||||
Text(txt)
|
||||
.foregroundColor(.white.opacity(0.65))
|
||||
}
|
||||
|
||||
var ProductLoadError: some View {
|
||||
Text(NSLocalizedString("Subscription Error", comment: "Ah dang there was an error loading subscription information from the AppStore. Please try again later :("))
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
|
||||
var SaveText: Text {
|
||||
Text(NSLocalizedString("Save 14%", comment: "Percentage of purchase price the user will save"))
|
||||
.font(.callout)
|
||||
.italic()
|
||||
.foregroundColor(DamusColors.green)
|
||||
}
|
||||
|
||||
func subscribe(_ product: Product) async throws {
|
||||
let result = try await product.purchase()
|
||||
switch result {
|
||||
case .success(.verified(let tx)):
|
||||
print("success \(tx.debugDescription)")
|
||||
show_welcome_sheet = true
|
||||
case .success(.unverified(let tx, let res)):
|
||||
print("success unverified \(tx.debugDescription) \(res.localizedDescription)")
|
||||
show_welcome_sheet = true
|
||||
case .pending:
|
||||
break
|
||||
case .userCancelled:
|
||||
break
|
||||
@unknown default:
|
||||
break
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
self.damus_state.purple.starred_profiles_cache[keypair.pubkey] = nil
|
||||
Task {
|
||||
await self.damus_state.purple.send_receipt()
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var product: Product? {
|
||||
return self.products.products?.filter({
|
||||
prod in prod.id == selection.rawValue
|
||||
}).first
|
||||
}
|
||||
|
||||
func price_description(product: Product) -> some View {
|
||||
if product.id == "purpleyearly" {
|
||||
return (
|
||||
AnyView(
|
||||
HStack(spacing: 10) {
|
||||
Text(NSLocalizedString("Annually", comment: "Annual renewal of purple subscription"))
|
||||
Spacer()
|
||||
Text(verbatim: non_discounted_price(product)).strikethrough().foregroundColor(DamusColors.white.opacity(0.5))
|
||||
Text(verbatim: product.displayPrice).fontWeight(.bold)
|
||||
}
|
||||
)
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
AnyView(
|
||||
HStack(spacing: 10) {
|
||||
Text(NSLocalizedString("Monthly", comment: "Monthly renewal of purple subscription"))
|
||||
Spacer()
|
||||
Text(verbatim: product.displayPrice).fontWeight(.bold)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func ProductsView(_ products: [Product]) -> some View {
|
||||
VStack(spacing: 10) {
|
||||
Text(NSLocalizedString("Save 20% off on an annual subscription", comment: "Savings for purchasing an annual subscription"))
|
||||
.font(.callout.bold())
|
||||
.foregroundColor(.white)
|
||||
ForEach(products) { product in
|
||||
Button(action: {
|
||||
Task { @MainActor in
|
||||
do {
|
||||
try await subscribe(product)
|
||||
} catch {
|
||||
print(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}, label: {
|
||||
price_description(product: product)
|
||||
})
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
|
||||
func PurchasedView(_ purchased: PurchasedProduct) -> some View {
|
||||
VStack(spacing: 10) {
|
||||
Text(NSLocalizedString("Purchased!", comment: "User purchased a subscription"))
|
||||
.font(.title2)
|
||||
.foregroundColor(.white)
|
||||
price_description(product: purchased.product)
|
||||
.foregroundColor(.white)
|
||||
.opacity(0.65)
|
||||
.frame(width: 200)
|
||||
Text(NSLocalizedString("Purchased on", comment: "Indicating when the user purchased the subscription"))
|
||||
.font(.title2)
|
||||
.foregroundColor(.white)
|
||||
Text(format_date(UInt32(purchased.tx.purchaseDate.timeIntervalSince1970)))
|
||||
.foregroundColor(.white)
|
||||
.opacity(0.65)
|
||||
if let expiry = purchased.tx.expirationDate {
|
||||
Text(NSLocalizedString("Renews on", comment: "Indicating when the subscription will renew"))
|
||||
.font(.title2)
|
||||
.foregroundColor(.white)
|
||||
Text(format_date(UInt32(expiry.timeIntervalSince1970)))
|
||||
.foregroundColor(.white)
|
||||
.opacity(0.65)
|
||||
}
|
||||
Button(action: {
|
||||
show_manage_subscriptions = true
|
||||
}, label: {
|
||||
Text(NSLocalizedString("Manage", comment: "Manage the damus subscription"))
|
||||
})
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
}
|
||||
}
|
||||
|
||||
var ProductStateView: some View {
|
||||
Group {
|
||||
switch self.products {
|
||||
case .failed:
|
||||
ProductLoadError
|
||||
case .loaded(let products):
|
||||
if let purchased {
|
||||
PurchasedView(purchased)
|
||||
} else {
|
||||
ProductsView(products)
|
||||
}
|
||||
case .loading:
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var MainContent: some View {
|
||||
VStack {
|
||||
HStack(spacing: 20) {
|
||||
Image("damus-dark-logo")
|
||||
.resizable()
|
||||
.frame(width: 60, height: 60)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 15.0))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.stroke(LinearGradient(
|
||||
colors: [DamusColors.lighterPink.opacity(0.8), .white.opacity(0), DamusColors.deepPurple.opacity(0.6)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing), lineWidth: 1)
|
||||
)
|
||||
.shadow(radius: 5)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(NSLocalizedString("Purple", comment: "Subscription service name"))
|
||||
.font(.system(size: 60.0).weight(.bold))
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
colors: [DamusColors.lighterPink, DamusColors.deepPurple],
|
||||
startPoint: .bottomLeading,
|
||||
endPoint: .topTrailing
|
||||
)
|
||||
)
|
||||
.foregroundColor(.white)
|
||||
.tracking(-2)
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 30)
|
||||
|
||||
VStack(alignment: .leading, spacing: 30) {
|
||||
Subtitle(NSLocalizedString("Help us stay independent in our mission for Freedom tech with our Purple subscription, and look cool doing it!", comment: "Damus purple subscription pitch"))
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
HStack(spacing: 20) {
|
||||
IconOnBox("heart.fill")
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Title(NSLocalizedString("Help Build The Future", comment: "Title for funding future damus development"))
|
||||
|
||||
Subtitle(NSLocalizedString("Support Damus development to help build the future of decentralized communication on the web.", comment: "Reason for supporting damus development"))
|
||||
}
|
||||
}
|
||||
|
||||
HStack(spacing: 20) {
|
||||
IconOnBox("ai-3-stars.fill")
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Title(NSLocalizedString("Exclusive features", comment: "Features only available on subscription service"))
|
||||
.padding(.bottom, -3)
|
||||
|
||||
HStack(spacing: 3) {
|
||||
Image("calendar")
|
||||
.resizable()
|
||||
.frame(width: 15, height: 15)
|
||||
|
||||
Text(NSLocalizedString("Coming soon", comment: "Feature is still in development and will be available soon"))
|
||||
.font(.caption)
|
||||
.bold()
|
||||
}
|
||||
.foregroundColor(DamusColors.pink)
|
||||
.padding(.vertical, 3)
|
||||
.padding(.horizontal, 8)
|
||||
.background(DamusColors.lightBackgroundPink)
|
||||
.cornerRadius(30.0)
|
||||
|
||||
Subtitle(NSLocalizedString("Be the first to access upcoming premium features: Automatic translations, longer note storage, and more", comment: "Description of new features to be expected"))
|
||||
.padding(.top, 3)
|
||||
}
|
||||
}
|
||||
|
||||
HStack(spacing: 20) {
|
||||
IconOnBox("badge")
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Title(NSLocalizedString("Supporter Badge", comment: "Title for supporter badge"))
|
||||
|
||||
Subtitle(NSLocalizedString("Get a special badge on your profile to show everyone your contribution to Freedom tech", comment: "Supporter badge description"))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
.padding([.trailing, .leading], 30)
|
||||
.padding(.bottom, 20)
|
||||
|
||||
VStack(alignment: .center) {
|
||||
ProductStateView
|
||||
}
|
||||
.padding([.top], 20)
|
||||
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DamusPurpleView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
/*
|
||||
DamusPurpleView(products: [
|
||||
DamusProduct(name: "Yearly", id: "purpleyearly", price: Decimal(69.99)),
|
||||
DamusProduct(name: "Monthly", id: "purple", price: Decimal(6.99)),
|
||||
])
|
||||
*/
|
||||
|
||||
DamusPurpleView(damus_state: test_damus_state)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
//
|
||||
// DamusPurpleWelcomeView.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Daniel D’Aquino on 2023-12-04.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
fileprivate extension Animation {
|
||||
static func content() -> Animation {
|
||||
Animation.easeInOut(duration: 1).delay(3)
|
||||
}
|
||||
}
|
||||
|
||||
struct DamusPurpleWelcomeView: View {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
@State var start = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Image("damus-dark-logo")
|
||||
.resizable()
|
||||
.frame(width: 50, height: 50)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10.0))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.stroke(LinearGradient(
|
||||
colors: [DamusColors.lighterPink.opacity(0.8), .white.opacity(0), DamusColors.deepPurple.opacity(0.6)],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing), lineWidth: 1)
|
||||
)
|
||||
.shadow(radius: 5)
|
||||
.padding(20)
|
||||
.opacity(start ? 1.0 : 0.0)
|
||||
.animation(.content(), value: start)
|
||||
|
||||
Text(NSLocalizedString("Welcome to Purple", comment: "Greeting to subscription service"))
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.bold)
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
colors: [.black, .black, DamusColors.pink, DamusColors.lighterPink],
|
||||
startPoint: start ? .init(x: -3, y: 4) : .bottomLeading,
|
||||
endPoint: start ? .topTrailing : .init(x: 3, y: -4)
|
||||
)
|
||||
)
|
||||
.opacity(start ? 1.0 : 0.0)
|
||||
.animation(Animation.easeInOut(duration: 3).delay(0), value: start)
|
||||
|
||||
Image(systemName: "star.fill")
|
||||
.resizable()
|
||||
.frame(width: 96, height: 90)
|
||||
.foregroundStyle(
|
||||
LinearGradient(
|
||||
colors: [.black, DamusColors.purple, .white, .white],
|
||||
startPoint: start ? .init(x: -1, y: 1.5) : .bottomLeading,
|
||||
endPoint: start ? .topTrailing : .init(x: 10, y: -11)
|
||||
)
|
||||
)
|
||||
.animation(Animation.snappy(duration: 3).delay(1), value: start)
|
||||
.shadow(
|
||||
color: start ? DamusColors.lightBackgroundPink : DamusColors.purple.opacity(0.3),
|
||||
radius: start ? 30 : 10
|
||||
)
|
||||
.animation(Animation.snappy(duration: 3).delay(0), value: start)
|
||||
.scaleEffect(x: start ? 1 : 3, y: start ? 1 : 3)
|
||||
.opacity(start ? 1.0 : 0.0)
|
||||
.animation(Animation.snappy(duration: 2).delay(0), value: start)
|
||||
|
||||
Text(NSLocalizedString("Thank you very much for signing up for Damus\u{00A0}Purple. Your contribution helps us continue our fight for a more Open and Free\u{00A0}internet.\n\nYou will also get access to premium features, and a star badge on your profile.\n\nEnjoy!", comment: "Appreciation to user for purchasing subscription service"))
|
||||
.lineSpacing(5)
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundStyle(.white.opacity(0.8))
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 50)
|
||||
.padding(.bottom, 20)
|
||||
.opacity(start ? 1.0 : 0.0)
|
||||
.animation(.content(), value: start)
|
||||
|
||||
Button(action: {
|
||||
dismiss()
|
||||
}, label: {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text(NSLocalizedString("Continue", comment: "Prompt to user to continue"))
|
||||
Spacer()
|
||||
}
|
||||
})
|
||||
.padding(.horizontal, 30)
|
||||
.buttonStyle(GradientButtonStyle())
|
||||
.opacity(start ? 1.0 : 0.0)
|
||||
.animation(Animation.easeInOut(duration: 2).delay(5), value: start)
|
||||
}
|
||||
.background(content: {
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.background(.black)
|
||||
Image("purple-blue-gradient-1")
|
||||
.offset(CGSize(width: 300.0, height: -0.0))
|
||||
.opacity(start ? 1.0 : 0.2)
|
||||
Image("stars-bg")
|
||||
.resizable(resizingMode: .stretch)
|
||||
.frame(width: 500, height: 500)
|
||||
.offset(x: -100, y: 50)
|
||||
.scaleEffect(start ? 1 : 1.1)
|
||||
.animation(Animation.easeOut(duration: 3).delay(0), value: start)
|
||||
Image("purple-blue-gradient-1")
|
||||
.offset(CGSize(width: 300.0, height: -0.0))
|
||||
.opacity(start ? 1.0 : 0.2)
|
||||
|
||||
}
|
||||
})
|
||||
.onAppear(perform: {
|
||||
withAnimation(.easeOut(duration: 6), {
|
||||
start = true
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
struct DamusPurpleWelcomeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DamusPurpleWelcomeView()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// RelayAuthenticationDetail.swift
|
||||
// damus
|
||||
//
|
||||
// Created by Charlie Fish on 12/18/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct RelayAuthenticationDetail: View {
|
||||
let state: RelayAuthenticationState
|
||||
|
||||
var body: some View {
|
||||
switch state {
|
||||
case .none:
|
||||
EmptyView()
|
||||
case .pending:
|
||||
Text(NSLocalizedString("Pending", comment: "Label to display that authentication to a server is pending."))
|
||||
case .verified:
|
||||
Text(NSLocalizedString("Authenticated", comment: "Label to display that authentication to a server has succeeded."))
|
||||
.foregroundStyle(DamusColors.success)
|
||||
case .error:
|
||||
Text(NSLocalizedString("Error", comment: "Label to display that authentication to a server has failed."))
|
||||
.foregroundStyle(DamusColors.danger)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct RelayAuthenticationDetail_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
RelayAuthenticationDetail(state: .none)
|
||||
RelayAuthenticationDetail(state: .pending)
|
||||
RelayAuthenticationDetail(state: .verified)
|
||||
}
|
||||
}
|
||||
@@ -23,15 +23,6 @@ struct RecommendedRelayView: View {
|
||||
self.model_cache = damus.relay_model_cache
|
||||
}
|
||||
|
||||
var recommended: [RelayDescriptor] {
|
||||
let rs: [RelayDescriptor] = []
|
||||
return BOOTSTRAP_RELAYS.reduce(into: rs) { xs, x in
|
||||
if damus.pool.get_relay(x) == nil, let url = RelayURL(x) {
|
||||
xs.append(RelayDescriptor(url: url, info: .rw))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let meta = model_cache.model(with_relay_id: relay)?.metadata
|
||||
|
||||
@@ -84,6 +75,8 @@ struct RecommendedRelayView: View {
|
||||
HStack {
|
||||
Text(meta?.name ?? relay)
|
||||
.lineLimit(1)
|
||||
.frame(maxWidth: 150)
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.contextMenu {
|
||||
CopyAction(relay: relay)
|
||||
|
||||
@@ -23,7 +23,8 @@ struct RelayConfigView: View {
|
||||
|
||||
var recommended: [RelayDescriptor] {
|
||||
let rs: [RelayDescriptor] = []
|
||||
return BOOTSTRAP_RELAYS.reduce(into: rs) { xs, x in
|
||||
let recommended_relay_addresses = get_default_bootstrap_relays()
|
||||
return recommended_relay_addresses.reduce(into: rs) { xs, x in
|
||||
if state.pool.get_relay(x) == nil, let url = RelayURL(x) {
|
||||
xs.append(RelayDescriptor(url: url, info: .rw))
|
||||
}
|
||||
@@ -87,11 +88,29 @@ struct RelayConfigView: View {
|
||||
.stroke(DamusLightGradient.gradient)
|
||||
}
|
||||
|
||||
HStack(spacing: 20) {
|
||||
ForEach(recommended, id: \.url) { r in
|
||||
RecommendedRelayView(damus: state, relay: r.url.id)
|
||||
ScrollView(.horizontal) {
|
||||
HStack(spacing: 20) {
|
||||
ForEach(recommended, id: \.url) { r in
|
||||
RecommendedRelayView(damus: state, relay: r.url.id)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 30)
|
||||
.padding(.vertical, 5)
|
||||
}
|
||||
.scrollIndicators(.hidden)
|
||||
.mask(
|
||||
HStack(spacing: 0) {
|
||||
LinearGradient(gradient: Gradient(colors: [Color.clear, Color.white]), startPoint: .leading, endPoint: .trailing)
|
||||
.frame(width: 30)
|
||||
|
||||
Rectangle()
|
||||
.fill(Color.white)
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
LinearGradient(gradient: Gradient(colors: [Color.white, Color.clear]), startPoint: .leading, endPoint: .trailing)
|
||||
.frame(width: 30)
|
||||
}
|
||||
)
|
||||
.padding()
|
||||
}
|
||||
.frame(minWidth: 250, maxWidth: .infinity, minHeight: 250, alignment: .center)
|
||||
|
||||
@@ -92,7 +92,14 @@ struct RelayDetailView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if let authentication_state: RelayAuthenticationState = relay_object?.authentication_state,
|
||||
authentication_state != .none {
|
||||
Section(NSLocalizedString("Authentication", comment: "Header label to display authentication details for a given relay.")) {
|
||||
RelayAuthenticationDetail(state: authentication_state)
|
||||
}
|
||||
}
|
||||
|
||||
if let pubkey = nip11?.pubkey {
|
||||
Section(NSLocalizedString("Admin", comment: "Label to display relay contact user.")) {
|
||||
UserViewRow(damus_state: state, pubkey: pubkey)
|
||||
@@ -175,9 +182,13 @@ struct RelayDetailView: View {
|
||||
}
|
||||
return attrString
|
||||
}
|
||||
|
||||
|
||||
private var relay_object: Relay? {
|
||||
state.pool.get_relay(relay)
|
||||
}
|
||||
|
||||
private var relay_connection: RelayConnection? {
|
||||
state.pool.get_relay(relay)?.connection
|
||||
relay_object?.connection
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -165,6 +165,8 @@ struct SaveKeysView: View {
|
||||
break
|
||||
case .ok:
|
||||
break
|
||||
case .auth:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ struct PullDownSearchView: View {
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
TextField("Search", text: $search_text)
|
||||
TextField(NSLocalizedString("Search", comment: "Title of the text field for searching."), text: $search_text)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.onChange(of: search_text) { query in
|
||||
debouncer.debounce {
|
||||
@@ -75,7 +75,7 @@ struct PullDownSearchView: View {
|
||||
end_editing()
|
||||
on_cancel()
|
||||
}, label: {
|
||||
Text("Cancel")
|
||||
Text("Cancel", comment: "Button to cancel out of search text entry mode.")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ struct AppearanceSettingsView: View {
|
||||
// MARK: - Images
|
||||
Section(NSLocalizedString("Images", comment: "Section title for images configuration.")) {
|
||||
self.EnableAnimationsToggle
|
||||
Toggle(NSLocalizedString("Always show images", comment: "Setting to always show and never blur images"), isOn: $settings.always_show_images)
|
||||
Toggle(NSLocalizedString("Blur images", comment: "Setting to blur images"), isOn: $settings.blur_images)
|
||||
.toggleStyle(.switch)
|
||||
|
||||
Toggle(NSLocalizedString("Media previews", comment: "Setting to show media"), isOn: $settings.media_previews)
|
||||
|
||||
@@ -17,12 +17,18 @@ struct DeveloperSettingsView: View {
|
||||
Toggle(NSLocalizedString("Developer Mode", comment: "Setting to enable developer mode"), isOn: $settings.developer_mode)
|
||||
.toggleStyle(.switch)
|
||||
if settings.developer_mode {
|
||||
Toggle("Always show onboarding", isOn: $settings.always_show_onboarding_suggestions)
|
||||
Toggle(NSLocalizedString("Always show onboarding", comment: "Developer mode setting to always show onboarding suggestions."), isOn: $settings.always_show_onboarding_suggestions)
|
||||
|
||||
Toggle("Enable experimental push notifications", isOn: $settings.enable_experimental_push_notifications)
|
||||
Toggle(NSLocalizedString("Enable experimental push notifications", comment: "Developer mode setting to enable experimental push notifications."), isOn: $settings.enable_experimental_push_notifications)
|
||||
.toggleStyle(.switch)
|
||||
|
||||
Toggle("Send device token to localhost", isOn: $settings.send_device_token_to_localhost)
|
||||
Toggle(NSLocalizedString("Send device token to localhost", comment: "Developer mode setting to send device token metadata to a local server instead of the damus.io server."), isOn: $settings.send_device_token_to_localhost)
|
||||
.toggleStyle(.switch)
|
||||
|
||||
Toggle("Enable experimental Purple API support", isOn: $settings.enable_experimental_purple_api)
|
||||
.toggleStyle(.switch)
|
||||
|
||||
Toggle("Purple API localhost test mode", isOn: $settings.purple_api_local_test_mode)
|
||||
.toggleStyle(.switch)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import SwiftUI
|
||||
|
||||
struct TranslationSettingsView: View {
|
||||
@ObservedObject var settings: UserSettingsStore
|
||||
var damus_state: DamusState
|
||||
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
@@ -19,11 +20,17 @@ struct TranslationSettingsView: View {
|
||||
.toggleStyle(.switch)
|
||||
|
||||
Picker(NSLocalizedString("Service", comment: "Prompt selection of translation service provider."), selection: $settings.translation_service) {
|
||||
ForEach(TranslationService.allCases, id: \.self) { server in
|
||||
ForEach(TranslationService.allCases.filter({ settings.enable_experimental_purple_api ? true : $0 != .purple }), id: \.self) { server in
|
||||
Text(server.model.displayName)
|
||||
.tag(server.model.tag)
|
||||
}
|
||||
}
|
||||
|
||||
if settings.translation_service == .purple && settings.enable_experimental_purple_api {
|
||||
NavigationLink(destination: DamusPurpleView(damus_state: damus_state)) {
|
||||
Text(NSLocalizedString("Configure Damus Purple", comment: "Button to allow Damus Purple to be configured"))
|
||||
}
|
||||
}
|
||||
|
||||
if settings.translation_service == .libretranslate {
|
||||
Picker(NSLocalizedString("Server", comment: "Prompt selection of LibreTranslate server to perform machine translations on notes"), selection: $settings.libretranslate_server) {
|
||||
@@ -103,6 +110,6 @@ struct TranslationSettingsView: View {
|
||||
|
||||
struct TranslationSettingsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TranslationSettingsView(settings: UserSettingsStore())
|
||||
TranslationSettingsView(settings: UserSettingsStore(), damus_state: test_damus_state)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ struct SideMenuView: View {
|
||||
.onTapGesture {
|
||||
isSidebarVisible.toggle()
|
||||
}
|
||||
|
||||
content
|
||||
}
|
||||
}
|
||||
@@ -51,17 +52,18 @@ struct SideMenuView: View {
|
||||
|
||||
NavigationLink(value: Route.Wallet(wallet: damus_state.wallet)) {
|
||||
navLabel(title: NSLocalizedString("Wallet", comment: "Sidebar menu label for Wallet view."), img: "wallet")
|
||||
/*
|
||||
HStack {
|
||||
Image("wallet")
|
||||
.tint(DamusColors.adaptableBlack)
|
||||
}
|
||||
|
||||
Text(NSLocalizedString("wallet", comment: "Sidebar menu label for Wallet view."))
|
||||
.font(.title2)
|
||||
.foregroundColor(textColor())
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.dynamicTypeSize(.xSmall)
|
||||
}*/
|
||||
if damus_state.settings.enable_experimental_purple_api {
|
||||
NavigationLink(destination: DamusPurpleView(damus_state: damus_state)) {
|
||||
HStack(spacing: 13) {
|
||||
Image("nostr-hashtag")
|
||||
Text("Purple")
|
||||
.foregroundColor(DamusColors.purple)
|
||||
.font(.title2.weight(.bold))
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(value: Route.MuteList(users: get_mutelist_users(damus_state.contacts.mutelist))) {
|
||||
|
||||
@@ -102,14 +102,11 @@ struct SuggestedHashtagsView: View {
|
||||
SingleCharacterAvatar(character: "#")
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("#\(hashtag)")
|
||||
Text(verbatim: "#\(hashtag)")
|
||||
.bold()
|
||||
|
||||
Text(self.count != 1 ? String(
|
||||
format: NSLocalizedString("%d users talking about it", comment: "A label indicating how many users have been talking about a hashtag"),
|
||||
self.count
|
||||
) : NSLocalizedString("1 user talking about it", comment: "A label indicating 1 user has been talking about a hashtag"))
|
||||
.foregroundStyle(.secondary)
|
||||
Text(pluralizedString(key: "users_talking_about_it", count: self.count))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
@@ -100,7 +100,7 @@ struct DamusVideoPlayer: View {
|
||||
private var live_indicator: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
Text("LIVE")
|
||||
Text("LIVE", comment: "Text indicator that the video is a livestream.")
|
||||
.bold()
|
||||
.foregroundColor(.red)
|
||||
.padding(.horizontal)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -6,6 +6,7 @@
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import StoreKit
|
||||
|
||||
@main
|
||||
struct damusApp: App {
|
||||
@@ -43,6 +44,9 @@ struct MainView: View {
|
||||
.onReceive(handle_notify(.logout)) { () in
|
||||
try? clear_keypair()
|
||||
keypair = nil
|
||||
// We need to disconnect and reconnect to all relays when the user signs out
|
||||
// This is to conform to NIP-42 and ensure we aren't persisting old connections
|
||||
notify(.disconnect_relays)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
|
||||
orientationTracker.setDeviceMajorAxis()
|
||||
@@ -61,6 +65,8 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
|
||||
SKPaymentQueue.default().add(StoreObserver.standard)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -84,7 +90,7 @@ class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDele
|
||||
let json: [String: Any] = ["deviceToken": token, "pubkey": pubkey.hex()]
|
||||
|
||||
// create post request
|
||||
let url = URL(string: settings.send_device_token_to_localhost ? "http://localhost:8000/user-info" : "https://notify.damus.io:8000/user-info")!
|
||||
let url = settings.send_device_token_to_localhost ? Constants.DEVICE_TOKEN_RECEIVER_TEST_URL : Constants.DEVICE_TOKEN_RECEIVER_PRODUCTION_URL
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -258,6 +258,22 @@
|
||||
<string>%2$@ Sats</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>users_talking_about_it</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@USERS@</string>
|
||||
<key>USERS</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>%d Benutzer spricht darüber</string>
|
||||
<key>other</key>
|
||||
<string>%d Benutzer sprechen darüber</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>word_count</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
|
||||
@@ -258,6 +258,22 @@
|
||||
<string>%2$@ sats</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>users_talking_about_it</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@USERS@</string>
|
||||
<key>USERS</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>%d user talking about it</string>
|
||||
<key>other</key>
|
||||
<string>%d users talking about it</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>word_count</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.2" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 http://docs.oasis-open.org/xliff/v1.2/os/xliff-core-1.2-strict.xsd">
|
||||
<file original="damus/en-US.lproj/InfoPlist.strings" source-language="en-US" target-language="en-US" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0.1" build-num="15A507"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
||||
@@ -21,8 +21,8 @@
|
||||
<note>Privacy - Media Library Usage Description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="NSCameraUsageDescription" xml:space="preserve">
|
||||
<source>Damus needs access to your camera if you want to scan QR codes and upload photos</source>
|
||||
<target>Damus needs access to your camera if you want to scan QR codes and upload photos</target>
|
||||
<source>Damus needs access to your camera in order to upload photos and scan QR codes.</source>
|
||||
<target>Damus needs access to your camera in order to upload photos and scan QR codes.</target>
|
||||
<note>Privacy - Camera Usage Description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="NSFaceIDUsageDescription" xml:space="preserve">
|
||||
@@ -31,8 +31,8 @@
|
||||
<note>Privacy - Face ID Usage Description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="NSMicrophoneUsageDescription" xml:space="preserve">
|
||||
<source>Damus needs access to your microphone if you want to upload recorded videos from it</source>
|
||||
<target>Damus needs access to your microphone if you want to upload recorded videos from it</target>
|
||||
<source>Damus needs access to your microphone for creating video recording posts</source>
|
||||
<target>Damus needs access to your microphone for creating video recording posts</target>
|
||||
<note>Privacy - Microphone Usage Description</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="NSPhotoLibraryAddUsageDescription" xml:space="preserve">
|
||||
@@ -44,7 +44,7 @@
|
||||
</file>
|
||||
<file original="damus/en-US.lproj/Localizable.strings" source-language="en-US" target-language="en-US" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0.1" build-num="15A507"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="%@ %@" xml:space="preserve">
|
||||
@@ -160,6 +160,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
|
||||
<target>Add all</target>
|
||||
<note>Button label to re-add all original participants as profiles to reply to in a note</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Add an external link" xml:space="preserve">
|
||||
<source>Add an external link</source>
|
||||
<target>Add an external link</target>
|
||||
<note>Placeholder as an example of what the user could set so that the link is opened when the status is tapped.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Add bookmark" xml:space="preserve">
|
||||
<source>Add bookmark</source>
|
||||
<target>Add bookmark</target>
|
||||
@@ -170,6 +175,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
|
||||
<target>Add relay</target>
|
||||
<note>Title text to indicate user to an add a relay.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Add your first post" xml:space="preserve">
|
||||
<source>Add your first post</source>
|
||||
<target>Add your first post</target>
|
||||
<note>Prompt given to the user during onboarding, suggesting them to write their first post</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Additional information" xml:space="preserve">
|
||||
<source>Additional information</source>
|
||||
<target>Additional information</target>
|
||||
@@ -185,6 +195,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
|
||||
<target>All</target>
|
||||
<note>Label for filter for all notifications.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="All recent notes" xml:space="preserve">
|
||||
<source>All recent notes</source>
|
||||
<target>All recent notes</target>
|
||||
<note>A label indicating that the notes being displayed below it are all recent notes</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Already on Nostr?" xml:space="preserve">
|
||||
<source>Already on Nostr?</source>
|
||||
<target>Already on Nostr?</target>
|
||||
@@ -195,6 +210,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
|
||||
<target>Always show images</target>
|
||||
<note>Setting to always show and never blur images</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Always show onboarding" xml:space="preserve">
|
||||
<source>Always show onboarding</source>
|
||||
<target>Always show onboarding</target>
|
||||
<note>Developer mode setting to always show onboarding suggestions.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="An additional percentage of each zap will be sent to support Damus development" xml:space="preserve">
|
||||
<source>An additional percentage of each zap will be sent to support Damus development</source>
|
||||
<target>An additional percentage of each zap will be sent to support Damus development</target>
|
||||
@@ -237,6 +257,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
|
||||
<target>Are you sure you want to attach this wallet?</target>
|
||||
<note>Prompt to ask user if they want to attach their Nostr Wallet Connect lightning wallet.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Are you sure you want to clear the cache? This will free space, but images may take longer to load again." xml:space="preserve">
|
||||
<source>Are you sure you want to clear the cache? This will free space, but images may take longer to load again.</source>
|
||||
<target>Are you sure you want to clear the cache? This will free space, but images may take longer to load again.</target>
|
||||
<note>Message explaining what it means to clear the cache, asking if user wants to proceed.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Are you sure you want to delete all of your bookmarks?" xml:space="preserve">
|
||||
<source>Are you sure you want to delete all of your bookmarks?</source>
|
||||
<target>Are you sure you want to delete all of your bookmarks?</target>
|
||||
@@ -309,11 +334,17 @@ Sentence composed of 2 variables to describe how many reposts. In source English
|
||||
<target>Broadcast music playing on Apple Music</target>
|
||||
<note>Toggle to enable or disable broadcasting what music is being played on Apple Music in their profile status.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cache has been cleared" xml:space="preserve">
|
||||
<source>Cache has been cleared</source>
|
||||
<target>Cache has been cleared</target>
|
||||
<note>Message indicating that the cache was successfully cleared.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Cancel" xml:space="preserve">
|
||||
<source>Cancel</source>
|
||||
<target>Cancel</target>
|
||||
<note>Alert button to cancel out of alert for muting a user.
|
||||
Button to cancel a repost.
|
||||
Button to cancel any interaction with the QRCode link.
|
||||
Button to cancel out of alert that creates a new mutelist.
|
||||
Button to cancel out of posting a note.
|
||||
Button to cancel out of view adding user inputted emoji.
|
||||
@@ -323,6 +354,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
|
||||
Cancel out of logging out the user.
|
||||
Text for button to cancel out of connecting Nostr Wallet Connect lightning ewallet.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Changing this setting will cause the cache to be cleared. This will free space, but images may take longer to load again. Are you sure you want to proceed?" xml:space="preserve">
|
||||
<source>Changing this setting will cause the cache to be cleared. This will free space, but images may take longer to load again. Are you sure you want to proceed?</source>
|
||||
<target>Changing this setting will cause the cache to be cleared. This will free space, but images may take longer to load again. Are you sure you want to proceed?</target>
|
||||
<note>Message explaining consequences of changing the 'enable animation' setting</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Choose from Library" xml:space="preserve">
|
||||
<source>Choose from Library</source>
|
||||
<target>Choose from Library</target>
|
||||
@@ -343,6 +379,16 @@ Sentence composed of 2 variables to describe how many reposts. In source English
|
||||
<target>Clear status</target>
|
||||
<note>Label to prompt user to select an expiration time for the profile status to clear.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Clearing Cache" xml:space="preserve">
|
||||
<source>Clearing Cache</source>
|
||||
<target>Clearing Cache</target>
|
||||
<note>Loading message indicating that the cache is being cleared.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Confirmation" xml:space="preserve">
|
||||
<source>Confirmation</source>
|
||||
<target>Confirmation</target>
|
||||
<note>Confirmation dialog title</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Connect To Relay" xml:space="preserve">
|
||||
<source>Connect To Relay</source>
|
||||
<target>Connect To Relay</target>
|
||||
@@ -380,6 +426,7 @@ Sentence composed of 2 variables to describe how many reposts. In source English
|
||||
<target>Copy</target>
|
||||
<note>Button to copy a relay server address.
|
||||
Button to copy an emoji reaction
|
||||
Button to copy the value found.
|
||||
Context menu option for copying the version of damus.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Copy Account ID" xml:space="preserve">
|
||||
@@ -547,6 +594,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
|
||||
<target>Disconnect Wallet</target>
|
||||
<note>Text for button to disconnect from Nostr Wallet Connect lightning wallet.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Dismiss" xml:space="preserve">
|
||||
<source>Dismiss</source>
|
||||
<target>Dismiss</target>
|
||||
<note>Button to dismiss alert</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Display name" xml:space="preserve">
|
||||
<source>Display name</source>
|
||||
<target>Display name</target>
|
||||
@@ -587,6 +639,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
|
||||
<target>Emoji Reactions</target>
|
||||
<note>Section title for emoji reactions that are currently added.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Enable experimental push notifications" xml:space="preserve">
|
||||
<source>Enable experimental push notifications</source>
|
||||
<target>Enable experimental push notifications</target>
|
||||
<note>Developer mode setting to enable experimental push notifications.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Encrypted" xml:space="preserve">
|
||||
<source>Encrypted</source>
|
||||
<target>Encrypted</target>
|
||||
@@ -632,11 +689,6 @@ Sentence composed of 2 variables to describe how many reposts. In source English
|
||||
<target>Failed to parse</target>
|
||||
<note>NostrScript error message when it fails to parse a script.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Filter" xml:space="preserve">
|
||||
<source>Filter</source>
|
||||
<target>Filter</target>
|
||||
<note>Button label text for filtering relay servers.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Follow" xml:space="preserve">
|
||||
<source>Follow</source>
|
||||
<target>Follow</target>
|
||||
@@ -707,6 +759,22 @@ Sentence composed of 2 variables to describe how many reposts. In source English
|
||||
<target>Font Size</target>
|
||||
<note>Section label for font size settings.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="For #Introductions! I’m a software developer. My side interests include languages and I am striving to be a #polyglot - I am a native English speaker and can speak French, German and Japanese." xml:space="preserve">
|
||||
<source>For #Introductions! I’m a software developer.
|
||||
|
||||
My side interests include languages and I am striving to be a #polyglot - I am a native English speaker and can speak French, German and Japanese.</source>
|
||||
<target>For #Introductions! I’m a software developer.
|
||||
|
||||
My side interests include languages and I am striving to be a #polyglot - I am a native English speaker and can speak French, German and Japanese.</target>
|
||||
<note>First post example given to the user during onboarding, as a suggestion as to what they could post first</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Found (qrCodeValue)" xml:space="preserve">
|
||||
<source>Found
|
||||
(qrCodeValue)</source>
|
||||
<target>Found
|
||||
(qrCodeValue)</target>
|
||||
<note>Alert message asking if the user wants to open the link.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Free" xml:space="preserve">
|
||||
<source>Free</source>
|
||||
<target>Free</target>
|
||||
@@ -720,13 +788,27 @@ Sentence composed of 2 variables to describe how many reposts. In source English
|
||||
<trans-unit id="Get API Key with BTC/Lightning" xml:space="preserve">
|
||||
<source>Get API Key with BTC/Lightning</source>
|
||||
<target>Get API Key with BTC/Lightning</target>
|
||||
<note>Button to navigate to nokyctranslate website to get a translation API key.</note>
|
||||
<note>Button to navigate to nokyctranslate website to get a translation API key.
|
||||
Button to navigate to translate.nostr.wine to get a translation API key.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Hashtags" xml:space="preserve">
|
||||
<source>Hashtags</source>
|
||||
<target>Hashtags</target>
|
||||
<note>Label for filter for seeing only hashtag follows.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Hello everybody! This is my first post on Damus, I am happy to meet you all 🤙. What’s up? #introductions" xml:space="preserve">
|
||||
<source>Hello everybody!
|
||||
|
||||
This is my first post on Damus, I am happy to meet you all 🤙. What’s up?
|
||||
|
||||
#introductions</source>
|
||||
<target>Hello everybody!
|
||||
|
||||
This is my first post on Damus, I am happy to meet you all 🤙. What’s up?
|
||||
|
||||
#introductions</target>
|
||||
<note>First post example given to the user during onboarding, as a suggestion as to what they could post first</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Help build the future of decentralized communication on the web." xml:space="preserve">
|
||||
<source>Help build the future of decentralized communication on the web.</source>
|
||||
<target>Help build the future of decentralized communication on the web.</target>
|
||||
@@ -752,6 +834,15 @@ Sentence composed of 2 variables to describe how many reposts. In source English
|
||||
<target>Home</target>
|
||||
<note>Navigation bar title for Home view where notes and replies appear from those who the user is following.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Howdy! I’m a graphic designer during the day and coder at night, but I’m also trying to spend more time outdoors. Hope to meet folks who are on their own journeys to a peaceful and free life!" xml:space="preserve">
|
||||
<source>Howdy! I’m a graphic designer during the day and coder at night, but I’m also trying to spend more time outdoors.
|
||||
|
||||
Hope to meet folks who are on their own journeys to a peaceful and free life!</source>
|
||||
<target>Howdy! I’m a graphic designer during the day and coder at night, but I’m also trying to spend more time outdoors.
|
||||
|
||||
Hope to meet folks who are on their own journeys to a peaceful and free life!</target>
|
||||
<note>First post example given to the user during onboarding, as a suggestion as to what they could post first</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Illegal Content" xml:space="preserve">
|
||||
<source>Illegal Content</source>
|
||||
<target>Illegal Content</target>
|
||||
@@ -798,6 +889,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
|
||||
<note>Navigation title for managing keys.
|
||||
Settings section for managing keys</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="LIVE" xml:space="preserve">
|
||||
<source>LIVE</source>
|
||||
<target>LIVE</target>
|
||||
<note>Text indicator that the video is a livestream.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Learn more about Nostr" xml:space="preserve">
|
||||
<source>Learn more about Nostr</source>
|
||||
<target>Learn more about Nostr</target>
|
||||
@@ -843,6 +939,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
|
||||
<target>Likes</target>
|
||||
<note>Setting to enable Like Local Notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Load media" xml:space="preserve">
|
||||
<source>Load media</source>
|
||||
<target>Load media</target>
|
||||
<note>Button to show media in note.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Local Notifications" xml:space="preserve">
|
||||
<source>Local Notifications</source>
|
||||
<target>Local Notifications</target>
|
||||
@@ -889,6 +990,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
|
||||
<target>Make sure your nsec account key is saved before you logout or you will lose access to this account</target>
|
||||
<note>Reminder message in alert to get customer to verify that their private security account key is saved saved before logging out.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Media previews" xml:space="preserve">
|
||||
<source>Media previews</source>
|
||||
<target>Media previews</target>
|
||||
<note>Setting to show media</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Mentioned by %@" xml:space="preserve">
|
||||
<source>Mentioned by %@</source>
|
||||
<target>Mentioned by %@</target>
|
||||
@@ -899,6 +1005,16 @@ Sentence composed of 2 variables to describe how many reposts. In source English
|
||||
<target>Mentions</target>
|
||||
<note>Setting to enable Mention Local Notification</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Merch" xml:space="preserve">
|
||||
<source>Merch</source>
|
||||
<target>Merch</target>
|
||||
<note>Sidebar menu label for merch store link.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Message" xml:space="preserve">
|
||||
<source>Message</source>
|
||||
<target>Message</target>
|
||||
<note>Button label that allows the user to start a direct message conversation with the user shown on-screen</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Mute" xml:space="preserve">
|
||||
<source>Mute</source>
|
||||
<target>Mute</target>
|
||||
@@ -980,6 +1096,11 @@ Sentence composed of 2 variables to describe how many reposts. In source English
|
||||
<target>No one will see that you zapped</target>
|
||||
<note>Description of anonymous zap type where the zap is sent anonymously and does not identify the user who sent it.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No results" xml:space="preserve">
|
||||
<source>No results</source>
|
||||
<target>No results</target>
|
||||
<note>A label indicating that note search resulted in no results</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="No zaps will be sent, only a lightning payment." xml:space="preserve">
|
||||
<source>No zaps will be sent, only a lightning payment.</source>
|
||||
<target>No zaps will be sent, only a lightning payment.</target>
|
||||
@@ -1028,8 +1149,7 @@ Sentence composed of 2 variables to describe how many reposts. In source English
|
||||
<trans-unit id="Notes" xml:space="preserve">
|
||||
<source>Notes</source>
|
||||
<target>Notes</target>
|
||||
<note>Label for filter for seeing only your notes (instead of notes and replies).
|
||||
Label for filter for seeing only notes (instead of notes and replies).</note>
|
||||
<note>A label indicating that the notes being displayed below it are from a timeline, not search results</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Notes & Replies" xml:space="preserve">
|
||||
<source>Notes & Replies</source>
|
||||
@@ -1068,6 +1188,12 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
|
||||
<target>Nudity</target>
|
||||
<note>Description of report type for nudity.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="OK" xml:space="preserve">
|
||||
<source>OK</source>
|
||||
<target>OK</target>
|
||||
<note>Button label indicating user wants to proceed.
|
||||
Button label to dismiss an error dialog</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Ok" xml:space="preserve">
|
||||
<source>Ok</source>
|
||||
<target>Ok</target>
|
||||
@@ -1098,6 +1224,16 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
|
||||
<target>OnlyZaps mode</target>
|
||||
<note>Setting toggle to hide reactions.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open in browser" xml:space="preserve">
|
||||
<source>Open in browser</source>
|
||||
<target>Open in browser</target>
|
||||
<note>Button to open the value found in browser.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Open in wallet" xml:space="preserve">
|
||||
<source>Open in wallet</source>
|
||||
<target>Open in wallet</target>
|
||||
<note>Button to open the value found in browser.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Optional" xml:space="preserve">
|
||||
<source>Optional</source>
|
||||
<target>Optional</target>
|
||||
@@ -1184,6 +1320,16 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
|
||||
<target>Profile Picture</target>
|
||||
<note>Label for Profile Picture section of user profile form.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Profile action sheets allow you to follow, zap, or DM profiles more quickly without having to view their full profile" xml:space="preserve">
|
||||
<source>Profile action sheets allow you to follow, zap, or DM profiles more quickly without having to view their full profile</source>
|
||||
<target>Profile action sheets allow you to follow, zap, or DM profiles more quickly without having to view their full profile</target>
|
||||
<note>Section footer clarifying what the profile action sheet feature does</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Profiles" xml:space="preserve">
|
||||
<source>Profiles</source>
|
||||
<target>Profiles</target>
|
||||
<note>Section title for profile view configuration.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Public" xml:space="preserve">
|
||||
<source>Public</source>
|
||||
<target>Public</target>
|
||||
@@ -1386,21 +1532,41 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
|
||||
<target>Save Image</target>
|
||||
<note>Context menu option to save an image.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Save Key in Secure Keychain" xml:space="preserve">
|
||||
<source>Save Key in Secure Keychain</source>
|
||||
<target>Save Key in Secure Keychain</target>
|
||||
<note>Toggle to save private key to the Apple secure keychain.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Scan Code" xml:space="preserve">
|
||||
<source>Scan Code</source>
|
||||
<target>Scan Code</target>
|
||||
<note>Button to switch to scan QR Code page.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Scan Your Private Key QR" xml:space="preserve">
|
||||
<source>Scan Your Private Key QR</source>
|
||||
<target>Scan Your Private Key QR</target>
|
||||
<note>Text to prompt scanning a QR code of a user's privkey to login to their profile.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Scan a user's pubkey" xml:space="preserve">
|
||||
<source>Scan a user's pubkey</source>
|
||||
<target>Scan a user's pubkey</target>
|
||||
<note>Text to prompt scanning a QR code of a user's pubkey to open their profile.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Scan for QR Code" xml:space="preserve">
|
||||
<source>Scan for QR Code</source>
|
||||
<target>Scan for QR Code</target>
|
||||
<note>Context menu option to scan image for a QR Code.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Scan the code" xml:space="preserve">
|
||||
<source>Scan the code</source>
|
||||
<target>Scan the code</target>
|
||||
<note>Text on QR code view to prompt viewer to scan the QR code on screen with their device camera.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Search" xml:space="preserve">
|
||||
<source>Search</source>
|
||||
<target>Search</target>
|
||||
<note>Title of the text field for searching.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Search hashtag: #%@" xml:space="preserve">
|
||||
<source>Search hashtag: #%@</source>
|
||||
<target>Search hashtag: #%@</target>
|
||||
@@ -1447,6 +1613,11 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
|
||||
<target>Send a message with your zap...</target>
|
||||
<note>Placeholder text for a comment to send as part of a zap to the user.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Send device token to localhost" xml:space="preserve">
|
||||
<source>Send device token to localhost</source>
|
||||
<target>Send device token to localhost</target>
|
||||
<note>Developer mode setting to send device token metadata to a local server instead of the damus.io server.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Server" xml:space="preserve">
|
||||
<source>Server</source>
|
||||
<target>Server</target>
|
||||
@@ -1522,6 +1693,16 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
|
||||
<target>Show only preferred languages on Universe feed</target>
|
||||
<note>Toggle to show notes that are only in the device's preferred languages on the Universe feed and hide notes that are in other languages.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show profile action sheets" xml:space="preserve">
|
||||
<source>Show profile action sheets</source>
|
||||
<target>Show profile action sheets</target>
|
||||
<note>Setting to show profile action sheets when clicking on a user's profile picture</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show recommended relays" xml:space="preserve">
|
||||
<source>Show recommended relays</source>
|
||||
<target>Show recommended relays</target>
|
||||
<note>Button to show recommended relays.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Show wallet selector" xml:space="preserve">
|
||||
<source>Show wallet selector</source>
|
||||
<target>Show wallet selector</target>
|
||||
@@ -1563,6 +1744,16 @@ Label for filter for seeing notes and replies (instead of only notes).</note>
|
||||
<note>Description of report type for spam.
|
||||
Section header for Universe/Search spam</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Staying humble..." xml:space="preserve">
|
||||
<source>Staying humble...</source>
|
||||
<target>Staying humble...</target>
|
||||
<note>Placeholder as an example of what the user could set as their profile status.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Suggested hashtags" xml:space="preserve">
|
||||
<source>Suggested hashtags</source>
|
||||
<target>Suggested hashtags</target>
|
||||
<note>A label indicating that the items below it are suggested hashtags</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Support Damus" xml:space="preserve">
|
||||
<source>Support Damus</source>
|
||||
<target>Support Damus</target>
|
||||
@@ -1615,6 +1806,15 @@ You're all set!</target>
|
||||
<target>This is a public key, you will not be able to make notes or interact in any way. This is used for viewing accounts from their perspective.</target>
|
||||
<note>Warning that the inputted account key is a public key and the result of what happens because of it.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This is my first post on Nostr 💜. I love drawing and folding Origami! Nice to meet you all! #introductions #plebchain " xml:space="preserve">
|
||||
<source>This is my first post on Nostr 💜. I love drawing and folding Origami!
|
||||
|
||||
Nice to meet you all! #introductions #plebchain </source>
|
||||
<target>This is my first post on Nostr 💜. I love drawing and folding Origami!
|
||||
|
||||
Nice to meet you all! #introductions #plebchain </target>
|
||||
<note>First post example given to the user during onboarding, as a suggestion as to what they could post first</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="This is your account ID, you can give this to your friends so that they can follow you. Tap to copy." xml:space="preserve">
|
||||
<source>This is your account ID, you can give this to your friends so that they can follow you. Tap to copy.</source>
|
||||
<target>This is your account ID, you can give this to your friends so that they can follow you. Tap to copy.</target>
|
||||
@@ -1635,6 +1835,11 @@ You're all set!</target>
|
||||
<target>Top Zap</target>
|
||||
<note>Text indicating that this zap is the one with the highest amount of sats.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Top hits" xml:space="preserve">
|
||||
<source>Top hits</source>
|
||||
<target>Top hits</target>
|
||||
<note>A label indicating that the notes being displayed below it are all top note search results</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Translate DMs" xml:space="preserve">
|
||||
<source>Translate DMs</source>
|
||||
<target>Translate DMs</target>
|
||||
@@ -1686,6 +1891,11 @@ You're all set!</target>
|
||||
<target>URL</target>
|
||||
<note>Example URL to LibreTranslate server</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unable to find a QR Code" xml:space="preserve">
|
||||
<source>Unable to find a QR Code</source>
|
||||
<target>Unable to find a QR Code</target>
|
||||
<note>Alert message letting user know a QR Code was not found.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Unfollow" xml:space="preserve">
|
||||
<source>Unfollow</source>
|
||||
<target>Unfollow</target>
|
||||
@@ -1757,6 +1967,11 @@ You're all set!</target>
|
||||
<target>View QR Code</target>
|
||||
<note>Button to switch to view users QR Code</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="View full profile" xml:space="preserve">
|
||||
<source>View full profile</source>
|
||||
<target>View full profile</target>
|
||||
<note>A button label that allows the user to see the full profile of the profile they are previewing</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="View multiple events per user" xml:space="preserve">
|
||||
<source>View multiple events per user</source>
|
||||
<target>View multiple events per user</target>
|
||||
@@ -1870,6 +2085,7 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
|
||||
<source>Zap</source>
|
||||
<target>Zap</target>
|
||||
<note>Accessibility label for zap button
|
||||
Button label that allows the user to zap (i.e. send a Bitcoin tip via the lightning network) the user shown on-screen
|
||||
Title of notification when a non-private zap is received.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Zap User" xml:space="preserve">
|
||||
@@ -1892,11 +2108,27 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
|
||||
<target>Zap attempt from connected wallet was canceled.</target>
|
||||
<note>Message to display when a zap from the user's connected wallet was canceled.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Zap failed" xml:space="preserve">
|
||||
<source>Zap failed</source>
|
||||
<target>Zap failed</target>
|
||||
<note>Button label indicating that a zap action was unsuccessful (i.e. the user was unable to send a Bitcoin tip via the lightning network to the user shown on-screen)
|
||||
Title of an alert indicating that a zap action failed</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Zap type" xml:space="preserve">
|
||||
<source>Zap type</source>
|
||||
<target>Zap type</target>
|
||||
<note>Text to indicate that the buttons below it is for choosing the type of zap to send.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Zapped!" xml:space="preserve">
|
||||
<source>Zapped!</source>
|
||||
<target>Zapped!</target>
|
||||
<note>Button label indicating that a zap action was successful (i.e. the user is successfully sent a Bitcoin tip via the lightning network to the user shown on-screen)</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Zapping" xml:space="preserve">
|
||||
<source>Zapping</source>
|
||||
<target>Zapping</target>
|
||||
<note>Button label indicating that a zap action is in progress (i.e. the user is currently sending a Bitcoin tip via the lightning network to the user shown on-screen)</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Zapping..." xml:space="preserve">
|
||||
<source>Zapping...</source>
|
||||
<target>Zapping...</target>
|
||||
@@ -1911,11 +2143,6 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
|
||||
Setting to enable Zap Local Notification
|
||||
Title for section in zap settings that controls general zap preferences.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="https://example.com" xml:space="preserve">
|
||||
<source>https://example.com</source>
|
||||
<target>https://example.com</target>
|
||||
<note>Placeholder as an example of what the user could set so that the link is opened when the status is tapped.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="https://example.com/pic.jpg" xml:space="preserve">
|
||||
<source>https://example.com/pic.jpg</source>
|
||||
<target>https://example.com/pic.jpg</target>
|
||||
@@ -2026,6 +2253,11 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
|
||||
<target>self</target>
|
||||
<note>Part of a larger sentence 'Replying to self' in US English. 'self' indicates that the user is replying to themself and no one else.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="translate.nostr.wine (DeepL, Pay with BTC)" xml:space="preserve">
|
||||
<source>translate.nostr.wine (DeepL, Pay with BTC)</source>
|
||||
<target>translate.nostr.wine (DeepL, Pay with BTC)</target>
|
||||
<note>Dropdown option for selecting translate.nostr.wine as the translation service.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="wallet" xml:space="preserve">
|
||||
<source>wallet</source>
|
||||
<target>wallet</target>
|
||||
@@ -2076,16 +2308,11 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
|
||||
<target>⚡</target>
|
||||
<note>Placeholder example for an emoji reaction</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="📋 Working" xml:space="preserve">
|
||||
<source>📋 Working</source>
|
||||
<target>📋 Working</target>
|
||||
<note>Placeholder as an example of what the user could set as their profile status.</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
<file original="damus/en-US.lproj/Localizable.stringsdict" source-language="en-US" target-language="en-US" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0" build-num="15A240d"/>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0.1" build-num="15A507"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="/followed_by_three_and_others:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
|
||||
@@ -2328,6 +2555,21 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
|
||||
<target>%2$@ sats</target>
|
||||
<note/>
|
||||
</trans-unit>
|
||||
<trans-unit id="/users_talking_about_it:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
|
||||
<source>%#@USERS@</source>
|
||||
<target>%#@USERS@</target>
|
||||
<note/>
|
||||
</trans-unit>
|
||||
<trans-unit id="/users_talking_about_it:dict/USERS:dict/one:dict/:string" xml:space="preserve">
|
||||
<source>%d user talking about it</source>
|
||||
<target>%d user talking about it</target>
|
||||
<note/>
|
||||
</trans-unit>
|
||||
<trans-unit id="/users_talking_about_it:dict/USERS:dict/other:dict/:string" xml:space="preserve">
|
||||
<source>%d users talking about it</source>
|
||||
<target>%d users talking about it</target>
|
||||
<note/>
|
||||
</trans-unit>
|
||||
<trans-unit id="/word_count:dict/NSStringLocalizedFormatKey:dict/:string" xml:space="preserve">
|
||||
<source>%#@WORDS@</source>
|
||||
<target>%#@WORDS@</target>
|
||||
@@ -2435,4 +2677,78 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
<file original="DamusNotificationService/InfoPlist.xcstrings" source-language="en-US" target-language="en-US" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0.1" build-num="15A507"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="CFBundleDisplayName" xml:space="preserve">
|
||||
<source>DamusNotificationService</source>
|
||||
<target state="new">DamusNotificationService</target>
|
||||
<note>Bundle display name</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="CFBundleName" xml:space="preserve">
|
||||
<source>DamusNotificationService</source>
|
||||
<target state="new">DamusNotificationService</target>
|
||||
<note>Bundle name</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="NSHumanReadableCopyright" xml:space="preserve">
|
||||
<source/>
|
||||
<target state="new"/>
|
||||
<note>Copyright (human-readable)</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
<file original="DamusNotificationService/Localizable.xcstrings" source-language="en-US" target-language="en-US" datatype="plaintext">
|
||||
<header>
|
||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="15.0.1" build-num="15A507"/>
|
||||
</header>
|
||||
<body>
|
||||
<trans-unit id="(Contents are encrypted)" xml:space="preserve">
|
||||
<source>(Contents are encrypted)</source>
|
||||
<target state="new">(Contents are encrypted)</target>
|
||||
<note>Label on push notification indicating that the contents of the message are encrypted</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Anonymous" xml:space="preserve">
|
||||
<source>Anonymous</source>
|
||||
<target state="new">Anonymous</target>
|
||||
<note>Placeholder display name of anonymous user.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Any" xml:space="preserve">
|
||||
<source>Any</source>
|
||||
<target state="new">Any</target>
|
||||
<note>Any amount of sats</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New message" xml:space="preserve">
|
||||
<source>New message</source>
|
||||
<target state="new">New message</target>
|
||||
<note>Title label for push notifications where a direct message was sent to the user</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="New note reaction" xml:space="preserve">
|
||||
<source>New note reaction</source>
|
||||
<target state="new">New note reaction</target>
|
||||
<note>Title label for push notifications where someone reacted to the user's post with a specific emoji</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Someone posted a note" xml:space="preserve">
|
||||
<source>Someone posted a note</source>
|
||||
<target state="new">Someone posted a note</target>
|
||||
<note>Title label for push notification where someone posted a note</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Someone reacted to your note" xml:space="preserve">
|
||||
<source>Someone reacted to your note</source>
|
||||
<target state="new">Someone reacted to your note</target>
|
||||
<note>Generic title label for push notifications where someone reacted to the user's post</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Someone reacted to your note with %@" xml:space="preserve">
|
||||
<source>Someone reacted to your note with %@</source>
|
||||
<target state="new">Someone reacted to your note with %@</target>
|
||||
<note>Body label for push notifications where someone reacted to the user's post with a specific emoji</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Someone zapped you ⚡️" xml:space="preserve">
|
||||
<source>Someone zapped you ⚡️</source>
|
||||
<target state="new">Someone zapped you ⚡️</target>
|
||||
<note>Title label for a push notification where someone zapped the user</note>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"sourceLanguage" : "en-US",
|
||||
"strings" : {
|
||||
"CFBundleDisplayName" : {
|
||||
"comment" : "Bundle display name",
|
||||
"extractionState" : "extracted_with_value",
|
||||
"localizations" : {
|
||||
"en-US" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "DamusNotificationService"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"CFBundleName" : {
|
||||
"comment" : "Bundle name",
|
||||
"extractionState" : "extracted_with_value",
|
||||
"localizations" : {
|
||||
"en-US" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "DamusNotificationService"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"NSHumanReadableCopyright" : {
|
||||
"comment" : "Copyright (human-readable)",
|
||||
"extractionState" : "extracted_with_value",
|
||||
"localizations" : {
|
||||
"en-US" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"sourceLanguage" : "en-US",
|
||||
"strings" : {
|
||||
"(Contents are encrypted)" : {
|
||||
"comment" : "Label on push notification indicating that the contents of the message are encrypted"
|
||||
},
|
||||
"Anonymous" : {
|
||||
"comment" : "Placeholder display name of anonymous user."
|
||||
},
|
||||
"Any" : {
|
||||
"comment" : "Any amount of sats"
|
||||
},
|
||||
"New message" : {
|
||||
"comment" : "Title label for push notifications where a direct message was sent to the user"
|
||||
},
|
||||
"New note reaction" : {
|
||||
"comment" : "Title label for push notifications where someone reacted to the user's post with a specific emoji"
|
||||
},
|
||||
"Someone posted a note" : {
|
||||
"comment" : "Title label for push notification where someone posted a note"
|
||||
},
|
||||
"Someone reacted to your note" : {
|
||||
"comment" : "Generic title label for push notifications where someone reacted to the user's post"
|
||||
},
|
||||
"Someone reacted to your note with %@" : {
|
||||
"comment" : "Body label for push notifications where someone reacted to the user's post with a specific emoji"
|
||||
},
|
||||
"Someone zapped you ⚡️" : {
|
||||
"comment" : "Title label for a push notification where someone zapped the user"
|
||||
}
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
@@ -5,10 +5,10 @@
|
||||
/* Privacy - Media Library Usage Description */
|
||||
"NSAppleMusicUsageDescription" = "Damus needs access to your media library for playback statuses";
|
||||
/* Privacy - Camera Usage Description */
|
||||
"NSCameraUsageDescription" = "Damus needs access to your camera if you want to upload photos from it";
|
||||
"NSCameraUsageDescription" = "Damus needs access to your camera in order to upload photos and scan QR codes.";
|
||||
/* Privacy - Face ID Usage Description */
|
||||
"NSFaceIDUsageDescription" = "Local authentication to access private key";
|
||||
/* Privacy - Microphone Usage Description */
|
||||
"NSMicrophoneUsageDescription" = "Damus needs access to your microphone if you want to upload recorded videos from it";
|
||||
"NSMicrophoneUsageDescription" = "Damus needs access to your microphone for creating video recording posts";
|
||||
/* Privacy - Photo Library Additions Usage Description */
|
||||
"NSPhotoLibraryAddUsageDescription" = "Granting Damus access to your photos allows you to save images.";
|
||||
|
||||
Binary file not shown.
@@ -258,6 +258,22 @@
|
||||
<string>%2$@ sats</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>users_talking_about_it</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
<string>%#@USERS@</string>
|
||||
<key>USERS</key>
|
||||
<dict>
|
||||
<key>NSStringFormatSpecTypeKey</key>
|
||||
<string>NSStringPluralRuleType</string>
|
||||
<key>NSStringFormatValueTypeKey</key>
|
||||
<string>d</string>
|
||||
<key>one</key>
|
||||
<string>%d user talking about it</string>
|
||||
<key>other</key>
|
||||
<string>%d users talking about it</string>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>word_count</key>
|
||||
<dict>
|
||||
<key>NSStringLocalizedFormatKey</key>
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
"project" : "damus.xcodeproj",
|
||||
"targetLocale" : "en-US",
|
||||
"toolInfo" : {
|
||||
"toolBuildNumber" : "15A240d",
|
||||
"toolBuildNumber" : "15A507",
|
||||
"toolID" : "com.apple.dt.xcode",
|
||||
"toolName" : "Xcode",
|
||||
"toolVersion" : "15.0"
|
||||
"toolVersion" : "15.0.1"
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user