Merge branch 'master' into user-cache
This commit is contained in:
2
.envrc
2
.envrc
@@ -1,4 +1,4 @@
|
|||||||
use nix
|
#use nix
|
||||||
|
|
||||||
export TODO_FILE=$PWD/TODO
|
export TODO_FILE=$PWD/TODO
|
||||||
|
|
||||||
|
|||||||
@@ -110,21 +110,6 @@ static inline int peek_char(struct cursor *cur, int ind) {
|
|||||||
return *(cur->p + ind);
|
return *(cur->p + ind);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int parse_digit(struct cursor *cur, int *digit) {
|
|
||||||
int c;
|
|
||||||
if ((c = peek_char(cur, 0)) == -1)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
c -= '0';
|
|
||||||
|
|
||||||
if (c >= 0 && c <= 9) {
|
|
||||||
*digit = c;
|
|
||||||
cur->p++;
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static inline int pull_byte(struct cursor *cur, u8 *byte) {
|
static inline int pull_byte(struct cursor *cur, u8 *byte) {
|
||||||
if (cur->p >= cur->end)
|
if (cur->p >= cur->end)
|
||||||
|
|||||||
@@ -12,6 +12,22 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
static int parse_digit(struct cursor *cur, int *digit) {
|
||||||
|
int c;
|
||||||
|
if ((c = peek_char(cur, 0)) == -1)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
c -= '0';
|
||||||
|
|
||||||
|
if (c >= 0 && c <= 9) {
|
||||||
|
*digit = c;
|
||||||
|
cur->p++;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static int parse_mention_index(struct cursor *cur, struct block *block) {
|
static int parse_mention_index(struct cursor *cur, struct block *block) {
|
||||||
int d1, d2, d3, ind;
|
int d1, d2, d3, ind;
|
||||||
const u8 *start = cur->p;
|
const u8 *start = cur->p;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ bool hex_decode(const char *str, size_t slen, void *buf, size_t bufsize);
|
|||||||
/**
|
/**
|
||||||
* hex_encode - Create a nul-terminated hex string
|
* hex_encode - Create a nul-terminated hex string
|
||||||
* @buf: the buffer to read the data from
|
* @buf: the buffer to read the data from
|
||||||
* @bufsize: the length of @buf
|
* @bufsize: the length of buf
|
||||||
* @dest: the string to fill
|
* @dest: the string to fill
|
||||||
* @destsize: the max size of the string
|
* @destsize: the max size of the string
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -53,6 +53,8 @@
|
|||||||
4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; };
|
4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; };
|
||||||
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */; };
|
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */; };
|
||||||
4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F372871EDE300040376 /* DirectMessageModel.swift */; };
|
4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F372871EDE300040376 /* DirectMessageModel.swift */; };
|
||||||
|
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C28595F2A12A2BE004746F7 /* SupporterBadge.swift */; };
|
||||||
|
4C2859622A12A7F0004746F7 /* GoldSupportGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2859612A12A7F0004746F7 /* GoldSupportGradient.swift */; };
|
||||||
4C285C8228385570008A31F1 /* CarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8128385570008A31F1 /* CarouselView.swift */; };
|
4C285C8228385570008A31F1 /* CarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8128385570008A31F1 /* CarouselView.swift */; };
|
||||||
4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8328385690008A31F1 /* CreateAccountView.swift */; };
|
4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8328385690008A31F1 /* CreateAccountView.swift */; };
|
||||||
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C85283892E7008A31F1 /* CreateAccountModel.swift */; };
|
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C85283892E7008A31F1 /* CreateAccountModel.swift */; };
|
||||||
@@ -137,13 +139,19 @@
|
|||||||
4C75EFB92804A2740006080F /* EventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFB82804A2740006080F /* EventView.swift */; };
|
4C75EFB92804A2740006080F /* EventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFB82804A2740006080F /* EventView.swift */; };
|
||||||
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFBA2804A34C0006080F /* ProofOfWork.swift */; };
|
4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFBA2804A34C0006080F /* ProofOfWork.swift */; };
|
||||||
4C7D09592A05BEAD00943473 /* KeyboardVisible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09582A05BEAD00943473 /* KeyboardVisible.swift */; };
|
4C7D09592A05BEAD00943473 /* KeyboardVisible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09582A05BEAD00943473 /* KeyboardVisible.swift */; };
|
||||||
|
4C7D095F2A098C5D00943473 /* ConnectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D095C2A098C5D00943473 /* ConnectWalletView.swift */; };
|
||||||
|
4C7D09602A098C5D00943473 /* WalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D095D2A098C5D00943473 /* WalletView.swift */; };
|
||||||
|
4C7D09622A098D0E00943473 /* WalletConnect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09612A098D0E00943473 /* WalletConnect.swift */; };
|
||||||
4C7D09662A0AE62100943473 /* AlbyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09652A0AE62100943473 /* AlbyButton.swift */; };
|
4C7D09662A0AE62100943473 /* AlbyButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09652A0AE62100943473 /* AlbyButton.swift */; };
|
||||||
|
4C7D09682A0AE9B200943473 /* NWCScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09672A0AE9B200943473 /* NWCScannerView.swift */; };
|
||||||
4C7D096D2A0AEA0400943473 /* CodeScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D096A2A0AEA0400943473 /* CodeScanner.swift */; };
|
4C7D096D2A0AEA0400943473 /* CodeScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D096A2A0AEA0400943473 /* CodeScanner.swift */; };
|
||||||
4C7D096E2A0AEA0400943473 /* ScannerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D096B2A0AEA0400943473 /* ScannerCoordinator.swift */; };
|
4C7D096E2A0AEA0400943473 /* ScannerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D096B2A0AEA0400943473 /* ScannerCoordinator.swift */; };
|
||||||
4C7D096F2A0AEA0400943473 /* ScannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D096C2A0AEA0400943473 /* ScannerViewController.swift */; };
|
4C7D096F2A0AEA0400943473 /* ScannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D096C2A0AEA0400943473 /* ScannerViewController.swift */; };
|
||||||
4C7D09722A0AEF5E00943473 /* DamusGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09712A0AEF5E00943473 /* DamusGradient.swift */; };
|
4C7D09722A0AEF5E00943473 /* DamusGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09712A0AEF5E00943473 /* DamusGradient.swift */; };
|
||||||
4C7D09742A0AEF9000943473 /* AlbyGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09732A0AEF9000943473 /* AlbyGradient.swift */; };
|
4C7D09742A0AEF9000943473 /* AlbyGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09732A0AEF9000943473 /* AlbyGradient.swift */; };
|
||||||
4C7D09762A0AF19E00943473 /* FillAndStroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09752A0AF19E00943473 /* FillAndStroke.swift */; };
|
4C7D09762A0AF19E00943473 /* FillAndStroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09752A0AF19E00943473 /* FillAndStroke.swift */; };
|
||||||
|
4C7D09782A0B0CC900943473 /* WalletModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D09772A0B0CC900943473 /* WalletModel.swift */; };
|
||||||
|
4C7D097E2A0C58B900943473 /* WalletConnectTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7D097D2A0C58B900943473 /* WalletConnectTests.swift */; };
|
||||||
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FF7D42823313F009601DB /* Mentions.swift */; };
|
4C7FF7D52823313F009601DB /* Mentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C7FF7D42823313F009601DB /* Mentions.swift */; };
|
||||||
4C8682872814DE470026224F /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8682862814DE470026224F /* ProfileView.swift */; };
|
4C8682872814DE470026224F /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8682862814DE470026224F /* ProfileView.swift */; };
|
||||||
4C8D00C829DF791C0036AF10 /* CompatibleAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */; };
|
4C8D00C829DF791C0036AF10 /* CompatibleAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */; };
|
||||||
@@ -265,6 +273,7 @@
|
|||||||
501F8C5A29FF70F5001AFC1D /* ProfileDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */; };
|
501F8C5A29FF70F5001AFC1D /* ProfileDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */; };
|
||||||
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; };
|
50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; };
|
||||||
50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B5685229F97CB400A23243 /* CredentialHandler.swift */; };
|
50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B5685229F97CB400A23243 /* CredentialHandler.swift */; };
|
||||||
|
50DA11262A16A23F00236234 /* Launch.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50DA11252A16A23F00236234 /* Launch.storyboard */; };
|
||||||
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; };
|
5C42E78C29DB76D90086AAC1 /* EmptyUserSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */; };
|
||||||
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
|
5C513FBA297F72980072348F /* CustomPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FB9297F72980072348F /* CustomPicker.swift */; };
|
||||||
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
|
5C513FCC2984ACA60072348F /* QRCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C513FCB2984ACA60072348F /* QRCodeView.swift */; };
|
||||||
@@ -442,6 +451,8 @@
|
|||||||
4C216F33286F5ACD00040376 /* DMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMView.swift; sourceTree = "<group>"; };
|
4C216F33286F5ACD00040376 /* DMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMView.swift; sourceTree = "<group>"; };
|
||||||
4C216F352870A9A700040376 /* InputDismissKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDismissKeyboard.swift; sourceTree = "<group>"; };
|
4C216F352870A9A700040376 /* InputDismissKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDismissKeyboard.swift; sourceTree = "<group>"; };
|
||||||
4C216F372871EDE300040376 /* DirectMessageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessageModel.swift; sourceTree = "<group>"; };
|
4C216F372871EDE300040376 /* DirectMessageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessageModel.swift; sourceTree = "<group>"; };
|
||||||
|
4C28595F2A12A2BE004746F7 /* SupporterBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupporterBadge.swift; sourceTree = "<group>"; };
|
||||||
|
4C2859612A12A7F0004746F7 /* GoldSupportGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoldSupportGradient.swift; sourceTree = "<group>"; };
|
||||||
4C285C8128385570008A31F1 /* CarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselView.swift; sourceTree = "<group>"; };
|
4C285C8128385570008A31F1 /* CarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselView.swift; sourceTree = "<group>"; };
|
||||||
4C285C8328385690008A31F1 /* CreateAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateAccountView.swift; sourceTree = "<group>"; };
|
4C285C8328385690008A31F1 /* CreateAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateAccountView.swift; sourceTree = "<group>"; };
|
||||||
4C285C85283892E7008A31F1 /* CreateAccountModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateAccountModel.swift; sourceTree = "<group>"; };
|
4C285C85283892E7008A31F1 /* CreateAccountModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateAccountModel.swift; sourceTree = "<group>"; };
|
||||||
@@ -556,13 +567,19 @@
|
|||||||
4C75EFB82804A2740006080F /* EventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventView.swift; sourceTree = "<group>"; };
|
4C75EFB82804A2740006080F /* EventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventView.swift; sourceTree = "<group>"; };
|
||||||
4C75EFBA2804A34C0006080F /* ProofOfWork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProofOfWork.swift; sourceTree = "<group>"; };
|
4C75EFBA2804A34C0006080F /* ProofOfWork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProofOfWork.swift; sourceTree = "<group>"; };
|
||||||
4C7D09582A05BEAD00943473 /* KeyboardVisible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardVisible.swift; sourceTree = "<group>"; };
|
4C7D09582A05BEAD00943473 /* KeyboardVisible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardVisible.swift; sourceTree = "<group>"; };
|
||||||
|
4C7D095C2A098C5D00943473 /* ConnectWalletView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectWalletView.swift; sourceTree = "<group>"; };
|
||||||
|
4C7D095D2A098C5D00943473 /* WalletView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletView.swift; sourceTree = "<group>"; };
|
||||||
|
4C7D09612A098D0E00943473 /* WalletConnect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnect.swift; sourceTree = "<group>"; };
|
||||||
4C7D09652A0AE62100943473 /* AlbyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbyButton.swift; sourceTree = "<group>"; };
|
4C7D09652A0AE62100943473 /* AlbyButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbyButton.swift; sourceTree = "<group>"; };
|
||||||
|
4C7D09672A0AE9B200943473 /* NWCScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NWCScannerView.swift; sourceTree = "<group>"; };
|
||||||
4C7D096A2A0AEA0400943473 /* CodeScanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodeScanner.swift; sourceTree = "<group>"; };
|
4C7D096A2A0AEA0400943473 /* CodeScanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodeScanner.swift; sourceTree = "<group>"; };
|
||||||
4C7D096B2A0AEA0400943473 /* ScannerCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScannerCoordinator.swift; sourceTree = "<group>"; };
|
4C7D096B2A0AEA0400943473 /* ScannerCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScannerCoordinator.swift; sourceTree = "<group>"; };
|
||||||
4C7D096C2A0AEA0400943473 /* ScannerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScannerViewController.swift; sourceTree = "<group>"; };
|
4C7D096C2A0AEA0400943473 /* ScannerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScannerViewController.swift; sourceTree = "<group>"; };
|
||||||
4C7D09712A0AEF5E00943473 /* DamusGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusGradient.swift; sourceTree = "<group>"; };
|
4C7D09712A0AEF5E00943473 /* DamusGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusGradient.swift; sourceTree = "<group>"; };
|
||||||
4C7D09732A0AEF9000943473 /* AlbyGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbyGradient.swift; sourceTree = "<group>"; };
|
4C7D09732A0AEF9000943473 /* AlbyGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbyGradient.swift; sourceTree = "<group>"; };
|
||||||
4C7D09752A0AF19E00943473 /* FillAndStroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FillAndStroke.swift; sourceTree = "<group>"; };
|
4C7D09752A0AF19E00943473 /* FillAndStroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FillAndStroke.swift; sourceTree = "<group>"; };
|
||||||
|
4C7D09772A0B0CC900943473 /* WalletModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletModel.swift; sourceTree = "<group>"; };
|
||||||
|
4C7D097D2A0C58B900943473 /* WalletConnectTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConnectTests.swift; sourceTree = "<group>"; };
|
||||||
4C7FF7D42823313F009601DB /* Mentions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mentions.swift; sourceTree = "<group>"; };
|
4C7FF7D42823313F009601DB /* Mentions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mentions.swift; sourceTree = "<group>"; };
|
||||||
4C8682862814DE470026224F /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
|
4C8682862814DE470026224F /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
|
||||||
4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompatibleAttribute.swift; sourceTree = "<group>"; };
|
4C8D00C729DF791C0036AF10 /* CompatibleAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompatibleAttribute.swift; sourceTree = "<group>"; };
|
||||||
@@ -692,6 +709,7 @@
|
|||||||
501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDatabase.swift; sourceTree = "<group>"; };
|
501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDatabase.swift; sourceTree = "<group>"; };
|
||||||
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; };
|
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; };
|
||||||
50B5685229F97CB400A23243 /* CredentialHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialHandler.swift; sourceTree = "<group>"; };
|
50B5685229F97CB400A23243 /* CredentialHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialHandler.swift; sourceTree = "<group>"; };
|
||||||
|
50DA11252A16A23F00236234 /* Launch.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Launch.storyboard; sourceTree = "<group>"; };
|
||||||
5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = "<group>"; };
|
5C42E78B29DB76D90086AAC1 /* EmptyUserSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyUserSearchView.swift; sourceTree = "<group>"; };
|
||||||
5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; };
|
5C513FB9297F72980072348F /* CustomPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomPicker.swift; sourceTree = "<group>"; };
|
||||||
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
|
5C513FCB2984ACA60072348F /* QRCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeView.swift; sourceTree = "<group>"; };
|
||||||
@@ -870,6 +888,7 @@
|
|||||||
4C54AA0629A540BA003E4487 /* NotificationsModel.swift */,
|
4C54AA0629A540BA003E4487 /* NotificationsModel.swift */,
|
||||||
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */,
|
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */,
|
||||||
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */,
|
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */,
|
||||||
|
4C7D09772A0B0CC900943473 /* WalletModel.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -929,6 +948,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
4C7D09692A0AEA0400943473 /* CodeScanner */,
|
4C7D09692A0AEA0400943473 /* CodeScanner */,
|
||||||
|
4C7D095A2A098C5C00943473 /* Wallet */,
|
||||||
4C8D1A6D29F31E4100ACDF75 /* Buttons */,
|
4C8D1A6D29F31E4100ACDF75 /* Buttons */,
|
||||||
4C1A9A1B29DDCF8B00516EAC /* Settings */,
|
4C1A9A1B29DDCF8B00516EAC /* Settings */,
|
||||||
4CFF8F6129CC9A80008DB934 /* Images */,
|
4CFF8F6129CC9A80008DB934 /* Images */,
|
||||||
@@ -984,6 +1004,7 @@
|
|||||||
4CF0ABD529817F5B00D66079 /* ReportView.swift */,
|
4CF0ABD529817F5B00D66079 /* ReportView.swift */,
|
||||||
4CF0ABE42981EE0C00D66079 /* EULAView.swift */,
|
4CF0ABE42981EE0C00D66079 /* EULAView.swift */,
|
||||||
3AA247FE297E3D900090C62D /* RepostsView.swift */,
|
3AA247FE297E3D900090C62D /* RepostsView.swift */,
|
||||||
|
50DA11252A16A23F00236234 /* Launch.storyboard */,
|
||||||
5C513FCB2984ACA60072348F /* QRCodeView.swift */,
|
5C513FCB2984ACA60072348F /* QRCodeView.swift */,
|
||||||
643EA5C7296B764E005081BB /* RelayFilterView.swift */,
|
643EA5C7296B764E005081BB /* RelayFilterView.swift */,
|
||||||
);
|
);
|
||||||
@@ -1012,6 +1033,16 @@
|
|||||||
path = Nostr;
|
path = Nostr;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
4C7D095A2A098C5C00943473 /* Wallet */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4C7D095C2A098C5D00943473 /* ConnectWalletView.swift */,
|
||||||
|
4C7D095D2A098C5D00943473 /* WalletView.swift */,
|
||||||
|
4C7D09672A0AE9B200943473 /* NWCScannerView.swift */,
|
||||||
|
);
|
||||||
|
path = Wallet;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
4C7D09692A0AEA0400943473 /* CodeScanner */ = {
|
4C7D09692A0AEA0400943473 /* CodeScanner */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -1027,6 +1058,7 @@
|
|||||||
children = (
|
children = (
|
||||||
4C7D09712A0AEF5E00943473 /* DamusGradient.swift */,
|
4C7D09712A0AEF5E00943473 /* DamusGradient.swift */,
|
||||||
4C7D09732A0AEF9000943473 /* AlbyGradient.swift */,
|
4C7D09732A0AEF9000943473 /* AlbyGradient.swift */,
|
||||||
|
4C2859612A12A7F0004746F7 /* GoldSupportGradient.swift */,
|
||||||
);
|
);
|
||||||
path = Gradients;
|
path = Gradients;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1034,6 +1066,7 @@
|
|||||||
4C7FF7D628233637009601DB /* Util */ = {
|
4C7FF7D628233637009601DB /* Util */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
4C7D09612A098D0E00943473 /* WalletConnect.swift */,
|
||||||
4C198DF329F88D23004C165C /* Images */,
|
4C198DF329F88D23004C165C /* Images */,
|
||||||
4C198DEA29F88C6B004C165C /* BlurHash */,
|
4C198DEA29F88C6B004C165C /* BlurHash */,
|
||||||
4CE4F0F329D779B5005914DB /* PostBox.swift */,
|
4CE4F0F329D779B5005914DB /* PostBox.swift */,
|
||||||
@@ -1203,6 +1236,7 @@
|
|||||||
4CE4F0F729DB7399005914DB /* ThiccDivider.swift */,
|
4CE4F0F729DB7399005914DB /* ThiccDivider.swift */,
|
||||||
4C1A9A2229DDDB8100516EAC /* IconLabel.swift */,
|
4C1A9A2229DDDB8100516EAC /* IconLabel.swift */,
|
||||||
4C8D00C929DF80350036AF10 /* TruncatedText.swift */,
|
4C8D00C929DF80350036AF10 /* TruncatedText.swift */,
|
||||||
|
4C28595F2A12A2BE004746F7 /* SupporterBadge.swift */,
|
||||||
);
|
);
|
||||||
path = Components;
|
path = Components;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1262,6 +1296,7 @@
|
|||||||
4CE6DEF627F7A08200C66700 /* damusTests */ = {
|
4CE6DEF627F7A08200C66700 /* damusTests */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
4C7D097D2A0C58B900943473 /* WalletConnectTests.swift */,
|
||||||
F944F56C29EA9CB20067B3BF /* Models */,
|
F944F56C29EA9CB20067B3BF /* Models */,
|
||||||
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */,
|
50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */,
|
||||||
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */,
|
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */,
|
||||||
@@ -1546,6 +1581,7 @@
|
|||||||
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */,
|
3ACB685F297633BC00C46468 /* Localizable.strings in Resources */,
|
||||||
4CE6DEEE27F7A08200C66700 /* Preview Assets.xcassets in Resources */,
|
4CE6DEEE27F7A08200C66700 /* Preview Assets.xcassets in Resources */,
|
||||||
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */,
|
3ACB685C297633BC00C46468 /* InfoPlist.strings in Resources */,
|
||||||
|
50DA11262A16A23F00236234 /* Launch.storyboard in Resources */,
|
||||||
4CE6DEEB27F7A08200C66700 /* Assets.xcassets in Resources */,
|
4CE6DEEB27F7A08200C66700 /* Assets.xcassets in Resources */,
|
||||||
4C198DF129F88C6B004C165C /* License.txt in Resources */,
|
4C198DF129F88C6B004C165C /* License.txt in Resources */,
|
||||||
4C198DF029F88C6B004C165C /* Readme.md in Resources */,
|
4C198DF029F88C6B004C165C /* Readme.md in Resources */,
|
||||||
@@ -1639,6 +1675,7 @@
|
|||||||
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */,
|
F7F0BA25297892BD009531F3 /* SwipeToDismiss.swift in Sources */,
|
||||||
4C8D00CA29DF80350036AF10 /* TruncatedText.swift in Sources */,
|
4C8D00CA29DF80350036AF10 /* TruncatedText.swift in Sources */,
|
||||||
4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */,
|
4C9BB83429C12D9900FC4E37 /* EventProfileName.swift in Sources */,
|
||||||
|
4C7D09602A098C5D00943473 /* WalletView.swift in Sources */,
|
||||||
4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */,
|
4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */,
|
||||||
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
|
4C75EFB328049D640006080F /* NostrEvent.swift in Sources */,
|
||||||
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
|
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */,
|
||||||
@@ -1682,6 +1719,7 @@
|
|||||||
4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */,
|
4C3AC79F2833115300E1F516 /* FollowButtonView.swift in Sources */,
|
||||||
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */,
|
4CC7AAE7297EFA7B00430951 /* Zap.swift in Sources */,
|
||||||
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
|
4C3BEFD22819DB9B00B3DE84 /* ProfileModel.swift in Sources */,
|
||||||
|
4C7D09682A0AE9B200943473 /* NWCScannerView.swift in Sources */,
|
||||||
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
|
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */,
|
||||||
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */,
|
7C95CAEE299DCEF1009DCB67 /* KFOptionSetter+.swift in Sources */,
|
||||||
4C7D09722A0AEF5E00943473 /* DamusGradient.swift in Sources */,
|
4C7D09722A0AEF5E00943473 /* DamusGradient.swift in Sources */,
|
||||||
@@ -1708,10 +1746,12 @@
|
|||||||
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */,
|
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */,
|
||||||
4C363A94282704FA006E126D /* Post.swift in Sources */,
|
4C363A94282704FA006E126D /* Post.swift in Sources */,
|
||||||
4C216F32286E388800040376 /* DMChatView.swift in Sources */,
|
4C216F32286E388800040376 /* DMChatView.swift in Sources */,
|
||||||
|
4C7D09782A0B0CC900943473 /* WalletModel.swift in Sources */,
|
||||||
4C7D09662A0AE62100943473 /* AlbyButton.swift in Sources */,
|
4C7D09662A0AE62100943473 /* AlbyButton.swift in Sources */,
|
||||||
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */,
|
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */,
|
||||||
4CFF8F6329CC9AD7008DB934 /* ImageContextMenuModifier.swift in Sources */,
|
4CFF8F6329CC9AD7008DB934 /* ImageContextMenuModifier.swift in Sources */,
|
||||||
4C54AA0A29A55429003E4487 /* EventGroup.swift in Sources */,
|
4C54AA0A29A55429003E4487 /* EventGroup.swift in Sources */,
|
||||||
|
4C7D09622A098D0E00943473 /* WalletConnect.swift in Sources */,
|
||||||
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
|
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
|
||||||
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
|
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
|
||||||
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */,
|
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */,
|
||||||
@@ -1721,12 +1761,14 @@
|
|||||||
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
|
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
|
||||||
F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */,
|
F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */,
|
||||||
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
|
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
|
||||||
|
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */,
|
||||||
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
||||||
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
|
4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */,
|
||||||
4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */,
|
4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */,
|
||||||
4CE1399229F0666100AC6A0B /* ShareActionButton.swift in Sources */,
|
4CE1399229F0666100AC6A0B /* ShareActionButton.swift in Sources */,
|
||||||
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */,
|
4C42812C298C848200DBF26F /* TranslateView.swift in Sources */,
|
||||||
4C363A9C282838B9006E126D /* EventRef.swift in Sources */,
|
4C363A9C282838B9006E126D /* EventRef.swift in Sources */,
|
||||||
|
4C7D095F2A098C5D00943473 /* ConnectWalletView.swift in Sources */,
|
||||||
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */,
|
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */,
|
||||||
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */,
|
4CD7641B28A1641400B6928F /* EndBlock.swift in Sources */,
|
||||||
4C3EA66528FF5F6800C48A62 /* mem.c in Sources */,
|
4C3EA66528FF5F6800C48A62 /* mem.c in Sources */,
|
||||||
@@ -1829,6 +1871,7 @@
|
|||||||
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
|
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */,
|
||||||
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
|
4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */,
|
||||||
4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */,
|
4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */,
|
||||||
|
4C2859622A12A7F0004746F7 /* GoldSupportGradient.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -1847,6 +1890,7 @@
|
|||||||
3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */,
|
3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */,
|
||||||
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
|
4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */,
|
||||||
4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */,
|
4C363A9E2828A822006E126D /* ReplyTests.swift in Sources */,
|
||||||
|
4C7D097E2A0C58B900943473 /* WalletConnectTests.swift in Sources */,
|
||||||
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */,
|
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */,
|
||||||
4CB8839A297322D200DC99E7 /* DMTests.swift in Sources */,
|
4CB8839A297322D200DC99E7 /* DMTests.swift in Sources */,
|
||||||
F944F56E29EA9CCC0067B3BF /* DamusParseContentTests.swift in Sources */,
|
F944F56E29EA9CCC0067B3BF /* DamusParseContentTests.swift in Sources */,
|
||||||
@@ -2120,7 +2164,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 24;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -2135,6 +2179,7 @@
|
|||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
|
INFOPLIST_KEY_UILaunchStoryboardName = Launch.storyboard;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
@@ -2167,7 +2212,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 24;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\"";
|
||||||
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
DEVELOPMENT_TEAM = XK7H4JAB3D;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -2182,6 +2227,7 @@
|
|||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
|
INFOPLIST_KEY_UILaunchStoryboardName = Launch.storyboard;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
|||||||
98
damus.xcodeproj/xcshareddata/xcschemes/Release.xcscheme
Normal file
98
damus.xcodeproj/xcshareddata/xcschemes/Release.xcscheme
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1420"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
|
||||||
|
BuildableName = "damus.app"
|
||||||
|
BlueprintName = "damus"
|
||||||
|
ReferencedContainer = "container:damus.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "4CE6DEF227F7A08200C66700"
|
||||||
|
BuildableName = "damusTests.xctest"
|
||||||
|
BlueprintName = "damusTests"
|
||||||
|
ReferencedContainer = "container:damus.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "4CE6DEFC27F7A08200C66700"
|
||||||
|
BuildableName = "damusUITests.xctest"
|
||||||
|
BlueprintName = "damusUITests"
|
||||||
|
ReferencedContainer = "container:damus.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
|
||||||
|
BuildableName = "damus.app"
|
||||||
|
BlueprintName = "damus"
|
||||||
|
ReferencedContainer = "container:damus.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "4CE6DEE227F7A08100C66700"
|
||||||
|
BuildableName = "damus.app"
|
||||||
|
BlueprintName = "damus"
|
||||||
|
ReferencedContainer = "container:damus.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
12
damus/Assets.xcassets/gradient.imageset/Contents.json
vendored
Normal file
12
damus/Assets.xcassets/gradient.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "gradient.jpg",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
damus/Assets.xcassets/gradient.imageset/gradient.jpg
vendored
Normal file
BIN
damus/Assets.xcassets/gradient.imageset/gradient.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 118 KiB |
@@ -14,9 +14,13 @@ fileprivate let damus_grad = [damus_grad_c1, damus_grad_c2, damus_grad_c3]
|
|||||||
|
|
||||||
struct DamusGradient: View {
|
struct DamusGradient: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
LinearGradient(colors: damus_grad, startPoint: .bottomLeading, endPoint: .topTrailing)
|
DamusGradient.gradient
|
||||||
.edgesIgnoringSafeArea([.top,.bottom])
|
.edgesIgnoringSafeArea([.top,.bottom])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static var gradient: LinearGradient {
|
||||||
|
LinearGradient(colors: damus_grad, startPoint: .bottomLeading, endPoint: .topTrailing)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DamusGradient_Previews: PreviewProvider {
|
struct DamusGradient_Previews: PreviewProvider {
|
||||||
|
|||||||
29
damus/Components/Gradients/GoldSupportGradient.swift
Normal file
29
damus/Components/Gradients/GoldSupportGradient.swift
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
//
|
||||||
|
// GoldSupportGradient.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-05-15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
fileprivate let gold_grad_c1 = hex_col(r: 226, g: 168, b: 0)
|
||||||
|
fileprivate let gold_grad_c2 = hex_col(r: 249, g: 243, b: 100)
|
||||||
|
|
||||||
|
fileprivate let gold_grad = [gold_grad_c2, gold_grad_c1]
|
||||||
|
|
||||||
|
let GoldGradient: LinearGradient =
|
||||||
|
LinearGradient(colors: gold_grad, startPoint: .bottomLeading, endPoint: .topTrailing)
|
||||||
|
|
||||||
|
struct GoldGradientView: View {
|
||||||
|
var body: some View {
|
||||||
|
GoldGradient
|
||||||
|
.edgesIgnoringSafeArea([.top,.bottom])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GoldGradientView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
GoldGradientView()
|
||||||
|
}
|
||||||
|
}
|
||||||
73
damus/Components/SupporterBadge.swift
Normal file
73
damus/Components/SupporterBadge.swift
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
//
|
||||||
|
// SupporterBadge.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-05-15.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SupporterBadge: View {
|
||||||
|
let percent: Int
|
||||||
|
|
||||||
|
let size: CGFloat = 17
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if percent < 100 {
|
||||||
|
Image("star.fill")
|
||||||
|
.resizable()
|
||||||
|
.frame(width:size, height:size)
|
||||||
|
.foregroundColor(support_level_color(percent))
|
||||||
|
} else {
|
||||||
|
Image("star.fill")
|
||||||
|
.resizable()
|
||||||
|
.frame(width:size, height:size)
|
||||||
|
.foregroundStyle(GoldGradient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func support_level_color(_ percent: Int) -> Color {
|
||||||
|
if percent == 0 {
|
||||||
|
return .gray
|
||||||
|
}
|
||||||
|
|
||||||
|
let percent_f = Double(percent) / 100.0
|
||||||
|
let cutoff = 0.5
|
||||||
|
let h = cutoff + (percent_f * cutoff); // Hue (note 0.2 = Green, see huge chart below)
|
||||||
|
let s = 0.9; // Saturation
|
||||||
|
let b = 0.9; // Brightness
|
||||||
|
|
||||||
|
return Color(hue: h, saturation: s, brightness: b)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SupporterBadge_Previews: PreviewProvider {
|
||||||
|
static func Level(_ p: Int) -> some View {
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
SupporterBadge(percent: p)
|
||||||
|
.frame(width: 50)
|
||||||
|
Text(verbatim: p.formatted())
|
||||||
|
.frame(width: 50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Level(1)
|
||||||
|
Level(10)
|
||||||
|
Level(20)
|
||||||
|
Level(30)
|
||||||
|
Level(40)
|
||||||
|
Level(50)
|
||||||
|
}
|
||||||
|
Level(60)
|
||||||
|
Level(70)
|
||||||
|
Level(80)
|
||||||
|
Level(90)
|
||||||
|
Level(100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -23,45 +23,97 @@ struct ZappingEvent {
|
|||||||
let event: NostrEvent
|
let event: NostrEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ZapButtonModel: ObservableObject {
|
||||||
|
var invoice: String? = nil
|
||||||
|
@Published var zapping: String = ""
|
||||||
|
@Published var showing_select_wallet: Bool = false
|
||||||
|
@Published var showing_zap_customizer: Bool = false
|
||||||
|
}
|
||||||
|
|
||||||
struct ZapButton: View {
|
struct ZapButton: View {
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
let event: NostrEvent
|
let event: NostrEvent
|
||||||
let lnurl: String
|
let lnurl: String
|
||||||
|
|
||||||
@ObservedObject var bar: ActionBarModel
|
@ObservedObject var zaps: ZapsDataModel
|
||||||
|
@StateObject var button: ZapButtonModel = ZapButtonModel()
|
||||||
|
|
||||||
@State var zapping: Bool = false
|
var our_zap: Zapping? {
|
||||||
@State var invoice: String = ""
|
zaps.zaps.first(where: { z in z.request.pubkey == damus_state.pubkey })
|
||||||
@State var showing_select_wallet: Bool = false
|
|
||||||
@State var showing_zap_customizer: Bool = false
|
|
||||||
@State var is_charging: Bool = false
|
|
||||||
|
|
||||||
var zap_img: String {
|
|
||||||
if bar.zapped {
|
|
||||||
return "bolt.fill"
|
|
||||||
}
|
|
||||||
|
|
||||||
if !zapping {
|
|
||||||
return "bolt"
|
|
||||||
}
|
|
||||||
|
|
||||||
return "bolt.fill"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var zap_color: Color? {
|
var zap_img: String {
|
||||||
if bar.zapped {
|
switch our_zap {
|
||||||
return Color.orange
|
case .none:
|
||||||
|
return "bolt"
|
||||||
|
case .zap:
|
||||||
|
return "bolt.fill"
|
||||||
|
case .pending:
|
||||||
|
return "bolt.fill"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var zap_color: Color {
|
||||||
|
if our_zap == nil {
|
||||||
|
return Color.gray
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_charging {
|
// always orange !
|
||||||
|
return Color.orange
|
||||||
|
/*
|
||||||
|
if our_zap.is_paid {
|
||||||
|
return Color.orange
|
||||||
|
} else {
|
||||||
return Color.yellow
|
return Color.yellow
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
if !zapping {
|
}
|
||||||
return nil
|
|
||||||
|
func tap() {
|
||||||
|
guard let our_zap else {
|
||||||
|
send_zap(damus_state: damus_state, event: event, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return Color.yellow
|
// we've tapped and we have a zap already... cancel if we can
|
||||||
|
switch our_zap {
|
||||||
|
case .zap:
|
||||||
|
// can't undo a zap we've already sent
|
||||||
|
// if we want to send more zaps we will need to long-press
|
||||||
|
print("cancel_zap: we already have a real zap, can't cancel")
|
||||||
|
break
|
||||||
|
case .pending(let pzap):
|
||||||
|
guard let res = cancel_zap(zap: pzap, box: damus_state.postbox, zapcache: damus_state.zaps, evcache: damus_state.events) else {
|
||||||
|
|
||||||
|
UIImpactFeedbackGenerator(style: .soft).impactOccurred()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UIImpactFeedbackGenerator(style: .rigid).impactOccurred()
|
||||||
|
|
||||||
|
switch res {
|
||||||
|
case .send_err(let cancel_err):
|
||||||
|
switch cancel_err {
|
||||||
|
case .nothing_to_cancel:
|
||||||
|
print("cancel_zap: got nothing_to_cancel in pending")
|
||||||
|
break
|
||||||
|
case .not_delayed:
|
||||||
|
print("cancel_zap: got not_delayed in pending")
|
||||||
|
break
|
||||||
|
case .too_late:
|
||||||
|
print("cancel_zap: got too_late in pending")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case .already_confirmed:
|
||||||
|
print("cancel_zap: got already_confirmed in pending")
|
||||||
|
break
|
||||||
|
case .not_nwc:
|
||||||
|
print("cancel_zap: got not_nwc in pending")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -69,37 +121,28 @@ struct ZapButton: View {
|
|||||||
Button(action: {
|
Button(action: {
|
||||||
}, label: {
|
}, label: {
|
||||||
Image(systemName: zap_img)
|
Image(systemName: zap_img)
|
||||||
.foregroundColor(zap_color == nil ? Color.gray : zap_color!)
|
.foregroundColor(zap_color)
|
||||||
.font(.footnote.weight(.medium))
|
.font(.footnote.weight(.medium))
|
||||||
})
|
})
|
||||||
.simultaneousGesture(LongPressGesture().onEnded {_ in
|
|
||||||
guard !zapping else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.showing_zap_customizer = true
|
|
||||||
})
|
|
||||||
.highPriorityGesture(TapGesture().onEnded {_ in
|
|
||||||
guard !zapping else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
send_zap(damus_state: damus_state, event: event, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type)
|
|
||||||
self.zapping = true
|
|
||||||
})
|
|
||||||
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
|
|
||||||
|
|
||||||
if bar.zap_total > 0 {
|
if zaps.zap_total > 0 {
|
||||||
Text(verbatim: format_msats_abbrev(bar.zap_total))
|
Text(verbatim: format_msats_abbrev(zaps.zap_total))
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundColor(bar.zapped ? Color.orange : Color.gray)
|
.foregroundColor(zap_color)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showing_zap_customizer) {
|
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
|
||||||
|
.simultaneousGesture(LongPressGesture().onEnded {_ in
|
||||||
|
button.showing_zap_customizer = true
|
||||||
|
})
|
||||||
|
.highPriorityGesture(TapGesture().onEnded {
|
||||||
|
tap()
|
||||||
|
})
|
||||||
|
.sheet(isPresented: $button.showing_zap_customizer) {
|
||||||
CustomizeZapView(state: damus_state, event: event, lnurl: lnurl)
|
CustomizeZapView(state: damus_state, event: event, lnurl: lnurl)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
|
.sheet(isPresented: $button.showing_select_wallet, onDismiss: {button.showing_select_wallet = false}) {
|
||||||
SelectWalletView(default_wallet: damus_state.settings.default_wallet, showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: invoice)
|
SelectWalletView(default_wallet: damus_state.settings.default_wallet, showingSelectWallet: $button.showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: button.invoice ?? "")
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.zapping)) { notif in
|
.onReceive(handle_notify(.zapping)) { notif in
|
||||||
let zap_ev = notif.object as! ZappingEvent
|
let zap_ev = notif.object as! ZappingEvent
|
||||||
@@ -117,15 +160,13 @@ struct ZapButton: View {
|
|||||||
break
|
break
|
||||||
case .got_zap_invoice(let inv):
|
case .got_zap_invoice(let inv):
|
||||||
if damus_state.settings.show_wallet_selector {
|
if damus_state.settings.show_wallet_selector {
|
||||||
self.invoice = inv
|
self.button.invoice = inv
|
||||||
self.showing_select_wallet = true
|
self.button.showing_select_wallet = true
|
||||||
} else {
|
} else {
|
||||||
let wallet = damus_state.settings.default_wallet.model
|
let wallet = damus_state.settings.default_wallet.model
|
||||||
open_with_wallet(wallet: wallet, invoice: inv)
|
open_with_wallet(wallet: wallet, invoice: inv)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.zapping = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,13 +174,25 @@ struct ZapButton: View {
|
|||||||
|
|
||||||
struct ZapButton_Previews: PreviewProvider {
|
struct ZapButton_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let bar = ActionBarModel(likes: 0, boosts: 0, zaps: 10, zap_total: 15623414, replies: 2, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil)
|
let pending_zap = PendingZap(amount_msat: 1000, target: ZapTarget.note(id: "noteid", author: "author"), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice)))
|
||||||
ZapButton(damus_state: test_damus_state(), event: test_event, lnurl: "lnurl", bar: bar)
|
let zaps = ZapsDataModel([.pending(pending_zap)])
|
||||||
|
|
||||||
|
ZapButton(damus_state: test_damus_state(), event: test_event, lnurl: "lnurl", zaps: zaps)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func initial_pending_zap_state(settings: UserSettingsStore) -> PendingZapState {
|
||||||
|
if let url = settings.nostr_wallet_connect,
|
||||||
|
let nwc = WalletConnectURL(str: url)
|
||||||
|
{
|
||||||
|
return .nwc(NWCPendingZapState(state: .fetching_invoice, url: nwc))
|
||||||
|
}
|
||||||
|
|
||||||
|
return .external(ExtPendingZapState(state: .fetching_invoice))
|
||||||
|
}
|
||||||
|
|
||||||
func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) {
|
func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) {
|
||||||
guard let keypair = damus_state.keypair.to_full() else {
|
guard let keypair = damus_state.keypair.to_full() else {
|
||||||
return
|
return
|
||||||
@@ -150,7 +203,19 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
|
|||||||
let target = ZapTarget.note(id: event.id, author: event.pubkey)
|
let target = ZapTarget.note(id: event.id, author: event.pubkey)
|
||||||
let content = comment ?? ""
|
let content = comment ?? ""
|
||||||
|
|
||||||
let zapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type)
|
guard let mzapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type) else {
|
||||||
|
// this should never happen
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let amount_msat = Int64(amount_sats ?? damus_state.settings.default_zap_amount) * 1000
|
||||||
|
let pending_zap_state = initial_pending_zap_state(settings: damus_state.settings)
|
||||||
|
let pending_zap = PendingZap(amount_msat: amount_msat, target: target, request: mzapreq, type: zap_type, state: pending_zap_state)
|
||||||
|
let zapreq = mzapreq.potentially_anon_outer_request.ev
|
||||||
|
let reqid = ZapRequestId(from_makezap: mzapreq)
|
||||||
|
|
||||||
|
UIImpactFeedbackGenerator(style: .heavy).impactOccurred()
|
||||||
|
damus_state.add_zap(zap: .pending(pending_zap))
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
var mpayreq = damus_state.lnurls.lookup(target.pubkey)
|
var mpayreq = damus_state.lnurls.lookup(target.pubkey)
|
||||||
@@ -161,6 +226,7 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
|
|||||||
guard let payreq = mpayreq else {
|
guard let payreq = mpayreq else {
|
||||||
// TODO: show error
|
// TODO: show error
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
|
||||||
let typ = ZappingEventType.failed(.bad_lnurl)
|
let typ = ZappingEventType.failed(.bad_lnurl)
|
||||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
|
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
|
||||||
notify(.zapping, ev)
|
notify(.zapping, ev)
|
||||||
@@ -172,10 +238,9 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
|
|||||||
damus_state.lnurls.endpoints[target.pubkey] = payreq
|
damus_state.lnurls.endpoints[target.pubkey] = payreq
|
||||||
}
|
}
|
||||||
|
|
||||||
let zap_amount = amount_sats ?? damus_state.settings.default_zap_amount
|
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, msats: amount_msat, zap_type: zap_type, comment: comment) else {
|
||||||
|
|
||||||
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount, zap_type: zap_type, comment: comment) else {
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
|
||||||
let typ = ZappingEventType.failed(.fetching_invoice)
|
let typ = ZappingEventType.failed(.fetching_invoice)
|
||||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
|
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
|
||||||
notify(.zapping, ev)
|
notify(.zapping, ev)
|
||||||
@@ -184,10 +249,87 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
|
|||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), event: event)
|
|
||||||
notify(.zapping, ev)
|
switch pending_zap_state {
|
||||||
|
case .nwc(let nwc_state):
|
||||||
|
// don't both continuing, user has canceled
|
||||||
|
if case .cancel_fetching_invoice = nwc_state.state {
|
||||||
|
remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var flusher: OnFlush? = nil
|
||||||
|
// Don't donate on custom zaps
|
||||||
|
if !is_custom && damus_state.settings.donation_percent > 0 {
|
||||||
|
flusher = .once({ pe in
|
||||||
|
// send donation zap when the pending zap is flushed, this allows user to cancel and not send a donation
|
||||||
|
Task.init { @MainActor in
|
||||||
|
await send_donation_zap(pool: damus_state.pool, postbox: damus_state.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, on_flush: flusher)
|
||||||
|
|
||||||
|
guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else {
|
||||||
|
print("nwc: failed to send nwc request for zapreq \(reqid.reqid)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
print("nwc: sending request \(nwc_req.id) zap_req_id \(reqid.reqid)")
|
||||||
|
|
||||||
|
if pzap_state.update_state(state: .postbox_pending(nwc_req)) {
|
||||||
|
// we don't need to trigger a ZapsDataModel update here
|
||||||
|
}
|
||||||
|
case .external(let pending_ext):
|
||||||
|
pending_ext.state = .done
|
||||||
|
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), event: event)
|
||||||
|
notify(.zapping, ev)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum CancelZapErr {
|
||||||
|
case send_err(CancelSendErr)
|
||||||
|
case already_confirmed
|
||||||
|
case not_nwc
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancel_zap(zap: PendingZap, box: PostBox, zapcache: Zaps, evcache: EventCache) -> CancelZapErr? {
|
||||||
|
guard case .nwc(let nwc_state) = zap.state else {
|
||||||
|
return .not_nwc
|
||||||
|
}
|
||||||
|
|
||||||
|
switch nwc_state.state {
|
||||||
|
case .fetching_invoice:
|
||||||
|
if nwc_state.update_state(state: .cancel_fetching_invoice) {
|
||||||
|
// we don't need to update the ZapsDataModel here
|
||||||
|
}
|
||||||
|
// let the code that retrieves the invoice remove the zap, because
|
||||||
|
// it still needs access to this pending zap to know to cancel
|
||||||
|
|
||||||
|
case .cancel_fetching_invoice:
|
||||||
|
// already cancelling?
|
||||||
|
print("cancel_zap: already cancelling")
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case .confirmed:
|
||||||
|
return .already_confirmed
|
||||||
|
|
||||||
|
case .postbox_pending(let nwc_req):
|
||||||
|
if let err = box.cancel_send(evid: nwc_req.id) {
|
||||||
|
return .send_err(err)
|
||||||
|
}
|
||||||
|
let reqid = ZapRequestId(from_pending: zap)
|
||||||
|
remove_zap(reqid: reqid, zapcache: zapcache, evcache: evcache)
|
||||||
|
|
||||||
|
case .failed:
|
||||||
|
let reqid = ZapRequestId(from_pending: zap)
|
||||||
|
remove_zap(reqid: reqid, zapcache: zapcache, evcache: evcache)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ struct ContentView: View {
|
|||||||
@State var profile_open: Bool = false
|
@State var profile_open: Bool = false
|
||||||
@State var thread_open: Bool = false
|
@State var thread_open: Bool = false
|
||||||
@State var search_open: Bool = false
|
@State var search_open: Bool = false
|
||||||
|
@State var wallet_open: Bool = false
|
||||||
|
@State var active_nwc: WalletConnectURL? = nil
|
||||||
@State var muting: String? = nil
|
@State var muting: String? = nil
|
||||||
@State var confirm_mute: Bool = false
|
@State var confirm_mute: Bool = false
|
||||||
@State var user_muted_confirm: Bool = false
|
@State var user_muted_confirm: Bool = false
|
||||||
@@ -78,6 +80,9 @@ struct ContentView: View {
|
|||||||
|
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
|
// connect retry timer
|
||||||
|
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
||||||
|
|
||||||
var mystery: some View {
|
var mystery: some View {
|
||||||
Text("Are you lost?", comment: "Text asking the user if they are lost in the app.")
|
Text("Are you lost?", comment: "Text asking the user if they are lost in the app.")
|
||||||
.id("what")
|
.id("what")
|
||||||
@@ -131,6 +136,7 @@ struct ContentView: View {
|
|||||||
profile_open = false
|
profile_open = false
|
||||||
thread_open = false
|
thread_open = false
|
||||||
search_open = false
|
search_open = false
|
||||||
|
wallet_open = false
|
||||||
isSideBarOpened = false
|
isSideBarOpened = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +147,9 @@ struct ContentView: View {
|
|||||||
|
|
||||||
func MainContent(damus: DamusState) -> some View {
|
func MainContent(damus: DamusState) -> some View {
|
||||||
VStack {
|
VStack {
|
||||||
|
NavigationLink(destination: WalletView(damus_state: damus, model: damus_state!.wallet), isActive: $wallet_open) {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
NavigationLink(destination: MaybeProfileView, isActive: $profile_open) {
|
NavigationLink(destination: MaybeProfileView, isActive: $profile_open) {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
@@ -231,16 +240,24 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func open_event(ev: NostrEvent) {
|
func open_event(ev: NostrEvent) {
|
||||||
|
popToRoot()
|
||||||
self.active_event = ev
|
self.active_event = ev
|
||||||
self.thread_open = true
|
self.thread_open = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func open_wallet(nwc: WalletConnectURL) {
|
||||||
|
self.damus_state!.wallet.new(nwc)
|
||||||
|
self.wallet_open = true
|
||||||
|
}
|
||||||
|
|
||||||
func open_profile(id: String) {
|
func open_profile(id: String) {
|
||||||
|
popToRoot()
|
||||||
self.active_profile = id
|
self.active_profile = id
|
||||||
self.profile_open = true
|
self.profile_open = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func open_search(filt: NostrFilter) {
|
func open_search(filt: NostrFilter) {
|
||||||
|
popToRoot()
|
||||||
self.active_search = filt
|
self.active_search = filt
|
||||||
self.search_open = true
|
self.search_open = true
|
||||||
}
|
}
|
||||||
@@ -320,34 +337,25 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onOpenURL { url in
|
.onOpenURL { url in
|
||||||
guard let link = decode_nostr_uri(url.absoluteString) else {
|
on_open_url(state: damus_state!, url: url) { res in
|
||||||
return
|
guard let res else {
|
||||||
}
|
return
|
||||||
|
|
||||||
switch link {
|
|
||||||
case .ref(let ref):
|
|
||||||
if ref.key == "p" {
|
|
||||||
active_profile = ref.ref_id
|
|
||||||
profile_open = true
|
|
||||||
} else if ref.key == "e" {
|
|
||||||
find_event(state: damus_state!, evid: ref.ref_id, search_type: .event, find_from: nil) { ev in
|
|
||||||
if let ev {
|
|
||||||
open_event(ev: ev)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case .filter(let filt):
|
|
||||||
active_search = filt
|
switch res {
|
||||||
search_open = true
|
case .filter(let filt): self.open_search(filt: filt)
|
||||||
break
|
case .profile(let id): self.open_profile(id: id)
|
||||||
// TODO: handle filter searches?
|
case .event(let ev): self.open_event(ev: ev)
|
||||||
|
case .wallet_connect(let nwc): self.open_wallet(nwc: nwc)}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.compose)) { notif in
|
.onReceive(handle_notify(.compose)) { notif in
|
||||||
let action = notif.object as! PostAction
|
let action = notif.object as! PostAction
|
||||||
self.active_sheet = .post(action)
|
self.active_sheet = .post(action)
|
||||||
}
|
}
|
||||||
|
.onReceive(timer) { n in
|
||||||
|
self.damus_state?.postbox.try_flushing_events()
|
||||||
|
}
|
||||||
.onReceive(handle_notify(.deleted_account)) { notif in
|
.onReceive(handle_notify(.deleted_account)) { notif in
|
||||||
self.is_deleted_account = true
|
self.is_deleted_account = true
|
||||||
}
|
}
|
||||||
@@ -360,13 +368,36 @@ struct ContentView: View {
|
|||||||
self.muting = pubkey
|
self.muting = pubkey
|
||||||
self.confirm_mute = true
|
self.confirm_mute = true
|
||||||
}
|
}
|
||||||
|
.onReceive(handle_notify(.attached_wallet)) { notif in
|
||||||
|
// update the lightning address on our profile when we attach a
|
||||||
|
// wallet with an associated
|
||||||
|
let nwc = notif.object as! WalletConnectURL
|
||||||
|
guard let ds = self.damus_state,
|
||||||
|
let lud16 = nwc.lud16,
|
||||||
|
let keypair = ds.keypair.to_full(),
|
||||||
|
let profile = ds.profiles.lookup(id: ds.pubkey),
|
||||||
|
lud16 != profile.lud16
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear zapper cache for old lud16
|
||||||
|
if profile.lud16 != nil {
|
||||||
|
// TODO: should this be somewhere else, where we process profile events!?
|
||||||
|
invalidate_zapper_cache(pubkey: keypair.pubkey, profiles: ds.profiles, lnurl: ds.lnurls)
|
||||||
|
}
|
||||||
|
|
||||||
|
profile.lud16 = lud16
|
||||||
|
let ev = make_metadata_event(keypair: keypair, metadata: profile)
|
||||||
|
ds.postbox.send(ev)
|
||||||
|
}
|
||||||
.onReceive(handle_notify(.broadcast_event)) { obj in
|
.onReceive(handle_notify(.broadcast_event)) { obj in
|
||||||
let ev = obj.object as! NostrEvent
|
let ev = obj.object as! NostrEvent
|
||||||
guard let ds = self.damus_state else {
|
guard let ds = self.damus_state else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ds.postbox.send(ev)
|
ds.postbox.send(ev)
|
||||||
if let profile = ds.profiles.profiles[ev.pubkey] {
|
if let profile = ds.profiles.lookup_with_timestamp(id: ev.pubkey) {
|
||||||
ds.postbox.send(profile.event)
|
ds.postbox.send(profile.event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -559,7 +590,8 @@ struct ContentView: View {
|
|||||||
let new_relay_filters = load_relay_filters(pubkey) == nil
|
let new_relay_filters = load_relay_filters(pubkey) == nil
|
||||||
for relay in bootstrap_relays {
|
for relay in bootstrap_relays {
|
||||||
if let url = RelayURL(relay) {
|
if let url = RelayURL(relay) {
|
||||||
add_new_relay(relay_filters: relay_filters, metadatas: metadatas, pool: pool, url: url, info: .rw, new_relay_filters: new_relay_filters)
|
let descriptor = RelayDescriptor(url: url, info: .rw)
|
||||||
|
add_new_relay(relay_filters: relay_filters, metadatas: metadatas, pool: pool, descriptor: descriptor, new_relay_filters: new_relay_filters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -570,6 +602,11 @@ struct ContentView: View {
|
|||||||
let settings = UserSettingsStore()
|
let settings = UserSettingsStore()
|
||||||
UserSettingsStore.shared = settings
|
UserSettingsStore.shared = settings
|
||||||
|
|
||||||
|
if let nwc_str = settings.nostr_wallet_connect,
|
||||||
|
let nwc = WalletConnectURL(str: nwc_str) {
|
||||||
|
try? pool.add_relay(.nwc(url: nwc.relay))
|
||||||
|
}
|
||||||
|
|
||||||
self.damus_state = DamusState(pool: pool,
|
self.damus_state = DamusState(pool: pool,
|
||||||
keypair: keypair,
|
keypair: keypair,
|
||||||
likes: EventCounter(our_pubkey: pubkey),
|
likes: EventCounter(our_pubkey: pubkey),
|
||||||
@@ -589,7 +626,8 @@ struct ContentView: View {
|
|||||||
postbox: PostBox(pool: pool),
|
postbox: PostBox(pool: pool),
|
||||||
bootstrap_relays: bootstrap_relays,
|
bootstrap_relays: bootstrap_relays,
|
||||||
replies: ReplyCounter(our_pubkey: pubkey),
|
replies: ReplyCounter(our_pubkey: pubkey),
|
||||||
muted_threads: MutedThreadsManager(keypair: keypair)
|
muted_threads: MutedThreadsManager(keypair: keypair),
|
||||||
|
wallet: WalletModel(settings: settings)
|
||||||
)
|
)
|
||||||
home.damus_state = self.damus_state!
|
home.damus_state = self.damus_state!
|
||||||
|
|
||||||
@@ -839,3 +877,40 @@ func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: Ev
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum OpenResult {
|
||||||
|
case profile(String)
|
||||||
|
case filter(NostrFilter)
|
||||||
|
case event(NostrEvent)
|
||||||
|
case wallet_connect(WalletConnectURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) -> Void) {
|
||||||
|
if let nwc = WalletConnectURL(str: url.absoluteString) {
|
||||||
|
result(.wallet_connect(nwc))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let link = decode_nostr_uri(url.absoluteString) else {
|
||||||
|
result(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch link {
|
||||||
|
case .ref(let ref):
|
||||||
|
if ref.key == "p" {
|
||||||
|
result(.profile(ref.ref_id))
|
||||||
|
} else if ref.key == "e" {
|
||||||
|
find_event(state: state, evid: ref.ref_id, search_type: .event, find_from: nil) { ev in
|
||||||
|
if let ev {
|
||||||
|
result(.event(ev))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .filter(let filt):
|
||||||
|
result(.filter(filt))
|
||||||
|
break
|
||||||
|
// TODO: handle filter searches?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -24,6 +24,26 @@
|
|||||||
<string>damus</string>
|
<string>damus</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>io.damus.nwc</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>nostrwalletconnect</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>io.damus.nwcp</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>nostr+walletconnect</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
|
|||||||
@@ -7,12 +7,17 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
enum Zapped {
|
||||||
|
case not_zapped
|
||||||
|
case pending
|
||||||
|
case zapped
|
||||||
|
}
|
||||||
|
|
||||||
class ActionBarModel: ObservableObject {
|
class ActionBarModel: ObservableObject {
|
||||||
@Published var our_like: NostrEvent?
|
@Published var our_like: NostrEvent?
|
||||||
@Published var our_boost: NostrEvent?
|
@Published var our_boost: NostrEvent?
|
||||||
@Published var our_reply: NostrEvent?
|
@Published var our_reply: NostrEvent?
|
||||||
@Published var our_zap: Zap?
|
@Published var our_zap: Zapping?
|
||||||
@Published var likes: Int
|
@Published var likes: Int
|
||||||
@Published var boosts: Int
|
@Published var boosts: Int
|
||||||
@Published var zaps: Int
|
@Published var zaps: Int
|
||||||
@@ -35,7 +40,7 @@ class ActionBarModel: ObservableObject {
|
|||||||
self.replies = 0
|
self.replies = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, replies: Int, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zap?, our_reply: NostrEvent?) {
|
init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, replies: Int, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zapping?, our_reply: NostrEvent?) {
|
||||||
self.likes = likes
|
self.likes = likes
|
||||||
self.boosts = boosts
|
self.boosts = boosts
|
||||||
self.zaps = zaps
|
self.zaps = zaps
|
||||||
@@ -64,10 +69,6 @@ class ActionBarModel: ObservableObject {
|
|||||||
return likes == 0 && boosts == 0 && zaps == 0
|
return likes == 0 && boosts == 0 && zaps == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var zapped: Bool {
|
|
||||||
return our_zap != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var liked: Bool {
|
var liked: Bool {
|
||||||
return our_like != nil
|
return our_like != nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,10 @@ struct DamusState {
|
|||||||
let bootstrap_relays: [String]
|
let bootstrap_relays: [String]
|
||||||
let replies: ReplyCounter
|
let replies: ReplyCounter
|
||||||
let muted_threads: MutedThreadsManager
|
let muted_threads: MutedThreadsManager
|
||||||
|
let wallet: WalletModel
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func add_zap(zap: Zap) -> Bool {
|
func add_zap(zap: Zapping) -> Bool {
|
||||||
// store generic zap mapping
|
// store generic zap mapping
|
||||||
self.zaps.add_zap(zap: zap)
|
self.zaps.add_zap(zap: zap)
|
||||||
// associate with events as well
|
// associate with events as well
|
||||||
@@ -47,5 +48,5 @@ struct DamusState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static var empty: DamusState {
|
static var empty: DamusState {
|
||||||
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil))) }
|
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil)), wallet: WalletModel(settings: UserSettingsStore())) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,21 +129,54 @@ class HomeModel: ObservableObject {
|
|||||||
handle_zap_event(ev)
|
handle_zap_event(ev)
|
||||||
case .zap_request:
|
case .zap_request:
|
||||||
break
|
break
|
||||||
|
case .nwc_request:
|
||||||
|
break
|
||||||
|
case .nwc_response:
|
||||||
|
handle_nwc_response(ev, relay: relay_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handle_nwc_response(_ ev: NostrEvent, relay: String) {
|
||||||
|
Task { @MainActor in
|
||||||
|
// TODO: Adapt KeychainStorage to StringCodable and instead of parsing to WalletConnectURL every time
|
||||||
|
guard let nwc_str = damus_state.settings.nostr_wallet_connect,
|
||||||
|
let nwc = WalletConnectURL(str: nwc_str),
|
||||||
|
let resp = await FullWalletResponse(from: ev, nwc: nwc) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// since command results are not returned for ephemeral events,
|
||||||
|
// remove the request from the postbox which is likely failing over and over
|
||||||
|
if damus_state.postbox.remove_relayer(relay_id: nwc.relay.id, event_id: resp.req_id) {
|
||||||
|
print("nwc: got response, removed \(resp.req_id) from the postbox [\(relay)]")
|
||||||
|
} else {
|
||||||
|
print("nwc: \(resp.req_id) not found in the postbox, nothing to remove [\(relay)]")
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let err = resp.response.error else {
|
||||||
|
print("nwc success: \(resp.response.result.debugDescription) [\(relay)]")
|
||||||
|
nwc_success(state: self.damus_state, resp: resp)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
print("nwc error: \(resp.response)")
|
||||||
|
nwc_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_zap_event_with_zapper(profiles: Profiles, ev: NostrEvent, our_keypair: Keypair, zapper: String) {
|
func handle_zap_event_with_zapper(profiles: Profiles, ev: NostrEvent, our_keypair: Keypair, zapper: String) {
|
||||||
|
|
||||||
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
|
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
damus_state.add_zap(zap: zap)
|
damus_state.add_zap(zap: .zap(zap))
|
||||||
|
|
||||||
guard zap.target.pubkey == our_keypair.pubkey else {
|
guard zap.target.pubkey == our_keypair.pubkey else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !notifications.insert_zap(zap) {
|
if !notifications.insert_zap(.zap(zap)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,6 +334,16 @@ class HomeModel: ObservableObject {
|
|||||||
//remove_bootstrap_nodes(damus_state)
|
//remove_bootstrap_nodes(damus_state)
|
||||||
send_home_filters(relay_id: relay_id)
|
send_home_filters(relay_id: relay_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// connect to nwc relays when connected
|
||||||
|
if let nwc_str = damus_state.settings.nostr_wallet_connect,
|
||||||
|
let r = pool.get_relay(relay_id),
|
||||||
|
r.descriptor.variant == .nwc,
|
||||||
|
let nwc = WalletConnectURL(str: nwc_str),
|
||||||
|
nwc.relay.id == relay_id
|
||||||
|
{
|
||||||
|
subscribe_to_nwc(url: nwc, pool: pool)
|
||||||
|
}
|
||||||
case .error(let merr):
|
case .error(let merr):
|
||||||
let desc = String(describing: merr)
|
let desc = String(describing: merr)
|
||||||
if desc.contains("Software caused connection abort") {
|
if desc.contains("Software caused connection abort") {
|
||||||
@@ -431,7 +474,7 @@ class HomeModel: ObservableObject {
|
|||||||
|
|
||||||
print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters])
|
print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters])
|
||||||
|
|
||||||
if let relay_id = relay_id {
|
if let relay_id {
|
||||||
pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid)), to: [relay_id])
|
pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid)), to: [relay_id])
|
||||||
pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid)), to: [relay_id])
|
pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid)), to: [relay_id])
|
||||||
pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid)), to: [relay_id])
|
pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid)), to: [relay_id])
|
||||||
@@ -691,7 +734,7 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P
|
|||||||
var old_nip05: String? = nil
|
var old_nip05: String? = nil
|
||||||
if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) {
|
if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) {
|
||||||
old_nip05 = mprof.profile.nip05
|
old_nip05 = mprof.profile.nip05
|
||||||
if mprof.timestamp > ev.created_at {
|
if mprof.event.created_at > ev.created_at {
|
||||||
// skip if we already have an newer profile
|
// skip if we already have an newer profile
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -708,7 +751,7 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P
|
|||||||
print("validated nip05 for '\(nip05)'")
|
print("validated nip05 for '\(nip05)'")
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
Task { @MainActor in
|
||||||
profiles.validated[ev.pubkey] = validated
|
profiles.validated[ev.pubkey] = validated
|
||||||
profiles.nip05_pubkey[nip05] = ev.pubkey
|
profiles.nip05_pubkey[nip05] = ev.pubkey
|
||||||
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
||||||
@@ -836,7 +879,8 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
|||||||
changed = true
|
changed = true
|
||||||
if new.contains(d) {
|
if new.contains(d) {
|
||||||
if let url = RelayURL(d) {
|
if let url = RelayURL(d) {
|
||||||
add_new_relay(relay_filters: state.relay_filters, metadatas: state.relay_metadata, pool: state.pool, url: url, info: decoded[d] ?? .rw, new_relay_filters: new_relay_filters)
|
let descriptor = RelayDescriptor(url: url, info: decoded[d] ?? .rw)
|
||||||
|
add_new_relay(relay_filters: state.relay_filters, metadatas: state.relay_metadata, pool: state.pool, descriptor: descriptor, new_relay_filters: new_relay_filters)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
state.pool.remove_relay(d)
|
state.pool.remove_relay(d)
|
||||||
@@ -849,8 +893,9 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func add_new_relay(relay_filters: RelayFilters, metadatas: RelayMetadatas, pool: RelayPool, url: RelayURL, info: RelayInfo, new_relay_filters: Bool) {
|
func add_new_relay(relay_filters: RelayFilters, metadatas: RelayMetadatas, pool: RelayPool, descriptor: RelayDescriptor, new_relay_filters: Bool) {
|
||||||
try? pool.add_relay(url, info: info)
|
try? pool.add_relay(descriptor)
|
||||||
|
let url = descriptor.url
|
||||||
|
|
||||||
let relay_id = url.id
|
let relay_id = url.id
|
||||||
guard metadatas.lookup(relay_id: relay_id) == nil else {
|
guard metadatas.lookup(relay_id: relay_id) == nil else {
|
||||||
@@ -1157,7 +1202,7 @@ func create_local_notification(profiles: Profiles, notify: LocalNotification) {
|
|||||||
title = String(format: NSLocalizedString("Reposted by %@", comment: "Reposted by heading in local notification"), displayName)
|
title = String(format: NSLocalizedString("Reposted by %@", comment: "Reposted by heading in local notification"), displayName)
|
||||||
identifier = "myBoostNotification"
|
identifier = "myBoostNotification"
|
||||||
case .like:
|
case .like:
|
||||||
title = String(format: NSLocalizedString("%@ reacted with %@", comment: "Reacted by heading in local notification"), displayName, notify.event.content)
|
title = String(format: NSLocalizedString("%@ reacted with %@", comment: "Reacted by heading in local notification"), displayName, to_reaction_emoji(ev: notify.event) ?? "")
|
||||||
identifier = "myLikeNotification"
|
identifier = "myLikeNotification"
|
||||||
case .dm:
|
case .dm:
|
||||||
title = displayName
|
title = displayName
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class ZapGroup {
|
class ZapGroup {
|
||||||
var zaps: [Zap]
|
var zaps: [Zapping]
|
||||||
var msat_total: Int64
|
var msat_total: Int64
|
||||||
var zappers: Set<String>
|
var zappers: Set<String>
|
||||||
|
|
||||||
@@ -17,22 +17,16 @@ class ZapGroup {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return first.event.created_at
|
return first.created_at
|
||||||
}
|
}
|
||||||
|
|
||||||
func zap_requests() -> [NostrEvent] {
|
func zap_requests() -> [NostrEvent] {
|
||||||
zaps.map { z in
|
zaps.map { z in z.request }
|
||||||
if let priv = z.private_request {
|
|
||||||
return priv
|
|
||||||
} else {
|
|
||||||
return z.request.ev
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool {
|
func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool {
|
||||||
for zap in zaps {
|
for zap in zaps {
|
||||||
if !isIncluded(zap.request_ev) {
|
if !isIncluded(zap.request) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,7 +35,7 @@ class ZapGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func filter(_ isIncluded: (NostrEvent) -> Bool) -> ZapGroup? {
|
func filter(_ isIncluded: (NostrEvent) -> Bool) -> ZapGroup? {
|
||||||
let new_zaps = zaps.filter { isIncluded($0.request_ev) }
|
let new_zaps = zaps.filter { isIncluded($0.request) }
|
||||||
guard new_zaps.count > 0 else {
|
guard new_zaps.count > 0 else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -59,15 +53,15 @@ class ZapGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func insert(_ zap: Zap) -> Bool {
|
func insert(_ zap: Zapping) -> Bool {
|
||||||
if !insert_uniq_sorted_zap_by_created(zaps: &zaps, new_zap: zap) {
|
if !insert_uniq_sorted_zap_by_created(zaps: &zaps, new_zap: zap) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
msat_total += zap.invoice.amount
|
msat_total += zap.amount
|
||||||
|
|
||||||
if !zappers.contains(zap.request.ev.pubkey) {
|
if !zappers.contains(zap.request.pubkey) {
|
||||||
zappers.insert(zap.request.ev.pubkey)
|
zappers.insert(zap.request.pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ enum NotificationItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class NotificationsModel: ObservableObject, ScrollQueue {
|
class NotificationsModel: ObservableObject, ScrollQueue {
|
||||||
var incoming_zaps: [Zap]
|
var incoming_zaps: [Zapping]
|
||||||
var incoming_events: [NostrEvent]
|
var incoming_events: [NostrEvent]
|
||||||
var should_queue: Bool
|
var should_queue: Bool
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for zap in incoming_zaps {
|
for zap in incoming_zaps {
|
||||||
pks.insert(zap.request.ev.pubkey)
|
pks.insert(zap.request.pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array(pks)
|
return Array(pks)
|
||||||
@@ -249,7 +249,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private func insert_zap_immediate(_ zap: Zap) -> Bool {
|
private func insert_zap_immediate(_ zap: Zapping) -> Bool {
|
||||||
switch zap.target {
|
switch zap.target {
|
||||||
case .note(let notezt):
|
case .note(let notezt):
|
||||||
let id = notezt.note_id
|
let id = notezt.note_id
|
||||||
@@ -285,7 +285,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func insert_zap(_ zap: Zap) -> Bool {
|
func insert_zap(_ zap: Zapping) -> Bool {
|
||||||
if should_queue {
|
if should_queue {
|
||||||
return insert_uniq_sorted_zap_by_created(zaps: &incoming_zaps, new_zap: zap)
|
return insert_uniq_sorted_zap_by_created(zaps: &incoming_zaps, new_zap: zap)
|
||||||
}
|
}
|
||||||
@@ -307,7 +307,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
|||||||
changed = changed || incoming_events.count != count
|
changed = changed || incoming_events.count != count
|
||||||
|
|
||||||
count = profile_zaps.zaps.count
|
count = profile_zaps.zaps.count
|
||||||
profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request.ev) }
|
profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request) }
|
||||||
changed = changed || profile_zaps.zaps.count != count
|
changed = changed || profile_zaps.zaps.count != count
|
||||||
|
|
||||||
for el in reactions {
|
for el in reactions {
|
||||||
@@ -325,7 +325,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
|||||||
for el in zaps {
|
for el in zaps {
|
||||||
count = el.value.zaps.count
|
count = el.value.zaps.count
|
||||||
el.value.zaps = el.value.zaps.filter {
|
el.value.zaps = el.value.zaps.filter {
|
||||||
isIncluded($0.request.ev)
|
isIncluded($0.request)
|
||||||
}
|
}
|
||||||
changed = changed || el.value.zaps.count != count
|
changed = changed || el.value.zaps.count != count
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,11 @@ let fallback_zap_amount = 1000
|
|||||||
if let loaded = UserDefaults.standard.object(forKey: self.key) as? T {
|
if let loaded = UserDefaults.standard.object(forKey: self.key) as? T {
|
||||||
self.value = loaded
|
self.value = loaded
|
||||||
} else if let loaded = UserDefaults.standard.object(forKey: key) as? T {
|
} else if let loaded = UserDefaults.standard.object(forKey: key) as? T {
|
||||||
// try to load from deprecated non-pubkey-keyed setting
|
// If pubkey-scoped setting does not exist but the deprecated non-pubkey-scoped setting does,
|
||||||
|
// migrate the deprecated setting into the pubkey-scoped one and delete the deprecated one.
|
||||||
self.value = loaded
|
self.value = loaded
|
||||||
|
UserDefaults.standard.set(loaded, forKey: self.key)
|
||||||
|
UserDefaults.standard.removeObject(forKey: key)
|
||||||
} else {
|
} else {
|
||||||
self.value = default_value
|
self.value = default_value
|
||||||
}
|
}
|
||||||
@@ -48,8 +51,11 @@ let fallback_zap_amount = 1000
|
|||||||
if let loaded = UserDefaults.standard.string(forKey: self.key), let val = T.init(from: loaded) {
|
if let loaded = UserDefaults.standard.string(forKey: self.key), let val = T.init(from: loaded) {
|
||||||
self.value = val
|
self.value = val
|
||||||
} else if let loaded = UserDefaults.standard.string(forKey: key), let val = T.init(from: loaded) {
|
} else if let loaded = UserDefaults.standard.string(forKey: key), let val = T.init(from: loaded) {
|
||||||
// try to load from deprecated non-pubkey-keyed setting
|
// If pubkey-scoped setting does not exist but the deprecated non-pubkey-scoped setting does,
|
||||||
|
// migrate the deprecated setting into the pubkey-scoped one and delete the deprecated one.
|
||||||
self.value = val
|
self.value = val
|
||||||
|
UserDefaults.standard.set(val.to_string(), forKey: self.key)
|
||||||
|
UserDefaults.standard.removeObject(forKey: key)
|
||||||
} else {
|
} else {
|
||||||
self.value = default_value
|
self.value = default_value
|
||||||
}
|
}
|
||||||
@@ -137,6 +143,9 @@ class UserSettingsStore: ObservableObject {
|
|||||||
|
|
||||||
@Setting(key: "disable_animation", default_value: UIAccessibility.isReduceMotionEnabled)
|
@Setting(key: "disable_animation", default_value: UIAccessibility.isReduceMotionEnabled)
|
||||||
var disable_animation: Bool
|
var disable_animation: Bool
|
||||||
|
|
||||||
|
@Setting(key: "donation_percent", default_value: 0)
|
||||||
|
var donation_percent: Int
|
||||||
|
|
||||||
// Helper for inverse of disable_animation.
|
// Helper for inverse of disable_animation.
|
||||||
// disable_animation was introduced as a setting first, but it's more natural for the settings UI to show the inverse.
|
// disable_animation was introduced as a setting first, but it's more natural for the settings UI to show the inverse.
|
||||||
@@ -201,6 +210,9 @@ class UserSettingsStore: ObservableObject {
|
|||||||
|
|
||||||
@KeychainStorage(account: "libretranslate_apikey")
|
@KeychainStorage(account: "libretranslate_apikey")
|
||||||
var internal_libretranslate_api_key: String?
|
var internal_libretranslate_api_key: String?
|
||||||
|
|
||||||
|
@KeychainStorage(account: "nostr_wallet_connect")
|
||||||
|
var nostr_wallet_connect: String? // TODO: strongly type this to WalletConnectURL
|
||||||
|
|
||||||
var can_translate: Bool {
|
var can_translate: Bool {
|
||||||
switch translation_service {
|
switch translation_service {
|
||||||
|
|||||||
64
damus/Models/WalletModel.swift
Normal file
64
damus/Models/WalletModel.swift
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
//
|
||||||
|
// WalletModel.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-05-09.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum WalletConnectState {
|
||||||
|
case new(WalletConnectURL)
|
||||||
|
case existing(WalletConnectURL)
|
||||||
|
case none
|
||||||
|
}
|
||||||
|
|
||||||
|
class WalletModel: ObservableObject {
|
||||||
|
var settings: UserSettingsStore
|
||||||
|
private(set) var previous_state: WalletConnectState
|
||||||
|
var inital_percent: Int
|
||||||
|
|
||||||
|
@Published private(set) var connect_state: WalletConnectState
|
||||||
|
|
||||||
|
init(state: WalletConnectState, settings: UserSettingsStore) {
|
||||||
|
self.connect_state = state
|
||||||
|
self.previous_state = .none
|
||||||
|
self.settings = settings
|
||||||
|
self.inital_percent = settings.donation_percent
|
||||||
|
}
|
||||||
|
|
||||||
|
init(settings: UserSettingsStore) {
|
||||||
|
self.settings = settings
|
||||||
|
if let str = settings.nostr_wallet_connect,
|
||||||
|
let nwc = WalletConnectURL(str: str) {
|
||||||
|
self.previous_state = .existing(nwc)
|
||||||
|
self.connect_state = .existing(nwc)
|
||||||
|
} else {
|
||||||
|
self.previous_state = .none
|
||||||
|
self.connect_state = .none
|
||||||
|
}
|
||||||
|
self.inital_percent = settings.donation_percent
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancel() {
|
||||||
|
self.connect_state = previous_state
|
||||||
|
self.objectWillChange.send()
|
||||||
|
}
|
||||||
|
|
||||||
|
func disconnect() {
|
||||||
|
self.settings.nostr_wallet_connect = nil
|
||||||
|
self.connect_state = .none
|
||||||
|
self.previous_state = .none
|
||||||
|
}
|
||||||
|
|
||||||
|
func new(_ nwc: WalletConnectURL) {
|
||||||
|
self.connect_state = .new(nwc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func connect(_ nwc: WalletConnectURL) {
|
||||||
|
self.settings.nostr_wallet_connect = nwc.to_url().absoluteString
|
||||||
|
notify(.attached_wallet, nwc)
|
||||||
|
self.connect_state = .existing(nwc)
|
||||||
|
self.previous_state = .existing(nwc)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ class ZapsModel: ObservableObject {
|
|||||||
self.target = target
|
self.target = target
|
||||||
}
|
}
|
||||||
|
|
||||||
var zaps: [Zap] {
|
var zaps: [Zapping] {
|
||||||
return state.events.lookup_zaps(target: target)
|
return state.events.lookup_zaps(target: target)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ class ZapsModel: ObservableObject {
|
|||||||
case .notice:
|
case .notice:
|
||||||
break
|
break
|
||||||
case .eose:
|
case .eose:
|
||||||
let events = state.events.lookup_zaps(target: target).map { $0.request_ev }
|
let events = state.events.lookup_zaps(target: target).map { $0.request }
|
||||||
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state)
|
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state)
|
||||||
case .event(_, let ev):
|
case .event(_, let ev):
|
||||||
guard ev.kind == 9735 else {
|
guard ev.kind == 9735 else {
|
||||||
@@ -61,22 +61,19 @@ class ZapsModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let zap = state.zaps.zaps[ev.id] {
|
if let zap = state.zaps.zaps[ev.id] {
|
||||||
if state.events.store_zap(zap: zap) {
|
state.events.store_zap(zap: zap)
|
||||||
objectWillChange.send()
|
return
|
||||||
}
|
|
||||||
} else {
|
|
||||||
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: state.keypair.privkey) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.state.add_zap(zap: zap) {
|
|
||||||
objectWillChange.send()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: state.keypair.privkey) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state.add_zap(zap: .zap(zap))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import Foundation
|
|||||||
class Profile: Codable {
|
class Profile: Codable {
|
||||||
var value: [String: AnyCodable]
|
var value: [String: AnyCodable]
|
||||||
|
|
||||||
init (name: String?, display_name: String?, about: String?, picture: String?, banner: String?, website: String?, lud06: String?, lud16: String?, nip05: String?) {
|
init (name: String?, display_name: String?, about: String?, picture: String?, banner: String?, website: String?, lud06: String?, lud16: String?, nip05: String?, damus_donation: Int?) {
|
||||||
self.value = [:]
|
self.value = [:]
|
||||||
self.name = name
|
self.name = name
|
||||||
self.display_name = display_name
|
self.display_name = display_name
|
||||||
@@ -21,6 +21,7 @@ class Profile: Codable {
|
|||||||
self.lud06 = lud06
|
self.lud06 = lud06
|
||||||
self.lud16 = lud16
|
self.lud16 = lud16
|
||||||
self.nip05 = nip05
|
self.nip05 = nip05
|
||||||
|
self.damus_donation = damus_donation
|
||||||
}
|
}
|
||||||
|
|
||||||
convenience init(persisted_profile: PersistedProfile) {
|
convenience init(persisted_profile: PersistedProfile) {
|
||||||
@@ -39,6 +40,10 @@ class Profile: Codable {
|
|||||||
return get_val(str)
|
return get_val(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func int(_ key: String) -> Int? {
|
||||||
|
return get_val(key)
|
||||||
|
}
|
||||||
|
|
||||||
private func get_val<T>(_ v: String) -> T? {
|
private func get_val<T>(_ v: String) -> T? {
|
||||||
guard let val = self.value[v] else{
|
guard let val = self.value[v] else{
|
||||||
return nil
|
return nil
|
||||||
@@ -64,6 +69,10 @@ class Profile: Codable {
|
|||||||
set_val(key, val)
|
set_val(key, val)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func set_int(_ key: String, _ val: Int?) {
|
||||||
|
set_val(key, val)
|
||||||
|
}
|
||||||
|
|
||||||
var reactions: Bool? {
|
var reactions: Bool? {
|
||||||
get { return get_val("reactions"); }
|
get { return get_val("reactions"); }
|
||||||
set(s) { set_val("reactions", s) }
|
set(s) { set_val("reactions", s) }
|
||||||
@@ -89,6 +98,11 @@ class Profile: Codable {
|
|||||||
set(s) { set_str("about", s) }
|
set(s) { set_str("about", s) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var damus_donation: Int? {
|
||||||
|
get { return int("damus_donation"); }
|
||||||
|
set(s) { set_int("damus_donation", s) }
|
||||||
|
}
|
||||||
|
|
||||||
var picture: String? {
|
var picture: String? {
|
||||||
get { return str("picture"); }
|
get { return str("picture"); }
|
||||||
set(s) { set_str("picture", s) }
|
set(s) { set_str("picture", s) }
|
||||||
@@ -192,7 +206,7 @@ class Profile: Codable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func make_test_profile() -> Profile {
|
func make_test_profile() -> Profile {
|
||||||
return Profile(name: "jb55", display_name: "Will", about: "Its a me", picture: "https://cdn.jb55.com/img/red-me.jpg", banner: "https://pbs.twimg.com/profile_banners/9918032/1531711830/600x200", website: "jb55.com", lud06: "jb55@jb55.com", lud16: nil, nip05: "jb55@jb55.com")
|
return Profile(name: "jb55", display_name: "Will", about: "Its a me", picture: "https://cdn.jb55.com/img/red-me.jpg", banner: "https://pbs.twimg.com/profile_banners/9918032/1531711830/600x200", website: "jb55.com", lud06: "jb55@jb55.com", lud16: nil, nip05: "jb55@jb55.com", damus_donation: 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func make_ln_url(_ str: String?) -> URL? {
|
func make_ln_url(_ str: String?) -> URL? {
|
||||||
|
|||||||
@@ -492,11 +492,11 @@ func make_boost_event(pubkey: String, privkey: String, boosted: NostrEvent) -> N
|
|||||||
return ev
|
return ev
|
||||||
}
|
}
|
||||||
|
|
||||||
func make_like_event(pubkey: String, privkey: String, liked: NostrEvent) -> NostrEvent {
|
func make_like_event(pubkey: String, privkey: String, liked: NostrEvent, content: String = "🤙") -> NostrEvent {
|
||||||
var tags: [[String]] = liked.tags.filter { tag in tag.count >= 2 && (tag[0] == "e" || tag[0] == "p") }
|
var tags: [[String]] = liked.tags.filter { tag in tag.count >= 2 && (tag[0] == "e" || tag[0] == "p") }
|
||||||
tags.append(["e", liked.id])
|
tags.append(["e", liked.id])
|
||||||
tags.append(["p", liked.pubkey])
|
tags.append(["p", liked.pubkey])
|
||||||
let ev = NostrEvent(content: "🤙", pubkey: pubkey, kind: 7, tags: tags)
|
let ev = NostrEvent(content: content, pubkey: pubkey, kind: 7, tags: tags)
|
||||||
ev.calculate_id()
|
ev.calculate_id()
|
||||||
ev.sign(privkey: privkey)
|
ev.sign(privkey: privkey)
|
||||||
|
|
||||||
@@ -512,7 +512,12 @@ func zap_target_to_tags(_ target: ZapTarget) -> [[String]] {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func make_private_zap_request_event(identity: FullKeypair, enc_key: FullKeypair, target: ZapTarget, message: String) -> String? {
|
struct PrivateZapRequest {
|
||||||
|
let req: ZapRequest
|
||||||
|
let enc: String
|
||||||
|
}
|
||||||
|
|
||||||
|
func make_private_zap_request_event(identity: FullKeypair, enc_key: FullKeypair, target: ZapTarget, message: String) -> PrivateZapRequest? {
|
||||||
// target tags must be the same as zap request target tags
|
// target tags must be the same as zap request target tags
|
||||||
let tags = zap_target_to_tags(target)
|
let tags = zap_target_to_tags(target)
|
||||||
|
|
||||||
@@ -520,10 +525,13 @@ func make_private_zap_request_event(identity: FullKeypair, enc_key: FullKeypair,
|
|||||||
note.id = calculate_event_id(ev: note)
|
note.id = calculate_event_id(ev: note)
|
||||||
note.sig = sign_event(privkey: identity.privkey, ev: note)
|
note.sig = sign_event(privkey: identity.privkey, ev: note)
|
||||||
|
|
||||||
guard let note_json = encode_json(note) else {
|
guard let note_json = encode_json(note),
|
||||||
|
let enc = encrypt_message(message: note_json, privkey: enc_key.privkey, to_pk: target.pubkey, encoding: .bech32)
|
||||||
|
else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return encrypt_message(message: note_json, privkey: enc_key.privkey, to_pk: target.pubkey, encoding: .bech32)
|
|
||||||
|
return PrivateZapRequest(req: ZapRequest(ev: note), enc: enc)
|
||||||
}
|
}
|
||||||
|
|
||||||
func decrypt_private_zap(our_privkey: String, zapreq: NostrEvent, target: ZapTarget) -> NostrEvent? {
|
func decrypt_private_zap(our_privkey: String, zapreq: NostrEvent, target: ZapTarget) -> NostrEvent? {
|
||||||
@@ -587,7 +595,30 @@ func generate_private_keypair(our_privkey: String, id: String, created_at: Int64
|
|||||||
return FullKeypair(pubkey: pubkey, privkey: privkey)
|
return FullKeypair(pubkey: pubkey, privkey: privkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func make_zap_request_event(keypair: FullKeypair, content: String, relays: [RelayDescriptor], target: ZapTarget, zap_type: ZapType) -> NostrEvent? {
|
enum MakeZapRequest {
|
||||||
|
case priv(ZapRequest, PrivateZapRequest)
|
||||||
|
case normal(ZapRequest)
|
||||||
|
|
||||||
|
var private_inner_request: ZapRequest {
|
||||||
|
switch self {
|
||||||
|
case .priv(_, let pzr):
|
||||||
|
return pzr.req
|
||||||
|
case .normal(let zr):
|
||||||
|
return zr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var potentially_anon_outer_request: ZapRequest {
|
||||||
|
switch self {
|
||||||
|
case .priv(let zr, _):
|
||||||
|
return zr
|
||||||
|
case .normal(let zr):
|
||||||
|
return zr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func make_zap_request_event(keypair: FullKeypair, content: String, relays: [RelayDescriptor], target: ZapTarget, zap_type: ZapType) -> MakeZapRequest? {
|
||||||
var tags = zap_target_to_tags(target)
|
var tags = zap_target_to_tags(target)
|
||||||
var relay_tag = ["relays"]
|
var relay_tag = ["relays"]
|
||||||
relay_tag.append(contentsOf: relays.map { $0.url.id })
|
relay_tag.append(contentsOf: relays.map { $0.url.id })
|
||||||
@@ -597,6 +628,8 @@ func make_zap_request_event(keypair: FullKeypair, content: String, relays: [Rela
|
|||||||
|
|
||||||
let now = Int64(Date().timeIntervalSince1970)
|
let now = Int64(Date().timeIntervalSince1970)
|
||||||
|
|
||||||
|
var privzap_req: PrivateZapRequest?
|
||||||
|
|
||||||
var message = content
|
var message = content
|
||||||
switch zap_type {
|
switch zap_type {
|
||||||
case .pub:
|
case .pub:
|
||||||
@@ -614,14 +647,20 @@ func make_zap_request_event(keypair: FullKeypair, content: String, relays: [Rela
|
|||||||
guard let privreq = make_private_zap_request_event(identity: keypair, enc_key: kp, target: target, message: message) else {
|
guard let privreq = make_private_zap_request_event(identity: keypair, enc_key: kp, target: target, message: message) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
tags.append(["anon", privreq])
|
tags.append(["anon", privreq.enc])
|
||||||
message = ""
|
message = ""
|
||||||
|
privzap_req = privreq
|
||||||
}
|
}
|
||||||
|
|
||||||
let ev = NostrEvent(content: message, pubkey: kp.pubkey, kind: 9734, tags: tags, createdAt: now)
|
let ev = NostrEvent(content: message, pubkey: kp.pubkey, kind: 9734, tags: tags, createdAt: now)
|
||||||
ev.id = calculate_event_id(ev: ev)
|
ev.id = calculate_event_id(ev: ev)
|
||||||
ev.sig = sign_event(privkey: kp.privkey, ev: ev)
|
ev.sig = sign_event(privkey: kp.privkey, ev: ev)
|
||||||
return ev
|
let zapreq = ZapRequest(ev: ev)
|
||||||
|
if let privzap_req {
|
||||||
|
return .priv(zapreq, privzap_req)
|
||||||
|
} else {
|
||||||
|
return .normal(zapreq)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func uniq<T: Hashable>(_ xs: [T]) -> [T] {
|
func uniq<T: Hashable>(_ xs: [T]) -> [T] {
|
||||||
@@ -927,6 +966,28 @@ func first_eref_mention(ev: NostrEvent, privkey: String?) -> Mention? {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Transforms a `NostrEvent` of known kind `NostrKind.like`to a human-readable emoji.
|
||||||
|
If the known kind is not a `NostrKind.like`, it will return `nil`.
|
||||||
|
If the event content is an empty string or `+`, it will map that to a heart ❤️ emoji.
|
||||||
|
If the event content is a "-", it will map that to a dislike 👎 emoji.
|
||||||
|
Otherwise, it will return the event content at face value without transforming it.
|
||||||
|
*/
|
||||||
|
func to_reaction_emoji(ev: NostrEvent) -> String? {
|
||||||
|
guard ev.known_kind == NostrKind.like else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ev.content {
|
||||||
|
case "", "+":
|
||||||
|
return "❤️"
|
||||||
|
case "-":
|
||||||
|
return "👎"
|
||||||
|
default:
|
||||||
|
return ev.content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension [ReferencedId] {
|
extension [ReferencedId] {
|
||||||
var pRefs: [ReferencedId] {
|
var pRefs: [ReferencedId] {
|
||||||
get {
|
get {
|
||||||
|
|||||||
@@ -22,4 +22,6 @@ enum NostrKind: Int {
|
|||||||
case list = 30000
|
case list = 30000
|
||||||
case zap = 9735
|
case zap = 9735
|
||||||
case zap_request = 9734
|
case zap_request = 9734
|
||||||
|
case nwc_request = 23194
|
||||||
|
case nwc_response = 23195
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class Profiles {
|
|||||||
qos: .userInteractive,
|
qos: .userInteractive,
|
||||||
attributes: .concurrent)
|
attributes: .concurrent)
|
||||||
|
|
||||||
var profiles: [String: TimestampedProfile] = [:]
|
private var profiles: [String: TimestampedProfile] = [:]
|
||||||
var validated: [String: NIP05] = [:]
|
var validated: [String: NIP05] = [:]
|
||||||
var nip05_pubkey: [String: String] = [:]
|
var nip05_pubkey: [String: String] = [:]
|
||||||
var zappers: [String: String] = [:]
|
var zappers: [String: String] = [:]
|
||||||
@@ -28,6 +28,12 @@ class Profiles {
|
|||||||
validated[pk]
|
validated[pk]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func enumerated() -> EnumeratedSequence<[String: TimestampedProfile]> {
|
||||||
|
return queue.sync {
|
||||||
|
return profiles.enumerated()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func lookup_zapper(pubkey: String) -> String? {
|
func lookup_zapper(pubkey: String) -> String? {
|
||||||
zappers[pubkey]
|
zappers[pubkey]
|
||||||
}
|
}
|
||||||
@@ -77,3 +83,9 @@ class Profiles {
|
|||||||
return Date.now.timeIntervalSince(pull_date) < Profiles.db_freshness_threshold
|
return Date.now.timeIntervalSince(pull_date) < Profiles.db_freshness_threshold
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func invalidate_zapper_cache(pubkey: String, profiles: Profiles, lnurl: LNUrls) {
|
||||||
|
profiles.zappers.removeValue(forKey: pubkey)
|
||||||
|
lnurl.endpoints.removeValue(forKey: pubkey)
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,21 +10,46 @@ import Foundation
|
|||||||
public struct RelayInfo: Codable {
|
public struct RelayInfo: Codable {
|
||||||
let read: Bool?
|
let read: Bool?
|
||||||
let write: Bool?
|
let write: Bool?
|
||||||
let ephemeral: Bool?
|
|
||||||
|
|
||||||
init(read: Bool, write: Bool, ephemeral: Bool = false) {
|
init(read: Bool, write: Bool) {
|
||||||
self.read = read
|
self.read = read
|
||||||
self.write = write
|
self.write = write
|
||||||
self.ephemeral = ephemeral
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static let rw = RelayInfo(read: true, write: true, ephemeral: false)
|
static let rw = RelayInfo(read: true, write: true)
|
||||||
static let ephemeral = RelayInfo(read: true, write: true, ephemeral: true)
|
}
|
||||||
|
|
||||||
|
enum RelayVariant {
|
||||||
|
case regular
|
||||||
|
case ephemeral
|
||||||
|
case nwc
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct RelayDescriptor {
|
public struct RelayDescriptor {
|
||||||
public let url: RelayURL
|
let url: RelayURL
|
||||||
public let info: RelayInfo
|
let info: RelayInfo
|
||||||
|
let variant: RelayVariant
|
||||||
|
|
||||||
|
init(url: RelayURL, info: RelayInfo, variant: RelayVariant = .regular) {
|
||||||
|
self.url = url
|
||||||
|
self.info = info
|
||||||
|
self.variant = variant
|
||||||
|
}
|
||||||
|
|
||||||
|
var ephemeral: Bool {
|
||||||
|
switch variant {
|
||||||
|
case .regular:
|
||||||
|
return false
|
||||||
|
case .ephemeral:
|
||||||
|
return true
|
||||||
|
case .nwc:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func nwc(url: RelayURL) -> RelayDescriptor {
|
||||||
|
return RelayDescriptor(url: url, info: .rw, variant: .nwc)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RelayFlags: Int {
|
enum RelayFlags: Int {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class RelayPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var our_descriptors: [RelayDescriptor] {
|
var our_descriptors: [RelayDescriptor] {
|
||||||
return all_descriptors.filter { d in !(d.info.ephemeral ?? false) }
|
return all_descriptors.filter { d in !d.ephemeral }
|
||||||
}
|
}
|
||||||
|
|
||||||
var all_descriptors: [RelayDescriptor] {
|
var all_descriptors: [RelayDescriptor] {
|
||||||
@@ -91,7 +91,8 @@ class RelayPool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func add_relay(_ url: RelayURL, info: RelayInfo) throws {
|
func add_relay(_ desc: RelayDescriptor) throws {
|
||||||
|
let url = desc.url
|
||||||
let relay_id = get_relay_id(url)
|
let relay_id = get_relay_id(url)
|
||||||
if get_relay(relay_id) != nil {
|
if get_relay(relay_id) != nil {
|
||||||
throw RelayError.RelayAlreadyExists
|
throw RelayError.RelayAlreadyExists
|
||||||
@@ -99,8 +100,7 @@ class RelayPool {
|
|||||||
let conn = RelayConnection(url: url) { event in
|
let conn = RelayConnection(url: url) { event in
|
||||||
self.handle_event(relay_id: relay_id, event: event)
|
self.handle_event(relay_id: relay_id, event: event)
|
||||||
}
|
}
|
||||||
let descriptor = RelayDescriptor(url: url, info: info)
|
let relay = Relay(descriptor: desc, connection: conn)
|
||||||
let relay = Relay(descriptor: descriptor, connection: conn)
|
|
||||||
self.relays.append(relay)
|
self.relays.append(relay)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@ class RelayPool {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relay.descriptor.info.ephemeral ?? false) && skip_ephemeral {
|
if relay.descriptor.ephemeral && skip_ephemeral {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,7 +266,7 @@ func add_rw_relay(_ pool: RelayPool, _ url: String) {
|
|||||||
guard let url = RelayURL(url) else {
|
guard let url = RelayURL(url) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try? pool.add_relay(url, info: RelayInfo.rw)
|
try? pool.add_relay(RelayDescriptor(url: url, info: .rw))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -55,11 +55,46 @@ class PreviewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ZapsDataModel: ObservableObject {
|
class ZapsDataModel: ObservableObject {
|
||||||
@Published var zaps: [Zap]
|
@Published var zaps: [Zapping]
|
||||||
|
|
||||||
init(_ zaps: [Zap]) {
|
init(_ zaps: [Zapping]) {
|
||||||
self.zaps = zaps
|
self.zaps = zaps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func confirm_nwc(reqid: String) {
|
||||||
|
guard let zap = zaps.first(where: { z in z.request.id == reqid }),
|
||||||
|
case .pending(let pzap) = zap
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch pzap.state {
|
||||||
|
case .external:
|
||||||
|
break
|
||||||
|
case .nwc(let nwc_state):
|
||||||
|
if nwc_state.update_state(state: .confirmed) {
|
||||||
|
self.objectWillChange.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var zap_total: Int64 {
|
||||||
|
zaps.reduce(0) { total, zap in total + zap.amount }
|
||||||
|
}
|
||||||
|
|
||||||
|
func from(_ pubkey: String) -> [Zapping] {
|
||||||
|
return self.zaps.filter { z in z.request.pubkey == pubkey }
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func remove(reqid: String) -> Bool {
|
||||||
|
guard zaps.first(where: { z in z.request.id == reqid }) != nil else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
self.zaps = zaps.filter { z in z.request.id != reqid }
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class RelativeTimeModel: ObservableObject {
|
class RelativeTimeModel: ObservableObject {
|
||||||
@@ -86,7 +121,7 @@ class EventData {
|
|||||||
return preview_model.state
|
return preview_model.state
|
||||||
}
|
}
|
||||||
|
|
||||||
init(zaps: [Zap] = []) {
|
init(zaps: [Zapping] = []) {
|
||||||
self.translations_model = .init(state: .havent_tried)
|
self.translations_model = .init(state: .havent_tried)
|
||||||
self.artifacts_model = .init(state: .not_loaded)
|
self.artifacts_model = .init(state: .not_loaded)
|
||||||
self.zaps_model = .init(zaps)
|
self.zaps_model = .init(zaps)
|
||||||
@@ -131,12 +166,23 @@ class EventCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func store_zap(zap: Zap) -> Bool {
|
func store_zap(zap: Zapping) -> Bool {
|
||||||
let data = get_cache_data(zap.target.id).zaps_model
|
let data = get_cache_data(zap.target.id).zaps_model
|
||||||
return insert_uniq_sorted_zap_by_amount(zaps: &data.zaps, new_zap: zap)
|
return insert_uniq_sorted_zap_by_amount(zaps: &data.zaps, new_zap: zap)
|
||||||
}
|
}
|
||||||
|
|
||||||
func lookup_zaps(target: ZapTarget) -> [Zap] {
|
func remove_zap(zap: Zapping) {
|
||||||
|
switch zap.target {
|
||||||
|
case .note(let note_target):
|
||||||
|
let zaps = get_cache_data(note_target.note_id).zaps_model
|
||||||
|
zaps.remove(reqid: zap.request.id)
|
||||||
|
case .profile:
|
||||||
|
// these aren't stored anywhere yet
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookup_zaps(target: ZapTarget) -> [Zapping] {
|
||||||
return get_cache_data(target.id).zaps_model.zaps
|
return get_cache_data(target.id).zaps_model.zaps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,18 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap, cmp: (Zap, Zap) -> Bool) -> Bool {
|
func insert_uniq_sorted_zap(zaps: inout [Zapping], new_zap: Zapping, cmp: (Zapping, Zapping) -> Bool) -> Bool {
|
||||||
var i: Int = 0
|
var i: Int = 0
|
||||||
|
|
||||||
for zap in zaps {
|
for zap in zaps {
|
||||||
// don't insert duplicate events
|
if new_zap.request.id == zap.request.id {
|
||||||
if new_zap.event.id == zap.event.id {
|
// replace pending
|
||||||
|
if !new_zap.is_pending && zap.is_pending {
|
||||||
|
print("nwc: replacing pending with real zap \(new_zap.request.id)")
|
||||||
|
zaps[i] = new_zap
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// don't insert duplicate events
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,16 +34,16 @@ func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap, cmp: (Zap, Zap) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func insert_uniq_sorted_zap_by_created(zaps: inout [Zap], new_zap: Zap) -> Bool {
|
func insert_uniq_sorted_zap_by_created(zaps: inout [Zapping], new_zap: Zapping) -> Bool {
|
||||||
return insert_uniq_sorted_zap(zaps: &zaps, new_zap: new_zap) { (a, b) in
|
return insert_uniq_sorted_zap(zaps: &zaps, new_zap: new_zap) { (a, b) in
|
||||||
a.event.created_at > b.event.created_at
|
a.created_at > b.created_at
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func insert_uniq_sorted_zap_by_amount(zaps: inout [Zap], new_zap: Zap) -> Bool {
|
func insert_uniq_sorted_zap_by_amount(zaps: inout [Zapping], new_zap: Zapping) -> Bool {
|
||||||
return insert_uniq_sorted_zap(zaps: &zaps, new_zap: new_zap) { (a, b) in
|
return insert_uniq_sorted_zap(zaps: &zaps, new_zap: new_zap) { (a, b) in
|
||||||
a.invoice.amount > b.invoice.amount
|
a.amount > b.amount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import secp256k1
|
|||||||
|
|
||||||
let PUBKEY_HRP = "npub"
|
let PUBKEY_HRP = "npub"
|
||||||
|
|
||||||
struct FullKeypair {
|
struct FullKeypair: Equatable {
|
||||||
let pubkey: String
|
let pubkey: String
|
||||||
let privkey: String
|
let privkey: String
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,6 +92,9 @@ extension Notification.Name {
|
|||||||
static var onlyzaps_mode: Notification.Name {
|
static var onlyzaps_mode: Notification.Name {
|
||||||
return Notification.Name("hide_reactions")
|
return Notification.Name("hide_reactions")
|
||||||
}
|
}
|
||||||
|
static var attached_wallet: Notification.Name {
|
||||||
|
return Notification.Name("attached_wallet")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {
|
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {
|
||||||
|
|||||||
@@ -22,20 +22,37 @@ class Relayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum OnFlush {
|
||||||
|
case once((PostedEvent) -> Void)
|
||||||
|
case all((PostedEvent) -> Void)
|
||||||
|
}
|
||||||
|
|
||||||
class PostedEvent {
|
class PostedEvent {
|
||||||
let event: NostrEvent
|
let event: NostrEvent
|
||||||
let skip_ephemeral: Bool
|
let skip_ephemeral: Bool
|
||||||
var remaining: [Relayer]
|
var remaining: [Relayer]
|
||||||
|
let flush_after: Date?
|
||||||
|
var flushed_once: Bool
|
||||||
|
let on_flush: OnFlush?
|
||||||
|
|
||||||
init(event: NostrEvent, remaining: [String], skip_ephemeral: Bool) {
|
init(event: NostrEvent, remaining: [String], skip_ephemeral: Bool, flush_after: Date?, on_flush: OnFlush?) {
|
||||||
self.event = event
|
self.event = event
|
||||||
self.skip_ephemeral = skip_ephemeral
|
self.skip_ephemeral = skip_ephemeral
|
||||||
|
self.flush_after = flush_after
|
||||||
|
self.on_flush = on_flush
|
||||||
|
self.flushed_once = false
|
||||||
self.remaining = remaining.map {
|
self.remaining = remaining.map {
|
||||||
Relayer(relay: $0, attempts: 0, retry_after: 2.0)
|
Relayer(relay: $0, attempts: 0, retry_after: 10.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum CancelSendErr {
|
||||||
|
case nothing_to_cancel
|
||||||
|
case not_delayed
|
||||||
|
case too_late
|
||||||
|
}
|
||||||
|
|
||||||
class PostBox {
|
class PostBox {
|
||||||
let pool: RelayPool
|
let pool: RelayPool
|
||||||
var events: [String: PostedEvent]
|
var events: [String: PostedEvent]
|
||||||
@@ -46,12 +63,37 @@ class PostBox {
|
|||||||
pool.register_handler(sub_id: "postbox", handler: handle_event)
|
pool.register_handler(sub_id: "postbox", handler: handle_event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// only works reliably on delay-sent events
|
||||||
|
func cancel_send(evid: String) -> CancelSendErr? {
|
||||||
|
guard let ev = events[evid] else {
|
||||||
|
return .nothing_to_cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let after = ev.flush_after else {
|
||||||
|
return .not_delayed
|
||||||
|
}
|
||||||
|
|
||||||
|
guard Date.now < after else {
|
||||||
|
return .too_late
|
||||||
|
}
|
||||||
|
|
||||||
|
events.removeValue(forKey: evid)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func try_flushing_events() {
|
func try_flushing_events() {
|
||||||
let now = Int64(Date().timeIntervalSince1970)
|
let now = Int64(Date().timeIntervalSince1970)
|
||||||
for kv in events {
|
for kv in events {
|
||||||
let event = kv.value
|
let event = kv.value
|
||||||
|
|
||||||
|
// some are delayed
|
||||||
|
if let after = event.flush_after, Date.now.timeIntervalSince1970 < after.timeIntervalSince1970 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
for relayer in event.remaining {
|
for relayer in event.remaining {
|
||||||
if relayer.last_attempt == nil || (now >= (relayer.last_attempt! + Int64(relayer.retry_after))) {
|
if relayer.last_attempt == nil ||
|
||||||
|
(now >= (relayer.last_attempt! + Int64(relayer.retry_after))) {
|
||||||
print("attempt #\(relayer.attempts) to flush event '\(event.event.content)' to \(relayer.relay) after \(relayer.retry_after) seconds")
|
print("attempt #\(relayer.attempts) to flush event '\(event.event.content)' to \(relayer.relay) after \(relayer.retry_after) seconds")
|
||||||
flush_event(event, to_relay: relayer)
|
flush_event(event, to_relay: relayer)
|
||||||
}
|
}
|
||||||
@@ -60,8 +102,6 @@ class PostBox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handle_event(relay_id: String, _ ev: NostrConnectionEvent) {
|
func handle_event(relay_id: String, _ ev: NostrConnectionEvent) {
|
||||||
try_flushing_events()
|
|
||||||
|
|
||||||
guard case .nostr_event(let resp) = ev else {
|
guard case .nostr_event(let resp) = ev else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -73,16 +113,31 @@ class PostBox {
|
|||||||
remove_relayer(relay_id: relay_id, event_id: cr.event_id)
|
remove_relayer(relay_id: relay_id, event_id: cr.event_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func remove_relayer(relay_id: String, event_id: String) {
|
@discardableResult
|
||||||
|
func remove_relayer(relay_id: String, event_id: String) -> Bool {
|
||||||
guard let ev = self.events[event_id] else {
|
guard let ev = self.events[event_id] else {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
ev.remaining = ev.remaining.filter {
|
|
||||||
$0.relay != relay_id
|
if let on_flush = ev.on_flush {
|
||||||
|
switch on_flush {
|
||||||
|
case .once(let cb):
|
||||||
|
if !ev.flushed_once {
|
||||||
|
ev.flushed_once = true
|
||||||
|
cb(ev)
|
||||||
|
}
|
||||||
|
case .all(let cb):
|
||||||
|
cb(ev)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let prev_count = ev.remaining.count
|
||||||
|
ev.remaining = ev.remaining.filter { $0.relay != relay_id }
|
||||||
|
let after_count = ev.remaining.count
|
||||||
if ev.remaining.count == 0 {
|
if ev.remaining.count == 0 {
|
||||||
self.events.removeValue(forKey: event_id)
|
self.events.removeValue(forKey: event_id)
|
||||||
}
|
}
|
||||||
|
return prev_count != after_count
|
||||||
}
|
}
|
||||||
|
|
||||||
private func flush_event(_ event: PostedEvent, to_relay: Relayer? = nil) {
|
private func flush_event(_ event: PostedEvent, to_relay: Relayer? = nil) {
|
||||||
@@ -95,20 +150,31 @@ class PostBox {
|
|||||||
relayer.attempts += 1
|
relayer.attempts += 1
|
||||||
relayer.last_attempt = Int64(Date().timeIntervalSince1970)
|
relayer.last_attempt = Int64(Date().timeIntervalSince1970)
|
||||||
relayer.retry_after *= 1.5
|
relayer.retry_after *= 1.5
|
||||||
|
if let relay = pool.get_relay(relayer.relay) {
|
||||||
|
print("flushing event \(event.event.id) to \(relayer.relay)")
|
||||||
|
} else {
|
||||||
|
print("could not find relay when flushing: \(relayer.relay)")
|
||||||
|
}
|
||||||
pool.send(.event(event.event), to: [relayer.relay], skip_ephemeral: event.skip_ephemeral)
|
pool.send(.event(event.event), to: [relayer.relay], skip_ephemeral: event.skip_ephemeral)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func send(_ event: NostrEvent, to: [String]? = nil, skip_ephemeral: Bool = true) {
|
func send(_ event: NostrEvent, to: [String]? = nil, skip_ephemeral: Bool = true, delay: TimeInterval? = nil, on_flush: OnFlush? = nil) {
|
||||||
// Don't add event if we already have it
|
// Don't add event if we already have it
|
||||||
if events[event.id] != nil {
|
if events[event.id] != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let remaining = to ?? pool.our_descriptors.map { $0.url.id }
|
let remaining = to ?? pool.our_descriptors.map { $0.url.id }
|
||||||
let posted_ev = PostedEvent(event: event, remaining: remaining, skip_ephemeral: skip_ephemeral)
|
let after = delay.map { d in Date.now.addingTimeInterval(d) }
|
||||||
|
let posted_ev = PostedEvent(event: event, remaining: remaining, skip_ephemeral: skip_ephemeral, flush_after: after, on_flush: on_flush)
|
||||||
|
|
||||||
events[event.id] = posted_ev
|
events[event.id] = posted_ev
|
||||||
|
|
||||||
flush_event(posted_ev)
|
if after == nil {
|
||||||
|
flush_event(posted_ev)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
260
damus/Util/WalletConnect.swift
Normal file
260
damus/Util/WalletConnect.swift
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
//
|
||||||
|
// WalletConnect.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-03-22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct WalletConnectURL: Equatable {
|
||||||
|
static func == (lhs: WalletConnectURL, rhs: WalletConnectURL) -> Bool {
|
||||||
|
return lhs.keypair == rhs.keypair &&
|
||||||
|
lhs.pubkey == rhs.pubkey &&
|
||||||
|
lhs.relay == rhs.relay
|
||||||
|
}
|
||||||
|
|
||||||
|
let relay: RelayURL
|
||||||
|
let keypair: FullKeypair
|
||||||
|
let pubkey: String
|
||||||
|
let lud16: String?
|
||||||
|
|
||||||
|
func to_url() -> URL {
|
||||||
|
var urlComponents = URLComponents()
|
||||||
|
urlComponents.scheme = "nostrwalletconnect"
|
||||||
|
urlComponents.host = pubkey
|
||||||
|
urlComponents.queryItems = [
|
||||||
|
URLQueryItem(name: "relay", value: relay.id),
|
||||||
|
URLQueryItem(name: "secret", value: keypair.privkey)
|
||||||
|
]
|
||||||
|
|
||||||
|
if let lud16 {
|
||||||
|
urlComponents.queryItems?.append(URLQueryItem(name: "lud16", value: lud16))
|
||||||
|
}
|
||||||
|
|
||||||
|
return urlComponents.url!
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(str: String) {
|
||||||
|
guard let url = URL(string: str),
|
||||||
|
url.scheme == "nostrwalletconnect" || url.scheme == "nostr+walletconnect",
|
||||||
|
let pk = url.host, pk.utf8.count == 64,
|
||||||
|
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
|
||||||
|
let items = components.queryItems,
|
||||||
|
let relay = items.first(where: { qi in qi.name == "relay" })?.value,
|
||||||
|
let relay_url = RelayURL(relay),
|
||||||
|
let secret = items.first(where: { qi in qi.name == "secret" })?.value,
|
||||||
|
secret.utf8.count == 64,
|
||||||
|
let our_pk = privkey_to_pubkey(privkey: secret)
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let lud16 = items.first(where: { qi in qi.name == "lud16" })?.value
|
||||||
|
let keypair = FullKeypair(pubkey: our_pk, privkey: secret)
|
||||||
|
self = WalletConnectURL(pubkey: pk, relay: relay_url, keypair: keypair, lud16: lud16)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(pubkey: String, relay: RelayURL, keypair: FullKeypair, lud16: String?) {
|
||||||
|
self.pubkey = pubkey
|
||||||
|
self.relay = relay
|
||||||
|
self.keypair = keypair
|
||||||
|
self.lud16 = lud16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WalletRequest<T: Codable>: Codable {
|
||||||
|
let method: String
|
||||||
|
let params: T?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WalletResponseErr: Codable {
|
||||||
|
let code: String?
|
||||||
|
let message: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PayInvoiceResponse: Decodable {
|
||||||
|
let preimage: String
|
||||||
|
}
|
||||||
|
|
||||||
|
enum WalletResponseResultType: String {
|
||||||
|
case pay_invoice
|
||||||
|
}
|
||||||
|
|
||||||
|
enum WalletResponseResult {
|
||||||
|
case pay_invoice(PayInvoiceResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FullWalletResponse {
|
||||||
|
let req_id: String
|
||||||
|
let response: WalletResponse
|
||||||
|
|
||||||
|
init?(from: NostrEvent, nwc: WalletConnectURL) async {
|
||||||
|
guard let req_id = from.referenced_ids.first else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.req_id = req_id.ref_id
|
||||||
|
|
||||||
|
let ares = Task {
|
||||||
|
guard let json = decrypt_dm(nwc.keypair.privkey, pubkey: nwc.pubkey, content: from.content, encoding: .base64),
|
||||||
|
let resp: WalletResponse = decode_json(json)
|
||||||
|
else {
|
||||||
|
let resp: WalletResponse? = nil
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let res = await ares.value else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.response = res
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WalletResponse: Decodable {
|
||||||
|
let result_type: WalletResponseResultType
|
||||||
|
let error: WalletResponseErr?
|
||||||
|
let result: WalletResponseResult?
|
||||||
|
|
||||||
|
private enum CodingKeys: CodingKey {
|
||||||
|
case result_type, error, result
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
let result_type_str = try container.decode(String.self, forKey: .result_type)
|
||||||
|
|
||||||
|
guard let result_type = WalletResponseResultType(rawValue: result_type_str) else {
|
||||||
|
throw DecodingError.typeMismatch(WalletResponseResultType.self, .init(codingPath: decoder.codingPath, debugDescription: "result_type \(result_type_str) is unknown"))
|
||||||
|
}
|
||||||
|
|
||||||
|
self.result_type = result_type
|
||||||
|
self.error = try container.decodeIfPresent(WalletResponseErr.self, forKey: .error)
|
||||||
|
|
||||||
|
guard self.error == nil else {
|
||||||
|
self.result = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch result_type {
|
||||||
|
case .pay_invoice:
|
||||||
|
let res = try container.decode(PayInvoiceResponse.self, forKey: .result)
|
||||||
|
self.result = .pay_invoice(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func make_wallet_pay_invoice_request(invoice: String) -> WalletRequest<PayInvoiceRequest> {
|
||||||
|
let data = PayInvoiceRequest(invoice: invoice)
|
||||||
|
return WalletRequest(method: "pay_invoice", params: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func make_wallet_balance_request() -> WalletRequest<EmptyRequest> {
|
||||||
|
return WalletRequest(method: "get_balance", params: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EmptyRequest: Codable {
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PayInvoiceRequest: Codable {
|
||||||
|
let invoice: String
|
||||||
|
}
|
||||||
|
|
||||||
|
func make_wallet_connect_request<T>(req: WalletRequest<T>, to_pk: String, keypair: FullKeypair) -> NostrEvent? {
|
||||||
|
let tags = [["p", to_pk]]
|
||||||
|
let created_at = Int64(Date().timeIntervalSince1970)
|
||||||
|
guard let content = encode_json(req) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return create_encrypted_event(content, to_pk: to_pk, tags: tags, keypair: keypair, created_at: created_at, kind: 23194)
|
||||||
|
}
|
||||||
|
|
||||||
|
func subscribe_to_nwc(url: WalletConnectURL, pool: RelayPool) {
|
||||||
|
var filter: NostrFilter = .filter_kinds([NostrKind.nwc_response.rawValue])
|
||||||
|
filter.authors = [url.pubkey]
|
||||||
|
filter.limit = 0
|
||||||
|
let sub = NostrSubscribe(filters: [filter], sub_id: "nwc")
|
||||||
|
|
||||||
|
pool.send(.subscribe(sub), to: [url.relay.id], skip_ephemeral: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func nwc_pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: String, delay: TimeInterval? = 5.0, on_flush: OnFlush? = nil) -> NostrEvent? {
|
||||||
|
let req = make_wallet_pay_invoice_request(invoice: invoice)
|
||||||
|
guard let ev = make_wallet_connect_request(req: req, to_pk: url.pubkey, keypair: url.keypair) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
try? pool.add_relay(.nwc(url: url.relay))
|
||||||
|
subscribe_to_nwc(url: url, pool: pool)
|
||||||
|
post.send(ev, to: [url.relay.id], skip_ephemeral: false, delay: delay, on_flush: on_flush)
|
||||||
|
return ev
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func nwc_success(state: DamusState, resp: FullWalletResponse) {
|
||||||
|
// find the pending zap and mark it as pending-confirmed
|
||||||
|
for kv in state.zaps.our_zaps {
|
||||||
|
let zaps = kv.value
|
||||||
|
|
||||||
|
for zap in zaps {
|
||||||
|
guard case .pending(let pzap) = zap,
|
||||||
|
case .nwc(let nwc_state) = pzap.state,
|
||||||
|
case .postbox_pending(let nwc_req) = nwc_state.state,
|
||||||
|
nwc_req.id == resp.req_id
|
||||||
|
else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if nwc_state.update_state(state: .confirmed) {
|
||||||
|
// notify the zaps model of an update so it can mark them as paid
|
||||||
|
state.events.get_cache_data(pzap.target.id).zaps_model.objectWillChange.send()
|
||||||
|
print("NWC success confirmed")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func send_donation_zap(pool: RelayPool, postbox: PostBox, nwc: WalletConnectURL, percent: Int, base_msats: Int64) async {
|
||||||
|
let percent_f = Double(percent) / 100.0
|
||||||
|
let donations_msats = Int64(percent_f * Double(base_msats))
|
||||||
|
|
||||||
|
let payreq = LNUrlPayRequest(allowsNostr: true, commentAllowed: nil, nostrPubkey: "", callback: "https://sendsats.lol/@damus")
|
||||||
|
guard let invoice = await fetch_zap_invoice(payreq, zapreq: nil, msats: donations_msats, zap_type: .non_zap, comment: nil) else {
|
||||||
|
// we failed... oh well. no donation for us.
|
||||||
|
print("damus-donation failed to fetch invoice")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
print("damus-donation donating...")
|
||||||
|
nwc_pay(url: nwc, pool: pool, post: postbox, invoice: invoice, delay: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func nwc_error(zapcache: Zaps, evcache: EventCache, resp: FullWalletResponse) {
|
||||||
|
// find a pending zap with the nwc request id associated with this response and remove it
|
||||||
|
for kv in zapcache.our_zaps {
|
||||||
|
let zaps = kv.value
|
||||||
|
|
||||||
|
for zap in zaps {
|
||||||
|
guard case .pending(let pzap) = zap,
|
||||||
|
case .nwc(let nwc_state) = pzap.state,
|
||||||
|
case .postbox_pending(let req) = nwc_state.state,
|
||||||
|
req.id == resp.req_id
|
||||||
|
else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the pending zap if there was an error
|
||||||
|
let reqid = ZapRequestId(from_pending: pzap)
|
||||||
|
remove_zap(reqid: reqid, zapcache: zapcache, evcache: evcache)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct NoteZapTarget: Equatable {
|
public struct NoteZapTarget: Equatable, Hashable {
|
||||||
public let note_id: String
|
public let note_id: String
|
||||||
public let author: String
|
public let author: String
|
||||||
}
|
}
|
||||||
@@ -41,6 +41,200 @@ public enum ZapTarget: Equatable {
|
|||||||
|
|
||||||
struct ZapRequest {
|
struct ZapRequest {
|
||||||
let ev: NostrEvent
|
let ev: NostrEvent
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ExtPendingZapStateType {
|
||||||
|
case fetching_invoice
|
||||||
|
case done
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExtPendingZapState: Equatable {
|
||||||
|
static func == (lhs: ExtPendingZapState, rhs: ExtPendingZapState) -> Bool {
|
||||||
|
return lhs.state == rhs.state
|
||||||
|
}
|
||||||
|
|
||||||
|
var state: ExtPendingZapStateType
|
||||||
|
|
||||||
|
init(state: ExtPendingZapStateType) {
|
||||||
|
self.state = state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PendingZapState: Equatable {
|
||||||
|
case nwc(NWCPendingZapState)
|
||||||
|
case external(ExtPendingZapState)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum NWCStateType: Equatable {
|
||||||
|
case fetching_invoice
|
||||||
|
case cancel_fetching_invoice
|
||||||
|
case postbox_pending(NostrEvent)
|
||||||
|
case confirmed
|
||||||
|
case failed
|
||||||
|
}
|
||||||
|
|
||||||
|
class NWCPendingZapState: Equatable {
|
||||||
|
private(set) var state: NWCStateType
|
||||||
|
let url: WalletConnectURL
|
||||||
|
|
||||||
|
init(state: NWCStateType, url: WalletConnectURL) {
|
||||||
|
self.state = state
|
||||||
|
self.url = url
|
||||||
|
}
|
||||||
|
|
||||||
|
//@discardableResult -- not discardable, the ZapsDataModel may need to send objectWillChange but we don't force it
|
||||||
|
func update_state(state: NWCStateType) -> Bool {
|
||||||
|
guard state != self.state else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
self.state = state
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
static func == (lhs: NWCPendingZapState, rhs: NWCPendingZapState) -> Bool {
|
||||||
|
return lhs.state == rhs.state && lhs.url == rhs.url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PendingZap {
|
||||||
|
let amount_msat: Int64
|
||||||
|
let target: ZapTarget
|
||||||
|
let request: ZapRequest
|
||||||
|
let type: ZapType
|
||||||
|
private(set) var state: PendingZapState
|
||||||
|
|
||||||
|
init(amount_msat: Int64, target: ZapTarget, request: MakeZapRequest, type: ZapType, state: PendingZapState) {
|
||||||
|
self.amount_msat = amount_msat
|
||||||
|
self.target = target
|
||||||
|
self.request = request.private_inner_request
|
||||||
|
self.type = type
|
||||||
|
self.state = state
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func update_state(model: ZapsDataModel, state: PendingZapState) -> Bool {
|
||||||
|
guard self.state != state else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
self.state = state
|
||||||
|
model.objectWillChange.send()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ZapRequestId: Equatable {
|
||||||
|
let reqid: String
|
||||||
|
|
||||||
|
init(from_zap: Zapping) {
|
||||||
|
self.reqid = from_zap.request.id
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from_makezap: MakeZapRequest) {
|
||||||
|
self.reqid = from_makezap.private_inner_request.ev.id
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from_pending: PendingZap) {
|
||||||
|
self.reqid = from_pending.request.ev.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Zapping {
|
||||||
|
case zap(Zap)
|
||||||
|
case pending(PendingZap)
|
||||||
|
|
||||||
|
var is_pending: Bool {
|
||||||
|
switch self {
|
||||||
|
case .zap:
|
||||||
|
return false
|
||||||
|
case .pending:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var is_paid: Bool {
|
||||||
|
switch self {
|
||||||
|
case .zap:
|
||||||
|
// we have a zap so this is proof of payment
|
||||||
|
return true
|
||||||
|
case .pending(let pzap):
|
||||||
|
switch pzap.state {
|
||||||
|
case .external:
|
||||||
|
// It could be but we don't know. We have to wait for a zap to know.
|
||||||
|
return false
|
||||||
|
case .nwc(let nwc_state):
|
||||||
|
// nwc confirmed that we have a payment, but we might not have zap yet
|
||||||
|
return nwc_state.state == .confirmed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var is_private: Bool {
|
||||||
|
switch self {
|
||||||
|
case .zap(let zap):
|
||||||
|
return zap.private_request != nil
|
||||||
|
case .pending(let pzap):
|
||||||
|
return pzap.type == .priv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var amount: Int64 {
|
||||||
|
switch self {
|
||||||
|
case .zap(let zap):
|
||||||
|
return zap.invoice.amount
|
||||||
|
case .pending(let pzap):
|
||||||
|
return pzap.amount_msat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var target: ZapTarget {
|
||||||
|
switch self {
|
||||||
|
case .zap(let zap):
|
||||||
|
return zap.target
|
||||||
|
case .pending(let pzap):
|
||||||
|
return pzap.target
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var request: NostrEvent {
|
||||||
|
switch self {
|
||||||
|
case .zap(let zap):
|
||||||
|
return zap.request_ev
|
||||||
|
case .pending(let pzap):
|
||||||
|
return pzap.request.ev
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var created_at: Int64 {
|
||||||
|
switch self {
|
||||||
|
case .zap(let zap):
|
||||||
|
return zap.event.created_at
|
||||||
|
case .pending(let pzap):
|
||||||
|
// pending zaps are created right away
|
||||||
|
return pzap.request.ev.created_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var event: NostrEvent? {
|
||||||
|
switch self {
|
||||||
|
case .zap(let zap):
|
||||||
|
return zap.event
|
||||||
|
case .pending:
|
||||||
|
// pending zaps don't have a zap event
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var is_anon: Bool {
|
||||||
|
switch self {
|
||||||
|
case .zap(let zap):
|
||||||
|
return zap.is_anon
|
||||||
|
case .pending(let pzap):
|
||||||
|
return pzap.type == .anon
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Zap {
|
struct Zap {
|
||||||
@@ -246,17 +440,16 @@ func fetch_static_payreq(_ lnurl: String) async -> LNUrlPayRequest? {
|
|||||||
return endpoint
|
return endpoint
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent?, sats: Int, zap_type: ZapType, comment: String?) async -> String? {
|
func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent?, msats: Int64, zap_type: ZapType, comment: String?) async -> String? {
|
||||||
guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else {
|
guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let zappable = payreq.allowsNostr ?? false
|
let zappable = payreq.allowsNostr ?? false
|
||||||
let amount: Int64 = Int64(sats) * 1000
|
|
||||||
|
|
||||||
var query = [URLQueryItem(name: "amount", value: "\(amount)")]
|
var query = [URLQueryItem(name: "amount", value: "\(msats)")]
|
||||||
|
|
||||||
if let zapreq, zappable && zap_type != .non_zap, let json = encode_json(zapreq) {
|
if zappable && zap_type != .non_zap, let json = encode_json(zapreq) {
|
||||||
print("zapreq json: \(json)")
|
print("zapreq json: \(json)")
|
||||||
query.append(URLQueryItem(name: "nostr", value: json))
|
query.append(URLQueryItem(name: "nostr", value: json))
|
||||||
}
|
}
|
||||||
@@ -293,5 +486,12 @@ func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent?, sats: Int
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// make sure it's the correct amount
|
||||||
|
guard let bolt11 = decode_bolt11(result.pr),
|
||||||
|
.specific(msats) == bolt11.amount
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return result.pr
|
return result.pr
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class Zaps {
|
class Zaps {
|
||||||
var zaps: [String: Zap]
|
var zaps: [String: Zapping]
|
||||||
let our_pubkey: String
|
let our_pubkey: String
|
||||||
var our_zaps: [String: [Zap]]
|
var our_zaps: [String: [Zapping]]
|
||||||
|
|
||||||
var event_counts: [String: Int]
|
var event_counts: [String: Int]
|
||||||
var event_totals: [String: Int64]
|
var event_totals: [String: Int64]
|
||||||
@@ -23,14 +23,41 @@ class Zaps {
|
|||||||
self.event_totals = [:]
|
self.event_totals = [:]
|
||||||
}
|
}
|
||||||
|
|
||||||
func add_zap(zap: Zap) {
|
func remove_zap(reqid: String) -> Zapping? {
|
||||||
if zaps[zap.event.id] != nil {
|
var res: Zapping? = nil
|
||||||
|
for kv in our_zaps {
|
||||||
|
let ours = kv.value
|
||||||
|
guard let zap = ours.first(where: { z in z.request.id == reqid }) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
res = zap
|
||||||
|
|
||||||
|
our_zaps[kv.key] = ours.filter { z in z.request.id != reqid }
|
||||||
|
|
||||||
|
if let count = event_counts[zap.target.id] {
|
||||||
|
event_counts[zap.target.id] = count - 1
|
||||||
|
}
|
||||||
|
if let total = event_totals[zap.target.id] {
|
||||||
|
event_totals[zap.target.id] = total - zap.amount
|
||||||
|
}
|
||||||
|
|
||||||
|
// we found the request id, we can stop looking
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
self.zaps.removeValue(forKey: reqid)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func add_zap(zap: Zapping) {
|
||||||
|
if zaps[zap.request.id] != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.zaps[zap.event.id] = zap
|
self.zaps[zap.request.id] = zap
|
||||||
|
|
||||||
// record our zaps for an event
|
// record our zaps for an event
|
||||||
if zap.request.ev.pubkey == our_pubkey {
|
if zap.request.pubkey == our_pubkey {
|
||||||
switch zap.target {
|
switch zap.target {
|
||||||
case .note(let note_target):
|
case .note(let note_target):
|
||||||
if our_zaps[note_target.note_id] == nil {
|
if our_zaps[note_target.note_id] == nil {
|
||||||
@@ -44,7 +71,7 @@ class Zaps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// don't count tips to self. lame.
|
// don't count tips to self. lame.
|
||||||
guard zap.request.ev.pubkey != zap.target.pubkey else {
|
guard zap.request.pubkey != zap.target.pubkey else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,8 +85,15 @@ class Zaps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
event_counts[id] = event_counts[id]! + 1
|
event_counts[id] = event_counts[id]! + 1
|
||||||
event_totals[id] = event_totals[id]! + zap.invoice.amount
|
event_totals[id] = event_totals[id]! + zap.amount
|
||||||
|
|
||||||
notify(.update_stats, zap.target.id)
|
notify(.update_stats, zap.target.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func remove_zap(reqid: ZapRequestId, zapcache: Zaps, evcache: EventCache) {
|
||||||
|
guard let zap = zapcache.remove_zap(reqid: reqid.reqid) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
evcache.get_cache_data(zap.target.id).zaps_model.remove(reqid: reqid.reqid)
|
||||||
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ struct EventActionBar: View {
|
|||||||
|
|
||||||
if let lnurl = self.lnurl {
|
if let lnurl = self.lnurl {
|
||||||
Spacer()
|
Spacer()
|
||||||
ZapButton(damus_state: damus_state, event: event, lnurl: lnurl, bar: bar)
|
ZapButton(damus_state: damus_state, event: event, lnurl: lnurl, zaps: self.damus_state.events.get_cache_data(self.event.id).zaps_model)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -227,7 +227,7 @@ struct EventActionBar_Previews: PreviewProvider {
|
|||||||
let likedbar_ours = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: test_event, our_boost: nil, our_zap: nil, our_reply: nil)
|
let likedbar_ours = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: test_event, our_boost: nil, our_zap: nil, our_reply: nil)
|
||||||
let maxed_bar = ActionBarModel(likes: 999, boosts: 999, zaps: 999, zap_total: 99999999, replies: 999, our_like: test_event, our_boost: test_event, our_zap: nil, our_reply: nil)
|
let maxed_bar = ActionBarModel(likes: 999, boosts: 999, zaps: 999, zap_total: 99999999, replies: 999, our_like: test_event, our_boost: test_event, our_zap: nil, our_reply: nil)
|
||||||
let extra_max_bar = ActionBarModel(likes: 9999, boosts: 9999, zaps: 9999, zap_total: 99999999, replies: 9999, our_like: test_event, our_boost: test_event, our_zap: nil, our_reply: test_event)
|
let extra_max_bar = ActionBarModel(likes: 9999, boosts: 9999, zaps: 9999, zap_total: 99999999, replies: 9999, our_like: test_event, our_boost: test_event, our_zap: nil, our_reply: test_event)
|
||||||
let mega_max_bar = ActionBarModel(likes: 9999999, boosts: 99999, zaps: 9999, zap_total: 99999999, replies: 9999999, our_like: test_event, our_boost: test_event, our_zap: test_zap, our_reply: test_event)
|
let mega_max_bar = ActionBarModel(likes: 9999999, boosts: 99999, zaps: 9999, zap_total: 99999999, replies: 9999999, our_like: test_event, our_boost: test_event, our_zap: .zap(test_zap), our_reply: test_event)
|
||||||
|
|
||||||
VStack(spacing: 50) {
|
VStack(spacing: 50) {
|
||||||
EventActionBar(damus_state: ds, event: ev, bar: bar)
|
EventActionBar(damus_state: ds, event: ev, bar: bar)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ struct AlbyButton: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Image("alby")
|
Image("alby")
|
||||||
|
|
||||||
Text("Connect to Alby")
|
Text("Attach Alby Wallet", comment: "Button to attach an Alby Wallet, a service that provides a Lightning wallet for zapping sats. Alby is the name of the service and should not be translated.")
|
||||||
}
|
}
|
||||||
.offset(x: -25)
|
.offset(x: -25)
|
||||||
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .center)
|
.frame(minWidth: 300, maxWidth: .infinity, minHeight: 50, maxHeight: 50, alignment: .center)
|
||||||
|
|||||||
@@ -181,7 +181,9 @@ struct TextEvent: View {
|
|||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
TopPart(is_anon: is_anon)
|
TopPart(is_anon: is_anon)
|
||||||
|
|
||||||
ReplyPart
|
if !options.contains(.no_replying_to) {
|
||||||
|
ReplyPart
|
||||||
|
}
|
||||||
|
|
||||||
EvBody(options: self.options)
|
EvBody(options: self.options)
|
||||||
|
|
||||||
|
|||||||
@@ -9,30 +9,30 @@ import SwiftUI
|
|||||||
|
|
||||||
struct ZapEvent: View {
|
struct ZapEvent: View {
|
||||||
let damus: DamusState
|
let damus: DamusState
|
||||||
let zap: Zap
|
let zap: Zapping
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
Text("⚡️ \(format_msats(zap.invoice.amount))", comment: "Text indicating the zap amount. i.e. number of satoshis that were tipped to a user")
|
Text("⚡️ \(format_msats(zap.amount))", comment: "Text indicating the zap amount. i.e. number of satoshis that were tipped to a user")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.padding([.top], 2)
|
.padding([.top], 2)
|
||||||
|
|
||||||
if zap.private_request != nil {
|
if zap.is_private {
|
||||||
Image(systemName: "lock.fill")
|
Image(systemName: "lock.fill")
|
||||||
.foregroundColor(DamusColors.green)
|
.foregroundColor(DamusColors.green)
|
||||||
.help(NSLocalizedString("Only you can see this message and who sent it.", comment: "Help text on green lock icon that explains that only the current user can see the message of a zap event and who sent the zap."))
|
.help(NSLocalizedString("Only you can see this message and who sent it.", comment: "Help text on green lock icon that explains that only the current user can see the message of a zap event and who sent the zap."))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if zap.is_pending {
|
||||||
|
Image(systemName: "clock.arrow.circlepath")
|
||||||
|
.foregroundColor(zap.is_paid ? Color.orange : DamusColors.yellow)
|
||||||
|
.help(NSLocalizedString("Only you can see this message and who sent it.", comment: "Help text on green lock icon that explains that only the current user can see the message of a zap event and who sent the zap."))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let priv = zap.private_request {
|
TextEvent(damus: damus, event: zap.request, pubkey: zap.request.pubkey, options: [.no_action_bar, .no_replying_to])
|
||||||
|
.padding([.top], 1)
|
||||||
TextEvent(damus: damus, event: priv, pubkey: priv.pubkey, options: [.no_action_bar, .no_replying_to])
|
|
||||||
.padding([.top], 1)
|
|
||||||
} else {
|
|
||||||
TextEvent(damus: damus, event: zap.request.ev, pubkey: zap.request.ev.pubkey, options: [.no_action_bar, .no_replying_to])
|
|
||||||
.padding([.top], 1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,12 +45,14 @@ let test_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper
|
|||||||
|
|
||||||
let test_private_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), request: test_zap_request, is_anon: false, private_request: test_event)
|
let test_private_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), request: test_zap_request, is_anon: false, private_request: test_event)
|
||||||
|
|
||||||
|
let test_pending_zap = PendingZap(amount_msat: 10000, target: .note(id: "id", author: "pk"), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice)))
|
||||||
|
|
||||||
struct ZapEvent_Previews: PreviewProvider {
|
struct ZapEvent_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack {
|
VStack {
|
||||||
ZapEvent(damus: test_damus_state(), zap: test_zap)
|
ZapEvent(damus: test_damus_state(), zap: .zap(test_zap))
|
||||||
|
|
||||||
ZapEvent(damus: test_damus_state(), zap: test_private_zap)
|
ZapEvent(damus: test_damus_state(), zap: .zap(test_private_zap))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
55
damus/Views/Launch.storyboard
Normal file
55
damus/Views/Launch.storyboard
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Y6W-OH-hqX">
|
||||||
|
<device id="retina6_12" orientation="portrait" appearance="light"/>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||||
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
|
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="s0d-6b-0kx">
|
||||||
|
<objects>
|
||||||
|
<viewController id="Y6W-OH-hqX" sceneMemberID="viewController">
|
||||||
|
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="gradient" translatesAutoresizingMaskIntoConstraints="NO" id="zoF-av-bOb">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
|
||||||
|
</imageView>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="damus-home" translatesAutoresizingMaskIntoConstraints="NO" id="LOu-EK-R9r">
|
||||||
|
<rect key="frame" x="153.66666666666666" y="383" width="86" height="86"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="86" id="KmA-28-Ngq"/>
|
||||||
|
<constraint firstAttribute="width" constant="86" id="ShD-nJ-gt9"/>
|
||||||
|
</constraints>
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
|
||||||
|
<color key="backgroundColor" systemColor="systemPurpleColor"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="LOu-EK-R9r" firstAttribute="centerY" secondItem="5EZ-qb-Rvc" secondAttribute="centerY" id="Y10-Wq-VOp"/>
|
||||||
|
<constraint firstItem="zoF-av-bOb" firstAttribute="top" secondItem="5EZ-qb-Rvc" secondAttribute="top" id="Y5l-Ax-ViU"/>
|
||||||
|
<constraint firstItem="zoF-av-bOb" firstAttribute="leading" secondItem="5EZ-qb-Rvc" secondAttribute="leading" id="bvq-6J-kYc"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="zoF-av-bOb" secondAttribute="bottom" id="dfj-BJ-nxB"/>
|
||||||
|
<constraint firstItem="LOu-EK-R9r" firstAttribute="centerX" secondItem="5EZ-qb-Rvc" secondAttribute="centerX" id="mtD-6Q-d3P"/>
|
||||||
|
<constraint firstAttribute="right" secondItem="zoF-av-bOb" secondAttribute="right" id="xQW-SS-8nb"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="-106.10687022900763" y="-29.577464788732396"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
<resources>
|
||||||
|
<image name="damus-home" width="43.333332061767578" height="43.333332061767578"/>
|
||||||
|
<image name="gradient" width="1125" height="2400"/>
|
||||||
|
<systemColor name="systemPurpleColor">
|
||||||
|
<color red="0.68627450980392157" green="0.32156862745098042" blue="0.87058823529411766" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
</systemColor>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
@@ -68,15 +68,11 @@ func event_group_author_name(profiles: Profiles, ind: Int, group: EventGroupType
|
|||||||
if let zapgrp = group.zap_group {
|
if let zapgrp = group.zap_group {
|
||||||
let zap = zapgrp.zaps[ind]
|
let zap = zapgrp.zaps[ind]
|
||||||
|
|
||||||
if let privzap = zap.private_request {
|
|
||||||
return event_author_name(profiles: profiles, pubkey: privzap.pubkey)
|
|
||||||
}
|
|
||||||
|
|
||||||
if zap.is_anon {
|
if zap.is_anon {
|
||||||
return NSLocalizedString("Anonymous", comment: "Placeholder author name of the anonymous person who zapped an event.")
|
return NSLocalizedString("Anonymous", comment: "Placeholder author name of the anonymous person who zapped an event.")
|
||||||
}
|
}
|
||||||
|
|
||||||
return event_author_name(profiles: profiles, pubkey: zap.request.ev.pubkey)
|
return event_author_name(profiles: profiles, pubkey: zap.request.pubkey)
|
||||||
} else {
|
} else {
|
||||||
let ev = group.events[ind]
|
let ev = group.events[ind]
|
||||||
return event_author_name(profiles: profiles, pubkey: ev.pubkey)
|
return event_author_name(profiles: profiles, pubkey: ev.pubkey)
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ struct PostView: View {
|
|||||||
@State var references: [ReferencedId] = []
|
@State var references: [ReferencedId] = []
|
||||||
@State var focusWordAttributes: (String?, NSRange?) = (nil, nil)
|
@State var focusWordAttributes: (String?, NSRange?) = (nil, nil)
|
||||||
@State var newCursorIndex: Int?
|
@State var newCursorIndex: Int?
|
||||||
|
@State var postTextViewCanScroll: Bool = true
|
||||||
|
|
||||||
@State var mediaToUpload: MediaUpload? = nil
|
@State var mediaToUpload: MediaUpload? = nil
|
||||||
|
|
||||||
@@ -203,7 +204,7 @@ struct PostView: View {
|
|||||||
|
|
||||||
var TextEntry: some View {
|
var TextEntry: some View {
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .topLeading) {
|
||||||
TextViewWrapper(attributedText: $post, cursorIndex: newCursorIndex, getFocusWordForMention: { word, range in
|
TextViewWrapper(attributedText: $post, postTextViewCanScroll: $postTextViewCanScroll, cursorIndex: newCursorIndex, getFocusWordForMention: { word, range in
|
||||||
focusWordAttributes = (word, range)
|
focusWordAttributes = (word, range)
|
||||||
self.newCursorIndex = nil
|
self.newCursorIndex = nil
|
||||||
})
|
})
|
||||||
@@ -335,7 +336,7 @@ struct PostView: View {
|
|||||||
|
|
||||||
// This if-block observes @ for tagging
|
// This if-block observes @ for tagging
|
||||||
if let searching {
|
if let searching {
|
||||||
UserSearch(damus_state: damus_state, search: searching, focusWordAttributes: $focusWordAttributes, newCursorIndex: $newCursorIndex, post: $post)
|
UserSearch(damus_state: damus_state, search: searching, focusWordAttributes: $focusWordAttributes, newCursorIndex: $newCursorIndex, postTextViewCanScroll: $postTextViewCanScroll, post: $post)
|
||||||
.frame(maxHeight: .infinity)
|
.frame(maxHeight: .infinity)
|
||||||
} else {
|
} else {
|
||||||
Divider()
|
Divider()
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ struct UserSearch: View {
|
|||||||
let search: String
|
let search: String
|
||||||
@Binding var focusWordAttributes: (String?, NSRange?)
|
@Binding var focusWordAttributes: (String?, NSRange?)
|
||||||
@Binding var newCursorIndex: Int?
|
@Binding var newCursorIndex: Int?
|
||||||
|
@Binding var postTextViewCanScroll: Bool
|
||||||
|
|
||||||
@Binding var post: NSMutableAttributedString
|
@Binding var post: NSMutableAttributedString
|
||||||
|
|
||||||
@@ -92,7 +93,14 @@ struct UserSearch: View {
|
|||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onAppear() {
|
||||||
|
postTextViewCanScroll = false
|
||||||
|
}
|
||||||
|
.onDisappear() {
|
||||||
|
postTextViewCanScroll = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct UserSearch_Previews: PreviewProvider {
|
struct UserSearch_Previews: PreviewProvider {
|
||||||
@@ -100,9 +108,10 @@ struct UserSearch_Previews: PreviewProvider {
|
|||||||
@State static var post: NSMutableAttributedString = NSMutableAttributedString(string: "some @jb55")
|
@State static var post: NSMutableAttributedString = NSMutableAttributedString(string: "some @jb55")
|
||||||
@State static var word: (String?, NSRange?) = (nil, nil)
|
@State static var word: (String?, NSRange?) = (nil, nil)
|
||||||
@State static var newCursorIndex: Int?
|
@State static var newCursorIndex: Int?
|
||||||
|
@State static var postTextViewCanScroll: Bool = false
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
UserSearch(damus_state: test_damus_state(), search: search, focusWordAttributes: $word, newCursorIndex: $newCursorIndex, post: $post)
|
UserSearch(damus_state: test_damus_state(), search: search, focusWordAttributes: $word, newCursorIndex: $newCursorIndex, postTextViewCanScroll: $postTextViewCanScroll, post: $post)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +149,7 @@ func search_users_for_autocomplete(profiles: Profiles, tags: [[String]], search
|
|||||||
}
|
}
|
||||||
|
|
||||||
// search profile cache as well
|
// search profile cache as well
|
||||||
for tup in profiles.profiles.enumerated() {
|
for tup in profiles.enumerated() {
|
||||||
let pk = tup.element.key
|
let pk = tup.element.key
|
||||||
let prof = tup.element.value.profile
|
let prof = tup.element.value.profile
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ struct EventProfileName: View {
|
|||||||
|
|
||||||
@State var display_name: DisplayName?
|
@State var display_name: DisplayName?
|
||||||
@State var nip05: NIP05?
|
@State var nip05: NIP05?
|
||||||
|
@State var donation: Int?
|
||||||
|
|
||||||
let size: EventViewKind
|
let size: EventViewKind
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ struct EventProfileName: View {
|
|||||||
self.pubkey = pubkey
|
self.pubkey = pubkey
|
||||||
self.profile = profile
|
self.profile = profile
|
||||||
self.size = size
|
self.size = size
|
||||||
|
self._donation = State(wrappedValue: profile?.damus_donation)
|
||||||
}
|
}
|
||||||
|
|
||||||
var friend_type: FriendType? {
|
var friend_type: FriendType? {
|
||||||
@@ -45,6 +47,15 @@ struct EventProfileName: View {
|
|||||||
return profile.reactions == false
|
return profile.reactions == false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var supporter: Int? {
|
||||||
|
guard let donation, donation > 0
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return donation
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 2) {
|
HStack(spacing: 2) {
|
||||||
switch current_display_name {
|
switch current_display_name {
|
||||||
@@ -73,6 +84,10 @@ struct EventProfileName: View {
|
|||||||
Image("zap-hashtag")
|
Image("zap-hashtag")
|
||||||
.frame(width: 14, height: 14)
|
.frame(width: 14, height: 14)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let supporter {
|
||||||
|
SupporterBadge(percent: supporter)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.profile_updated)) { notif in
|
.onReceive(handle_notify(.profile_updated)) { notif in
|
||||||
let update = notif.object as! ProfileUpdate
|
let update = notif.object as! ProfileUpdate
|
||||||
@@ -81,6 +96,7 @@ struct EventProfileName: View {
|
|||||||
}
|
}
|
||||||
display_name = Profile.displayName(profile: update.profile, pubkey: pubkey)
|
display_name = Profile.displayName(profile: update.profile, pubkey: pubkey)
|
||||||
nip05 = damus_state.profiles.is_validated(pubkey)
|
nip05 = damus_state.profiles.is_validated(pubkey)
|
||||||
|
donation = update.profile.damus_donation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ struct ProfileName: View {
|
|||||||
|
|
||||||
@State var display_name: DisplayName?
|
@State var display_name: DisplayName?
|
||||||
@State var nip05: NIP05?
|
@State var nip05: NIP05?
|
||||||
|
@State var donation: Int?
|
||||||
|
|
||||||
init(pubkey: String, profile: Profile?, damus: DamusState, show_nip5_domain: Bool = true) {
|
init(pubkey: String, profile: Profile?, damus: DamusState, show_nip5_domain: Bool = true) {
|
||||||
self.pubkey = pubkey
|
self.pubkey = pubkey
|
||||||
@@ -75,6 +76,17 @@ struct ProfileName: View {
|
|||||||
return profile.reactions == false
|
return profile.reactions == false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var supporter: Int? {
|
||||||
|
guard let profile,
|
||||||
|
let donation = profile.damus_donation,
|
||||||
|
donation > 0
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return donation
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 2) {
|
HStack(spacing: 2) {
|
||||||
Text(verbatim: "\(prefix)\(name_choice)")
|
Text(verbatim: "\(prefix)\(name_choice)")
|
||||||
@@ -90,6 +102,9 @@ struct ProfileName: View {
|
|||||||
Image("zap-hashtag")
|
Image("zap-hashtag")
|
||||||
.frame(width: 14, height: 14)
|
.frame(width: 14, height: 14)
|
||||||
}
|
}
|
||||||
|
if let supporter {
|
||||||
|
SupporterBadge(percent: supporter)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.profile_updated)) { notif in
|
.onReceive(handle_notify(.profile_updated)) { notif in
|
||||||
let update = notif.object as! ProfileUpdate
|
let update = notif.object as! ProfileUpdate
|
||||||
@@ -98,6 +113,7 @@ struct ProfileName: View {
|
|||||||
}
|
}
|
||||||
display_name = Profile.displayName(profile: update.profile, pubkey: pubkey)
|
display_name = Profile.displayName(profile: update.profile, pubkey: pubkey)
|
||||||
nip05 = damus_state.profiles.is_validated(pubkey)
|
nip05 = damus_state.profiles.is_validated(pubkey)
|
||||||
|
donation = profile?.damus_donation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ func get_profile_url(picture: String?, pubkey: String, profiles: Profiles) -> UR
|
|||||||
func make_preview_profiles(_ pubkey: String) -> Profiles {
|
func make_preview_profiles(_ pubkey: String) -> Profiles {
|
||||||
let profiles = Profiles()
|
let profiles = Profiles()
|
||||||
let picture = "http://cdn.jb55.com/img/red-me.jpg"
|
let picture = "http://cdn.jb55.com/img/red-me.jpg"
|
||||||
let profile = Profile(name: "jb55", display_name: "William Casarin", about: "It's me", picture: picture, banner: "", website: "https://jb55.com", lud06: nil, lud16: nil, nip05: "jb55.com")
|
let profile = Profile(name: "jb55", display_name: "William Casarin", about: "It's me", picture: picture, banner: "", website: "https://jb55.com", lud06: nil, lud16: nil, nip05: "jb55.com", damus_donation: nil)
|
||||||
let ts_profile = TimestampedProfile(profile: profile, timestamp: 0, event: test_event)
|
let ts_profile = TimestampedProfile(profile: profile, timestamp: 0, event: test_event)
|
||||||
profiles.add(id: pubkey, profile: ts_profile)
|
profiles.add(id: pubkey, profile: ts_profile)
|
||||||
return profiles
|
return profiles
|
||||||
|
|||||||
@@ -496,8 +496,11 @@ struct ProfileView_Previews: PreviewProvider {
|
|||||||
func test_damus_state() -> DamusState {
|
func test_damus_state() -> DamusState {
|
||||||
let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
|
let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
|
||||||
let damus = DamusState.empty
|
let damus = DamusState.empty
|
||||||
|
let settings = UserSettingsStore()
|
||||||
|
settings.donation_percent = 100
|
||||||
|
settings.default_zap_amount = 1971
|
||||||
|
|
||||||
let prof = Profile(name: "damus", display_name: "damus", about: "iOS app!", picture: "https://damus.io/img/logo.png", banner: "", website: "https://damus.io", lud06: nil, lud16: "jb55@sendsats.lol", nip05: "damus.io")
|
let prof = Profile(name: "damus", display_name: "damus", about: "iOS app!", picture: "https://damus.io/img/logo.png", banner: "", website: "https://damus.io", lud06: nil, lud16: "jb55@sendsats.lol", nip05: "damus.io", damus_donation: nil)
|
||||||
let tsprof = TimestampedProfile(profile: prof, timestamp: 0, event: test_event)
|
let tsprof = TimestampedProfile(profile: prof, timestamp: 0, event: test_event)
|
||||||
damus.profiles.add(id: pubkey, profile: tsprof)
|
damus.profiles.add(id: pubkey, profile: tsprof)
|
||||||
return damus
|
return damus
|
||||||
|
|||||||
@@ -12,10 +12,7 @@ struct ReactionView: View {
|
|||||||
let reaction: NostrEvent
|
let reaction: NostrEvent
|
||||||
|
|
||||||
var content: String {
|
var content: String {
|
||||||
if reaction.content == "" || reaction.content == "+" {
|
return to_reaction_emoji(ev: reaction) ?? ""
|
||||||
return "❤️"
|
|
||||||
}
|
|
||||||
return reaction.content
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|||||||
@@ -88,8 +88,8 @@ struct RelayConfigView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let info = RelayInfo.rw
|
let info = RelayInfo.rw
|
||||||
|
let descriptor = RelayDescriptor(url: url, info: info)
|
||||||
guard (try? state.pool.add_relay(url, info: info)) != nil else {
|
guard (try? state.pool.add_relay(descriptor)) != nil else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,11 @@ struct RelayDetailView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func FieldText(_ str: String?) -> some View {
|
func FieldText(_ str: String?) -> some View {
|
||||||
Text(str ?? "No data available")
|
if let s = str {
|
||||||
|
return Text(verbatim: s)
|
||||||
|
} else {
|
||||||
|
return Text("No data available", comment: "Text indicating that there is no data available to show for specific metadata about a relay server.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
|||||||
@@ -224,5 +224,5 @@ struct SaveKeysView_Previews: PreviewProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func create_account_to_metadata(_ model: CreateAccountModel) -> Profile {
|
func create_account_to_metadata(_ model: CreateAccountModel) -> Profile {
|
||||||
return Profile(name: model.nick_name, display_name: model.real_name, about: model.about, picture: model.profile_image, banner: nil, website: nil, lud06: nil, lud16: nil, nip05: nil)
|
return Profile(name: model.nick_name, display_name: model.real_name, about: model.about, picture: model.profile_image, banner: nil, website: nil, lud06: nil, lud16: nil, nip05: nil, damus_donation: nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ func make_hashtagable(_ str: String) -> String {
|
|||||||
|
|
||||||
func search_profiles(profiles: Profiles, search: String) -> [SearchedUser] {
|
func search_profiles(profiles: Profiles, search: String) -> [SearchedUser] {
|
||||||
let new = search.lowercased()
|
let new = search.lowercased()
|
||||||
return profiles.profiles.enumerated().reduce(into: []) { acc, els in
|
return profiles.enumerated().reduce(into: []) { acc, els in
|
||||||
let pk = els.element.key
|
let pk = els.element.key
|
||||||
let prof = els.element.value.profile
|
let prof = els.element.value.profile
|
||||||
|
|
||||||
|
|||||||
@@ -48,11 +48,17 @@ struct SideMenuView: View {
|
|||||||
navLabel(title: NSLocalizedString("Profile", comment: "Sidebar menu label for Profile view."), systemImage: "person")
|
navLabel(title: NSLocalizedString("Profile", comment: "Sidebar menu label for Profile view."), systemImage: "person")
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
NavigationLink(destination: WalletView(damus_state: damus_state, model: damus_state.wallet)) {
|
||||||
NavigationLink(destination: EmptyView()) {
|
HStack {
|
||||||
navLabel(title: NSLocalizedString("Wallet", comment: "Sidebar menu label for Wallet view."), systemImage: "bolt")
|
Image("wallet")
|
||||||
|
.tint(DamusColors.adaptableBlack)
|
||||||
|
|
||||||
|
Text(NSLocalizedString("Wallet", comment: "Sidebar menu label for Wallet view."))
|
||||||
|
.font(.title2)
|
||||||
|
.foregroundColor(textColor())
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
NavigationLink(destination: MutelistView(damus_state: damus_state, users: get_mutelist_users(damus_state.contacts.mutelist) )) {
|
NavigationLink(destination: MutelistView(damus_state: damus_state, users: get_mutelist_users(damus_state.contacts.mutelist) )) {
|
||||||
navLabel(title: NSLocalizedString("Muted", comment: "Sidebar menu label for muted users view."), systemImage: "exclamationmark.octagon")
|
navLabel(title: NSLocalizedString("Muted", comment: "Sidebar menu label for muted users view."), systemImage: "exclamationmark.octagon")
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ import SwiftUI
|
|||||||
|
|
||||||
struct TextViewWrapper: UIViewRepresentable {
|
struct TextViewWrapper: UIViewRepresentable {
|
||||||
@Binding var attributedText: NSMutableAttributedString
|
@Binding var attributedText: NSMutableAttributedString
|
||||||
|
@Binding var postTextViewCanScroll: Bool
|
||||||
let cursorIndex: Int?
|
let cursorIndex: Int?
|
||||||
var getFocusWordForMention: ((String?, NSRange?) -> Void)? = nil
|
var getFocusWordForMention: ((String?, NSRange?) -> Void)? = nil
|
||||||
|
|
||||||
func makeUIView(context: Context) -> UITextView {
|
func makeUIView(context: Context) -> UITextView {
|
||||||
let textView = UITextView()
|
let textView = UITextView()
|
||||||
textView.delegate = context.coordinator
|
textView.delegate = context.coordinator
|
||||||
|
textView.isScrollEnabled = postTextViewCanScroll
|
||||||
textView.showsVerticalScrollIndicator = false
|
textView.showsVerticalScrollIndicator = false
|
||||||
TextViewWrapper.setTextProperties(textView)
|
TextViewWrapper.setTextProperties(textView)
|
||||||
return textView
|
return textView
|
||||||
@@ -29,6 +31,7 @@ struct TextViewWrapper: UIViewRepresentable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateUIView(_ uiView: UITextView, context: Context) {
|
func updateUIView(_ uiView: UITextView, context: Context) {
|
||||||
|
uiView.isScrollEnabled = postTextViewCanScroll
|
||||||
uiView.attributedText = attributedText
|
uiView.attributedText = attributedText
|
||||||
TextViewWrapper.setTextProperties(uiView)
|
TextViewWrapper.setTextProperties(uiView)
|
||||||
setCursorPosition(textView: uiView)
|
setCursorPosition(textView: uiView)
|
||||||
|
|||||||
104
damus/Views/Wallet/ConnectWalletView.swift
Normal file
104
damus/Views/Wallet/ConnectWalletView.swift
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
//
|
||||||
|
// ConnectWalletView.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-05-05.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ConnectWalletView: View {
|
||||||
|
@Environment(\.openURL) private var openURL
|
||||||
|
@ObservedObject var model: WalletModel
|
||||||
|
|
||||||
|
@State var scanning: Bool = false
|
||||||
|
@State var error: String? = nil
|
||||||
|
@State var wallet_scan_result: WalletScanResult = .scanning
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
MainContent
|
||||||
|
.navigationTitle(NSLocalizedString("Attach a Wallet", comment: "Navigation title for attaching Nostr Wallet Connect lightning wallet."))
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
.padding()
|
||||||
|
.onChange(of: wallet_scan_result) { res in
|
||||||
|
scanning = false
|
||||||
|
|
||||||
|
switch res {
|
||||||
|
case .success(let url):
|
||||||
|
error = nil
|
||||||
|
self.model.new(url)
|
||||||
|
|
||||||
|
case .failed:
|
||||||
|
error = "Invalid nostr wallet connection string"
|
||||||
|
|
||||||
|
case .scanning:
|
||||||
|
error = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AreYouSure(nwc: WalletConnectURL) -> some View {
|
||||||
|
VStack {
|
||||||
|
Text("Are you sure you want to attach this wallet?", comment: "Prompt to ask user if they want to attach their Nostr Wallet Connect lightning wallet.")
|
||||||
|
.font(.title)
|
||||||
|
|
||||||
|
Text(nwc.relay.id)
|
||||||
|
.font(.body)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
|
||||||
|
if let lud16 = nwc.lud16 {
|
||||||
|
Text(lud16)
|
||||||
|
.font(.body)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
|
||||||
|
BigButton(NSLocalizedString("Attach", comment: "Text for button to attach Nostr Wallet Connect lightning wallet.")) {
|
||||||
|
model.connect(nwc)
|
||||||
|
}
|
||||||
|
|
||||||
|
BigButton(NSLocalizedString("Cancel", comment: "Text for button to cancel out of connecting Nostr Wallet Connect lightning ewallet.")) {
|
||||||
|
model.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ConnectWallet: some View {
|
||||||
|
VStack {
|
||||||
|
NavigationLink(destination: WalletScannerView(result: $wallet_scan_result), isActive: $scanning) {
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
AlbyButton() {
|
||||||
|
openURL(URL(string:"https://nwc.getalby.com/apps/new?c=Damus")!)
|
||||||
|
}
|
||||||
|
|
||||||
|
BigButton(NSLocalizedString("Attach Wallet", comment: "Text for button to attach Nostr Wallet Connect lightning wallet.")) {
|
||||||
|
scanning = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if let err = self.error {
|
||||||
|
Text(err)
|
||||||
|
.foregroundColor(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var MainContent: some View {
|
||||||
|
Group {
|
||||||
|
switch model.connect_state {
|
||||||
|
case .new(let nwc):
|
||||||
|
AreYouSure(nwc: nwc)
|
||||||
|
case .existing:
|
||||||
|
Text(verbatim: "Shouldn't happen")
|
||||||
|
case .none:
|
||||||
|
ConnectWallet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ConnectWalletView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
ConnectWalletView(model: WalletModel(settings: UserSettingsStore()))
|
||||||
|
}
|
||||||
|
}
|
||||||
77
damus/Views/Wallet/NWCScannerView.swift
Normal file
77
damus/Views/Wallet/NWCScannerView.swift
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
//
|
||||||
|
// QRScannerView.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-05-09.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
enum WalletScanResult: Equatable {
|
||||||
|
static func == (lhs: WalletScanResult, rhs: WalletScanResult) -> Bool {
|
||||||
|
switch lhs {
|
||||||
|
case .success(let a):
|
||||||
|
switch rhs {
|
||||||
|
case .success(let b):
|
||||||
|
return a == b
|
||||||
|
case .failed:
|
||||||
|
return false
|
||||||
|
case .scanning:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case .failed:
|
||||||
|
switch rhs {
|
||||||
|
case .success:
|
||||||
|
return false
|
||||||
|
case .failed:
|
||||||
|
return true
|
||||||
|
case .scanning:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case .scanning:
|
||||||
|
switch rhs {
|
||||||
|
case .success:
|
||||||
|
return false
|
||||||
|
case .failed:
|
||||||
|
return false
|
||||||
|
case .scanning:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case success(WalletConnectURL)
|
||||||
|
case failed
|
||||||
|
case scanning
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WalletScannerView: View {
|
||||||
|
@Binding var result: WalletScanResult
|
||||||
|
|
||||||
|
@Environment(\.dismiss) var dismiss
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
CodeScannerView(codeTypes: [.qr]) { res in
|
||||||
|
switch res {
|
||||||
|
case .success(let success):
|
||||||
|
guard let url = WalletConnectURL(str: success.string) else {
|
||||||
|
result = .failed
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result = .success(url)
|
||||||
|
case .failure:
|
||||||
|
result = .failed
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct QRScannerView_Previews: PreviewProvider {
|
||||||
|
@State static var result: WalletScanResult = .scanning
|
||||||
|
static var previews: some View {
|
||||||
|
WalletScannerView(result: $result)
|
||||||
|
}
|
||||||
|
}
|
||||||
198
damus/Views/Wallet/WalletView.swift
Normal file
198
damus/Views/Wallet/WalletView.swift
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
//
|
||||||
|
// WalletView.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-05-05.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct WalletView: View {
|
||||||
|
let damus_state: DamusState
|
||||||
|
@ObservedObject var model: WalletModel
|
||||||
|
@ObservedObject var settings: UserSettingsStore
|
||||||
|
|
||||||
|
init(damus_state: DamusState, model: WalletModel? = nil) {
|
||||||
|
self.damus_state = damus_state
|
||||||
|
self._model = ObservedObject(wrappedValue: model ?? damus_state.wallet)
|
||||||
|
self._settings = ObservedObject(wrappedValue: damus_state.settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MainWalletView(nwc: WalletConnectURL) -> some View {
|
||||||
|
VStack {
|
||||||
|
SupportDamus
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text(verbatim: nwc.relay.id)
|
||||||
|
|
||||||
|
if let lud16 = nwc.lud16 {
|
||||||
|
Text(verbatim: lud16)
|
||||||
|
}
|
||||||
|
|
||||||
|
BigButton(NSLocalizedString("Disconnect Wallet", comment: "Text for button to disconnect from Nostr Wallet Connect lightning wallet.")) {
|
||||||
|
self.model.disconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.navigationTitle(NSLocalizedString("Wallet", comment: "Navigation title for Wallet view"))
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
|
||||||
|
func donation_binding() -> Binding<Double> {
|
||||||
|
return Binding(get: {
|
||||||
|
return Double(model.settings.donation_percent)
|
||||||
|
}, set: { v in
|
||||||
|
model.settings.donation_percent = Int(v)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static let min_donation: Double = 0.0
|
||||||
|
static let max_donation: Double = 100.0
|
||||||
|
|
||||||
|
var percent: Double {
|
||||||
|
Double(model.settings.donation_percent) / 100.0
|
||||||
|
}
|
||||||
|
|
||||||
|
var tip_msats: String {
|
||||||
|
let msats = Int64(percent * Double(model.settings.default_zap_amount * 1000))
|
||||||
|
let s = format_msats_abbrev(msats)
|
||||||
|
// TODO: fix formatting and remove this hack
|
||||||
|
let parts = s.split(separator: ".")
|
||||||
|
if parts.count == 1 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if let end = parts[safe: 1] {
|
||||||
|
if end.allSatisfy({ c in c.isNumber }) {
|
||||||
|
return String(parts[0])
|
||||||
|
} else {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
var SupportDamus: some View {
|
||||||
|
ZStack(alignment: .topLeading) {
|
||||||
|
RoundedRectangle(cornerRadius: 20)
|
||||||
|
.fill(DamusGradient.gradient.opacity(0.5))
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 20) {
|
||||||
|
HStack {
|
||||||
|
Image("logo-nobg")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 50, height: 50)
|
||||||
|
Text("Support Damus", comment: "Text calling for the user to support Damus through zaps")
|
||||||
|
.font(.title.bold())
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("Help build the future of decentralized communication on the web.", comment: "Text indicating the goal of developing Damus which the user can help with.")
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
|
||||||
|
Text("An additional percentage of each zap will be sent to support Damus development", comment: "Text indicating that they can contribute zaps to support Damus development.")
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
|
||||||
|
let binding = donation_binding()
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Slider(value: binding,
|
||||||
|
in: WalletView.min_donation...WalletView.max_donation,
|
||||||
|
label: { })
|
||||||
|
Text("\(Int(binding.wrappedValue))%", comment: "Percentage of additional zap that should be sent to support Damus development.")
|
||||||
|
.font(.title.bold())
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.frame(width: 80)
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack{
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Text("\(Image("zap.fill")) \(format_msats_abbrev(Int64(model.settings.default_zap_amount) * 1000))")
|
||||||
|
.font(.title)
|
||||||
|
.foregroundColor(percent == 0 ? .gray : .yellow)
|
||||||
|
.frame(width: 120)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text("Zap", comment: "Text underneath the number of sats indicating that it's the amount used for zaps.")
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text(verbatim: "+")
|
||||||
|
.font(.title)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
Text("\(Image("zap.fill")) \(tip_msats)")
|
||||||
|
.font(.title)
|
||||||
|
.foregroundColor(percent == 0 ? .gray : Color.yellow)
|
||||||
|
.frame(width: 120)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(verbatim: percent == 0 ? "🩶" : "💜")
|
||||||
|
.foregroundColor(.white)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
EventProfile(damus_state: damus_state, pubkey: damus_state.pubkey, profile: damus_state.profiles.lookup(id: damus_state.pubkey), size: .small)
|
||||||
|
}
|
||||||
|
.padding(25)
|
||||||
|
}
|
||||||
|
.frame(height: 370)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
switch model.connect_state {
|
||||||
|
case .new:
|
||||||
|
ConnectWalletView(model: model)
|
||||||
|
case .none:
|
||||||
|
ConnectWalletView(model: model)
|
||||||
|
case .existing(let nwc):
|
||||||
|
MainWalletView(nwc: nwc)
|
||||||
|
.onAppear() {
|
||||||
|
model.inital_percent = settings.donation_percent
|
||||||
|
}
|
||||||
|
.onChange(of: settings.donation_percent) { p in
|
||||||
|
guard let profile = damus_state.profiles.lookup(id: damus_state.pubkey) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
profile.damus_donation = p
|
||||||
|
|
||||||
|
notify(.profile_updated, ProfileUpdate(pubkey: damus_state.pubkey, profile: profile))
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
guard let keypair = damus_state.keypair.to_full(),
|
||||||
|
let profile = damus_state.profiles.lookup(id: damus_state.pubkey),
|
||||||
|
model.inital_percent != profile.damus_donation
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
profile.damus_donation = settings.donation_percent
|
||||||
|
let meta = make_metadata_event(keypair: keypair, metadata: profile)
|
||||||
|
let tsprofile = TimestampedProfile(profile: profile, timestamp: meta.created_at, event: meta)
|
||||||
|
damus_state.profiles.add(id: damus_state.pubkey, profile: tsprofile)
|
||||||
|
damus_state.postbox.send(meta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let test_wallet_connect_url = WalletConnectURL(pubkey: "pk", relay: .init("wss://relay.damus.io")!, keypair: test_damus_state().keypair.to_full()!, lud16: "jb55@sendsats.com")
|
||||||
|
|
||||||
|
struct WalletView_Previews: PreviewProvider {
|
||||||
|
static let tds = test_damus_state()
|
||||||
|
static var previews: some View {
|
||||||
|
WalletView(damus_state: tds, model: WalletModel(state: .existing(test_wallet_connect_url), settings: tds.settings))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -136,7 +136,7 @@ struct CustomizeZapView: View {
|
|||||||
VStack(alignment: .center, spacing: 0) {
|
VStack(alignment: .center, spacing: 0) {
|
||||||
TextField("", text: $custom_amount)
|
TextField("", text: $custom_amount)
|
||||||
.placeholder(when: custom_amount.isEmpty, alignment: .center) {
|
.placeholder(when: custom_amount.isEmpty, alignment: .center) {
|
||||||
Text(String("0"))
|
Text(verbatim: 0.formatted())
|
||||||
}
|
}
|
||||||
.accentColor(.clear)
|
.accentColor(.clear)
|
||||||
.font(.system(size: 72, weight: .heavy))
|
.font(.system(size: 72, weight: .heavy))
|
||||||
|
|||||||
@@ -9,17 +9,20 @@ import SwiftUI
|
|||||||
|
|
||||||
struct ZapsView: View {
|
struct ZapsView: View {
|
||||||
let state: DamusState
|
let state: DamusState
|
||||||
@StateObject var model: ZapsModel
|
var model: ZapsModel
|
||||||
|
|
||||||
|
@ObservedObject var zaps: ZapsDataModel
|
||||||
|
|
||||||
init(state: DamusState, target: ZapTarget) {
|
init(state: DamusState, target: ZapTarget) {
|
||||||
self.state = state
|
self.state = state
|
||||||
self._model = StateObject(wrappedValue: ZapsModel(state: state, target: target))
|
self.model = ZapsModel(state: state, target: target)
|
||||||
|
self._zaps = ObservedObject(wrappedValue: state.events.get_cache_data(target.id).zaps_model)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack {
|
LazyVStack {
|
||||||
ForEach(model.zaps, id: \.event.id) { zap in
|
ForEach(zaps.zaps, id: \.request.id) { zap in
|
||||||
ZapEvent(damus: state, zap: zap)
|
ZapEvent(damus: state, zap: zap)
|
||||||
.padding([.horizontal])
|
.padding([.horizontal])
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -42,11 +42,6 @@
|
|||||||
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3" build-num="14E222b"/>
|
<tool tool-id="com.apple.dt.xcode" tool-name="Xcode" tool-version="14.3" build-num="14E222b"/>
|
||||||
</header>
|
</header>
|
||||||
<body>
|
<body>
|
||||||
<trans-unit id="%@" xml:space="preserve">
|
|
||||||
<source>%@</source>
|
|
||||||
<target>%@</target>
|
|
||||||
<note>No comment provided by engineer.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="%@ %@" xml:space="preserve">
|
<trans-unit id="%@ %@" xml:space="preserve">
|
||||||
<source>%@ %@</source>
|
<source>%@ %@</source>
|
||||||
<target>%@ %@</target>
|
<target>%@ %@</target>
|
||||||
@@ -83,6 +78,11 @@ Sentence composed of 2 variables to describe how many people are following a use
|
|||||||
<target>%@. Tip your friend's posts and stack sats with Bitcoin⚡️, the native currency of the internet.</target>
|
<target>%@. Tip your friend's posts and stack sats with Bitcoin⚡️, the native currency of the internet.</target>
|
||||||
<note>Explanation of what can be done by users to earn money. There is a heading that precedes this explanation which is a variable to this string.</note>
|
<note>Explanation of what can be done by users to earn money. There is a heading that precedes this explanation which is a variable to this string.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="%lld%%" xml:space="preserve">
|
||||||
|
<source>%lld%%</source>
|
||||||
|
<target>%lld%%</target>
|
||||||
|
<note>Percentage of additional zap that should be sent to support Damus development.</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="%lld/%lld" xml:space="preserve">
|
<trans-unit id="%lld/%lld" xml:space="preserve">
|
||||||
<source>%lld/%lld</source>
|
<source>%lld/%lld</source>
|
||||||
<target>%lld/%lld</target>
|
<target>%lld/%lld</target>
|
||||||
@@ -174,6 +174,11 @@ Sentence composed of 2 variables to describe how many people are following a use
|
|||||||
<target>Always show images</target>
|
<target>Always show images</target>
|
||||||
<note>Setting to always show and never blur images</note>
|
<note>Setting to always show and never blur images</note>
|
||||||
</trans-unit>
|
</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>
|
||||||
|
<note>Text indicating that they can contribute zaps to support Damus development.</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Animations" xml:space="preserve">
|
<trans-unit id="Animations" xml:space="preserve">
|
||||||
<source>Animations</source>
|
<source>Animations</source>
|
||||||
<target>Animations</target>
|
<target>Animations</target>
|
||||||
@@ -201,6 +206,11 @@ Sentence composed of 2 variables to describe how many people are following a use
|
|||||||
<target>Are you lost?</target>
|
<target>Are you lost?</target>
|
||||||
<note>Text asking the user if they are lost in the app.</note>
|
<note>Text asking the user if they are lost in the app.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Are you sure you want to attach this wallet?" xml:space="preserve">
|
||||||
|
<source>Are you sure you want to attach this wallet?</source>
|
||||||
|
<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 delete all of your bookmarks?" xml:space="preserve">
|
<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>
|
<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>
|
<target>Are you sure you want to delete all of your bookmarks?</target>
|
||||||
@@ -216,6 +226,26 @@ Sentence composed of 2 variables to describe how many people are following a use
|
|||||||
<target>Are you sure you want to upload this media?</target>
|
<target>Are you sure you want to upload this media?</target>
|
||||||
<note>Alert message asking if the user wants to upload media.</note>
|
<note>Alert message asking if the user wants to upload media.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Attach" xml:space="preserve">
|
||||||
|
<source>Attach</source>
|
||||||
|
<target>Attach</target>
|
||||||
|
<note>Text for button to attach Nostr Wallet Connect lightning wallet.</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Attach Alby Wallet" xml:space="preserve">
|
||||||
|
<source>Attach Alby Wallet</source>
|
||||||
|
<target>Attach Alby Wallet</target>
|
||||||
|
<note>Button to attach an Alby Wallet, a service that provides a Lightning wallet for zapping sats. Alby is the name of the service and should not be translated.</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Attach Wallet" xml:space="preserve">
|
||||||
|
<source>Attach Wallet</source>
|
||||||
|
<target>Attach Wallet</target>
|
||||||
|
<note>Text for button to attach Nostr Wallet Connect lightning wallet.</note>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="Attach a Wallet" xml:space="preserve">
|
||||||
|
<source>Attach a Wallet</source>
|
||||||
|
<target>Attach a Wallet</target>
|
||||||
|
<note>Navigation title for attaching Nostr Wallet Connect lightning wallet.</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Automatically translate notes" xml:space="preserve">
|
<trans-unit id="Automatically translate notes" xml:space="preserve">
|
||||||
<source>Automatically translate notes</source>
|
<source>Automatically translate notes</source>
|
||||||
<target>Automatically translate notes</target>
|
<target>Automatically translate notes</target>
|
||||||
@@ -264,7 +294,8 @@ Sentence composed of 2 variables to describe how many people are following a use
|
|||||||
Button to cancel the upload.
|
Button to cancel the upload.
|
||||||
Cancel deleting bookmarks.
|
Cancel deleting bookmarks.
|
||||||
Cancel deleting the user.
|
Cancel deleting the user.
|
||||||
Cancel out of logging out the user.</note>
|
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>
|
||||||
<trans-unit id="Choose from Library" xml:space="preserve">
|
<trans-unit id="Choose from Library" xml:space="preserve">
|
||||||
<source>Choose from Library</source>
|
<source>Choose from Library</source>
|
||||||
@@ -471,10 +502,10 @@ Sentence composed of 2 variables to describe how many people are following a use
|
|||||||
<target>Disconnect From Relay</target>
|
<target>Disconnect From Relay</target>
|
||||||
<note>Button to disconnect from the relay.</note>
|
<note>Button to disconnect from the relay.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Dismiss" xml:space="preserve">
|
<trans-unit id="Disconnect Wallet" xml:space="preserve">
|
||||||
<source>Dismiss</source>
|
<source>Disconnect Wallet</source>
|
||||||
<target>Dismiss</target>
|
<target>Disconnect Wallet</target>
|
||||||
<note>Button to dismiss a text field alert.</note>
|
<note>Text for button to disconnect from Nostr Wallet Connect lightning wallet.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Display Name" xml:space="preserve">
|
<trans-unit id="Display Name" xml:space="preserve">
|
||||||
<source>Display Name</source>
|
<source>Display Name</source>
|
||||||
@@ -581,6 +612,11 @@ Sentence composed of 2 variables to describe how many people are following a use
|
|||||||
<target>Get API Key with BTC/Lightning</target>
|
<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.</note>
|
||||||
</trans-unit>
|
</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>
|
||||||
|
<note>Text indicating the goal of developing Damus which the user can help with.</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Hide" xml:space="preserve">
|
<trans-unit id="Hide" xml:space="preserve">
|
||||||
<source>Hide</source>
|
<source>Hide</source>
|
||||||
<target>Hide</target>
|
<target>Hide</target>
|
||||||
@@ -777,6 +813,11 @@ Sentence composed of 2 variables to describe how many people are following a use
|
|||||||
<target>No</target>
|
<target>No</target>
|
||||||
<note>Button to cancel out of posting a note after being alerted that it looks like they might be posting a private key.</note>
|
<note>Button to cancel out of posting a note after being alerted that it looks like they might be posting a private key.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="No data available" xml:space="preserve">
|
||||||
|
<source>No data available</source>
|
||||||
|
<target>No data available</target>
|
||||||
|
<note>Text indicating that there is no data available to show for specific metadata about a relay server.</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="No mute list found, create a new one? This will overwrite any previous mute lists." xml:space="preserve">
|
<trans-unit id="No mute list found, create a new one? This will overwrite any previous mute lists." xml:space="preserve">
|
||||||
<source>No mute list found, create a new one? This will overwrite any previous mute lists.</source>
|
<source>No mute list found, create a new one? This will overwrite any previous mute lists.</source>
|
||||||
<target>No mute list found, create a new one? This will overwrite any previous mute lists.</target>
|
<target>No mute list found, create a new one? This will overwrite any previous mute lists.</target>
|
||||||
@@ -995,8 +1036,7 @@ Button text to indicate that the zap type is a private zap.</note>
|
|||||||
<trans-unit id="Relay" xml:space="preserve">
|
<trans-unit id="Relay" xml:space="preserve">
|
||||||
<source>Relay</source>
|
<source>Relay</source>
|
||||||
<target>Relay</target>
|
<target>Relay</target>
|
||||||
<note>Label to display relay address.
|
<note>Label to display relay address.</note>
|
||||||
Text field for relay server. Used for testing purposes.</note>
|
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Relays" xml:space="preserve">
|
<trans-unit id="Relays" xml:space="preserve">
|
||||||
<source>Relays</source>
|
<source>Relays</source>
|
||||||
@@ -1067,11 +1107,6 @@ Button text to indicate that the zap type is a private zap.</note>
|
|||||||
<target>Repost</target>
|
<target>Repost</target>
|
||||||
<note>Button to repost a note</note>
|
<note>Button to repost a note</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Repost Note" xml:space="preserve">
|
|
||||||
<source>Repost Note</source>
|
|
||||||
<target>Repost Note</target>
|
|
||||||
<note>Title text to indicate that the buttons below are meant to be used to repost a note to others.</note>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="Reposted" xml:space="preserve">
|
<trans-unit id="Reposted" xml:space="preserve">
|
||||||
<source>Reposted</source>
|
<source>Reposted</source>
|
||||||
<target>Reposted</target>
|
<target>Reposted</target>
|
||||||
@@ -1232,6 +1267,11 @@ Button text to indicate that the zap type is a private zap.</note>
|
|||||||
<target>Software</target>
|
<target>Software</target>
|
||||||
<note>Label to display relay software.</note>
|
<note>Label to display relay software.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Support Damus" xml:space="preserve">
|
||||||
|
<source>Support Damus</source>
|
||||||
|
<target>Support Damus</target>
|
||||||
|
<note>Text calling for the user to support Damus through zaps</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Supported NIPs" xml:space="preserve">
|
<trans-unit id="Supported NIPs" xml:space="preserve">
|
||||||
<source>Supported NIPs</source>
|
<source>Supported NIPs</source>
|
||||||
<target>Supported NIPs</target>
|
<target>Supported NIPs</target>
|
||||||
@@ -1358,6 +1398,11 @@ Button text to indicate that the zap type is a private zap.</note>
|
|||||||
<target>Universe 🛸</target>
|
<target>Universe 🛸</target>
|
||||||
<note>Toolbar label for the universal view where posts from all connected relay servers appear.</note>
|
<note>Toolbar label for the universal view where posts from all connected relay servers appear.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="Unmute" xml:space="preserve">
|
||||||
|
<source>Unmute</source>
|
||||||
|
<target>Unmute</target>
|
||||||
|
<note>Button to unmute a profile.</note>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="Unmute conversation" xml:space="preserve">
|
<trans-unit id="Unmute conversation" xml:space="preserve">
|
||||||
<source>Unmute conversation</source>
|
<source>Unmute conversation</source>
|
||||||
<target>Unmute conversation</target>
|
<target>Unmute conversation</target>
|
||||||
@@ -1415,7 +1460,8 @@ YOU WILL NO LONGER BE ABLE TO LOG INTO DAMUS USING THIS ACCOUNT KEY.
|
|||||||
<trans-unit id="Wallet" xml:space="preserve">
|
<trans-unit id="Wallet" xml:space="preserve">
|
||||||
<source>Wallet</source>
|
<source>Wallet</source>
|
||||||
<target>Wallet</target>
|
<target>Wallet</target>
|
||||||
<note>Sidebar menu label for Wallet view.
|
<note>Navigation title for Wallet view
|
||||||
|
Sidebar menu label for Wallet view.
|
||||||
Title for section in zap settings that controls the Lightning wallet selection.</note>
|
Title for section in zap settings that controls the Lightning wallet selection.</note>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="Website" xml:space="preserve">
|
<trans-unit id="Website" xml:space="preserve">
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -236,6 +236,24 @@
|
|||||||
<string>Republicaciones</string>
|
<string>Republicaciones</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>sats</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
<string>%#@SATS@</string>
|
||||||
|
<key>SATS</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringFormatSpecTypeKey</key>
|
||||||
|
<string>NSStringPluralRuleType</string>
|
||||||
|
<key>NSStringFormatValueTypeKey</key>
|
||||||
|
<string>d</string>
|
||||||
|
<key>one</key>
|
||||||
|
<string>sat</string>
|
||||||
|
<key>many</key>
|
||||||
|
<string>sats</string>
|
||||||
|
<key>other</key>
|
||||||
|
<string>sats</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
<key>sats_count</key>
|
<key>sats_count</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSStringLocalizedFormatKey</key>
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -15,7 +15,7 @@
|
|||||||
<key>one</key>
|
<key>one</key>
|
||||||
<string>... %d یادداشت دیگر ...</string>
|
<string>... %d یادداشت دیگر ...</string>
|
||||||
<key>other</key>
|
<key>other</key>
|
||||||
<string>... %d یادداشتهای دیگر ...</string>
|
<string>... %d یادداشت های دیگر ...</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>followers_count</key>
|
<key>followers_count</key>
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
<key>one</key>
|
<key>one</key>
|
||||||
<string>%2$@ و %1$d نفر دیگر به یک مطلب که شما در آن تگ شدهاید بازخورد دادهاند</string>
|
<string>%2$@ و %1$d نفر دیگر به یک مطلب که شما در آن تگ شدهاید بازخورد دادهاند</string>
|
||||||
<key>other</key>
|
<key>other</key>
|
||||||
<string>%2$@ و %1$d نفر دیگر به یک مطلب که شما در آن تگ شدهاید بازخورد دادهاند</string>
|
<string>%2$@ و %1$d نفر دیگر به یک یادداشت که شما در آن تگ شدهاید واکنش دادهاند</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>reacted_your_post_3</key>
|
<key>reacted_your_post_3</key>
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
<key>one</key>
|
<key>one</key>
|
||||||
<string>%2$@ و %1$d نفر دیگر به مطلب شما بازخورد دادهاند</string>
|
<string>%2$@ و %1$d نفر دیگر به مطلب شما بازخورد دادهاند</string>
|
||||||
<key>other</key>
|
<key>other</key>
|
||||||
<string>%2$@ و %1$d نفر دیگر به مطلب شما بازخورد دادهاند</string>
|
<string>%2$@ و %1$d نفر دیگر به یادداشت شما واکنش دادهاند</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>reacted_your_profile_3</key>
|
<key>reacted_your_profile_3</key>
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
<key>one</key>
|
<key>one</key>
|
||||||
<string>%2$@ و %1$d نفر دیگر به نمایهی شما بازخورد دادهاند</string>
|
<string>%2$@ و %1$d نفر دیگر به نمایهی شما بازخورد دادهاند</string>
|
||||||
<key>other</key>
|
<key>other</key>
|
||||||
<string>%2$@ و %1$d نفر دیگر به نمایهی شما بازخورد دادهاند</string>
|
<string>%2$@ و %1$d نفر دیگر به نمایه شما واکنش دادهاند</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>reactions_count</key>
|
<key>reactions_count</key>
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
<key>one</key>
|
<key>one</key>
|
||||||
<string>بازخورد</string>
|
<string>بازخورد</string>
|
||||||
<key>other</key>
|
<key>other</key>
|
||||||
<string>بازخوردها</string>
|
<string>واکنش ها</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>relays_count</key>
|
<key>relays_count</key>
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
<key>one</key>
|
<key>one</key>
|
||||||
<string>%2$@ و %1$d نفر دیگر یک مطلب که شما در آن تگ شدهاید را بازنشر کردهاند</string>
|
<string>%2$@ و %1$d نفر دیگر یک مطلب که شما در آن تگ شدهاید را بازنشر کردهاند</string>
|
||||||
<key>other</key>
|
<key>other</key>
|
||||||
<string>%2$@ و %1$d نفر دیگر یک مطلب که شما در آن تگ شدهاید را بازنشر کردهاند</string>
|
<string>%2$@ و %1$d نفر دیگر یک یادداشت که شما در آن تگ شدهاید را بازنشر کردهاند</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>reposted_your_post_3</key>
|
<key>reposted_your_post_3</key>
|
||||||
@@ -175,7 +175,7 @@
|
|||||||
<key>one</key>
|
<key>one</key>
|
||||||
<string>%2$@ و %1$d نفر دیگر مطلب شما را بازنشر کردهاند</string>
|
<string>%2$@ و %1$d نفر دیگر مطلب شما را بازنشر کردهاند</string>
|
||||||
<key>other</key>
|
<key>other</key>
|
||||||
<string>%2$@ و %1$d نفر دیگر مطلب شما را بازنشر کردهاند</string>
|
<string>%2$@ و %1$d نفر دیگر یادداشت شما را بازنشر کردهاند</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>reposted_your_profile_3</key>
|
<key>reposted_your_profile_3</key>
|
||||||
@@ -210,6 +210,22 @@
|
|||||||
<string>بازنشرها</string>
|
<string>بازنشرها</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>sats</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
<string>%#@SATS@</string>
|
||||||
|
<key>SATS</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringFormatSpecTypeKey</key>
|
||||||
|
<string>NSStringPluralRuleType</string>
|
||||||
|
<key>NSStringFormatValueTypeKey</key>
|
||||||
|
<string>d</string>
|
||||||
|
<key>one</key>
|
||||||
|
<string>ساتوشی</string>
|
||||||
|
<key>other</key>
|
||||||
|
<string>ساتوشی</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
<key>sats_count</key>
|
<key>sats_count</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSStringLocalizedFormatKey</key>
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
@@ -226,6 +242,38 @@
|
|||||||
<string>%2$@ ساتوشی</string>
|
<string>%2$@ ساتوشی</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>zap_notification_no_message</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
<string>%1$#@NOTIFICATION@</string>
|
||||||
|
<key>NOTIFICATION</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringFormatSpecTypeKey</key>
|
||||||
|
<string>NSStringPluralRuleType</string>
|
||||||
|
<key>NSStringFormatValueTypeKey</key>
|
||||||
|
<string>@</string>
|
||||||
|
<key>one</key>
|
||||||
|
<string>%2$@ ساتوشی از %3$@ دریافت کردید</string>
|
||||||
|
<key>other</key>
|
||||||
|
<string>%2$@ ساتوشی از %3$@ دریافت کردید</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
<key>zap_notification_with_message</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
<string>%1$#@NOTIFICATION@</string>
|
||||||
|
<key>NOTIFICATION</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringFormatSpecTypeKey</key>
|
||||||
|
<string>NSStringPluralRuleType</string>
|
||||||
|
<key>NSStringFormatValueTypeKey</key>
|
||||||
|
<string>@</string>
|
||||||
|
<key>one</key>
|
||||||
|
<string>%2$@ ساتوشی از %3$@ دریافت کردید: "%4$@"</string>
|
||||||
|
<key>other</key>
|
||||||
|
<string>%2$@ ساتوشی از %3$@ دریافت کردید: "%4$@"</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
<key>zapped_tagged_in_3</key>
|
<key>zapped_tagged_in_3</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSStringLocalizedFormatKey</key>
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
@@ -239,7 +287,7 @@
|
|||||||
<key>one</key>
|
<key>one</key>
|
||||||
<string>%2$@ و %1$d نفر دیگر یک مطلب که شما در آن تگ شدهاید را زپ کردهاند</string>
|
<string>%2$@ و %1$d نفر دیگر یک مطلب که شما در آن تگ شدهاید را زپ کردهاند</string>
|
||||||
<key>other</key>
|
<key>other</key>
|
||||||
<string>%2$@ و %1$d نفر دیگر یک مطلب که شما در آن تگ شدهاید را زپ کردهاند</string>
|
<string>%2$@ و %1$d نفر دیگر یک یادداشت که شما در آن تگ شدهاید را زپ کردهاند</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>zapped_your_post_3</key>
|
<key>zapped_your_post_3</key>
|
||||||
@@ -255,7 +303,7 @@
|
|||||||
<key>one</key>
|
<key>one</key>
|
||||||
<string>%2$@ و %1$d نفر دیگر مطلب شما را زپ کردهاند</string>
|
<string>%2$@ و %1$d نفر دیگر مطلب شما را زپ کردهاند</string>
|
||||||
<key>other</key>
|
<key>other</key>
|
||||||
<string>%2$@ و %1$d نفر دیگر مطلب شما را زپ کردهاند</string>
|
<string>%2$@ و %1$d نفر دیگر یادداشت شما را زپ کردهاند</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>zapped_your_profile_3</key>
|
<key>zapped_your_profile_3</key>
|
||||||
@@ -287,7 +335,7 @@
|
|||||||
<key>one</key>
|
<key>one</key>
|
||||||
<string>Zap</string>
|
<string>Zap</string>
|
||||||
<key>other</key>
|
<key>other</key>
|
||||||
<string>Zaps</string>
|
<string>زپ</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@@ -236,6 +236,24 @@
|
|||||||
<string>Republications</string>
|
<string>Republications</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>sats</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
<string>%#@SATS@</string>
|
||||||
|
<key>SATS</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSStringFormatSpecTypeKey</key>
|
||||||
|
<string>NSStringPluralRuleType</string>
|
||||||
|
<key>NSStringFormatValueTypeKey</key>
|
||||||
|
<string>d</string>
|
||||||
|
<key>one</key>
|
||||||
|
<string>sat</string>
|
||||||
|
<key>many</key>
|
||||||
|
<string>sats</string>
|
||||||
|
<key>other</key>
|
||||||
|
<string>sats</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
<key>sats_count</key>
|
<key>sats_count</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSStringLocalizedFormatKey</key>
|
<key>NSStringLocalizedFormatKey</key>
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -32,4 +32,26 @@ class LikeTests: XCTestCase {
|
|||||||
XCTAssertEqual(like_ev.last_refid()!.ref_id, id)
|
XCTAssertEqual(like_ev.last_refid()!.ref_id, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testToReactionEmoji() {
|
||||||
|
let privkey = "0fc2092231f958f8d57d66f5e238bb45b6a2571f44c0ce024bbc6f3a9c8a15fe"
|
||||||
|
let pubkey = "30c6d1dc7f7c156794fa15055e651b758a61b99f50fcf759de59386050bf6ae2"
|
||||||
|
let liked = NostrEvent(content: "awesome #[0] post", pubkey: "orig_pk", tags: [["p", "cindy"], ["e", "bob"]])
|
||||||
|
liked.calculate_id()
|
||||||
|
let id = liked.id
|
||||||
|
|
||||||
|
let emptyReaction = make_like_event(pubkey: pubkey, privkey: privkey, liked: liked, content: "")
|
||||||
|
let plusReaction = make_like_event(pubkey: pubkey, privkey: privkey, liked: liked, content: "+")
|
||||||
|
let minusReaction = make_like_event(pubkey: pubkey, privkey: privkey, liked: liked, content: "-")
|
||||||
|
let heartReaction = make_like_event(pubkey: pubkey, privkey: privkey, liked: liked, content: "❤️")
|
||||||
|
let thumbsUpReaction = make_like_event(pubkey: pubkey, privkey: privkey, liked: liked, content: "👍")
|
||||||
|
let shakaReaction = make_like_event(pubkey: pubkey, privkey: privkey, liked: liked, content: "🤙")
|
||||||
|
|
||||||
|
XCTAssertEqual(to_reaction_emoji(ev: emptyReaction), "❤️")
|
||||||
|
XCTAssertEqual(to_reaction_emoji(ev: plusReaction), "❤️")
|
||||||
|
XCTAssertEqual(to_reaction_emoji(ev: minusReaction), "👎")
|
||||||
|
XCTAssertEqual(to_reaction_emoji(ev: heartReaction), "❤️")
|
||||||
|
XCTAssertEqual(to_reaction_emoji(ev: thumbsUpReaction), "👍")
|
||||||
|
XCTAssertEqual(to_reaction_emoji(ev: shakaReaction), "🤙")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
84
damusTests/WalletConnectTests.swift
Normal file
84
damusTests/WalletConnectTests.swift
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
//
|
||||||
|
// WalletConnectTests.swift
|
||||||
|
// damusTests
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-04-02.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
@testable import damus
|
||||||
|
|
||||||
|
final class WalletConnectTests: XCTestCase {
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDownWithError() throws {
|
||||||
|
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||||
|
}
|
||||||
|
|
||||||
|
func testWalletBalanceRequest() throws {
|
||||||
|
// This is an example of a functional test case.
|
||||||
|
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||||
|
// Any test you write for XCTest can be annotated as throws and async.
|
||||||
|
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
|
||||||
|
// Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.
|
||||||
|
}
|
||||||
|
|
||||||
|
func get_test_nwc() -> WalletConnectURL {
|
||||||
|
let pk = "9d088f4760422443d4699b485e2ac66e565a2f5da1198c55ddc5679458e3f67a"
|
||||||
|
let sec = "ff2eefd57196d42089e1b42acc39916d7ecac52e0625bd70597bbd5be14aff18"
|
||||||
|
let relay = "wss://relay.getalby.com/v1"
|
||||||
|
let str = "nostrwalletconnect://\(pk)?relay=\(relay)&secret=\(sec)"
|
||||||
|
|
||||||
|
return WalletConnectURL(str: str)!
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDoesNWCParse() {
|
||||||
|
let pk = "9d088f4760422443d4699b485e2ac66e565a2f5da1198c55ddc5679458e3f67a"
|
||||||
|
let sec = "ff2eefd57196d42089e1b42acc39916d7ecac52e0625bd70597bbd5be14aff18"
|
||||||
|
let relay = "wss://relay.getalby.com/v1"
|
||||||
|
let str = "nostrwalletconnect://\(pk)?relay=\(relay)&secret=\(sec)&lud16=jb55@jb55.com"
|
||||||
|
|
||||||
|
let url = WalletConnectURL(str: str)
|
||||||
|
XCTAssertNotNil(url)
|
||||||
|
guard let url else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
XCTAssertEqual(url.pubkey, pk)
|
||||||
|
XCTAssertEqual(url.keypair.privkey, sec)
|
||||||
|
XCTAssertEqual(url.keypair.pubkey, privkey_to_pubkey(privkey: sec))
|
||||||
|
XCTAssertEqual(url.relay.id, relay)
|
||||||
|
XCTAssertEqual(url.lud16, "jb55@jb55.com")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNWCEphemeralRelay() {
|
||||||
|
let sec = "8ba3a6b3b57d0f4211bb1ea4d8d1e351a367e9b4ea694746e0a4a452b2bc4d37"
|
||||||
|
let pk = "89446b900c70d62438dcf66756405eea6225ad94dc61f3856f62f9699111a9a6"
|
||||||
|
let nwc = WalletConnectURL(str: "nostrwalletconnect://\(pk)?relay=ws://127.0.0.1&secret=\(sec)&lud16=jb55@jb55.com")!
|
||||||
|
|
||||||
|
let pool = RelayPool()
|
||||||
|
let box = PostBox(pool: pool)
|
||||||
|
|
||||||
|
nwc_pay(url: nwc, pool: pool, post: box, invoice: "invoice")
|
||||||
|
|
||||||
|
XCTAssertEqual(pool.our_descriptors.count, 0)
|
||||||
|
XCTAssertEqual(pool.all_descriptors.count, 1)
|
||||||
|
XCTAssertEqual(pool.all_descriptors[0].variant, .nwc)
|
||||||
|
XCTAssertEqual(pool.all_descriptors[0].url.id, "ws://127.0.0.1")
|
||||||
|
XCTAssertEqual(box.events.count, 1)
|
||||||
|
let ev = box.events.first!.value
|
||||||
|
XCTAssertEqual(ev.skip_ephemeral, false)
|
||||||
|
XCTAssertEqual(ev.remaining.count, 1)
|
||||||
|
XCTAssertEqual(ev.remaining[0].relay, "ws://127.0.0.1")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPerformanceExample() throws {
|
||||||
|
// This is an example of a performance test case.
|
||||||
|
self.measure {
|
||||||
|
// Put the code you want to measure the time of here.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -24,13 +24,14 @@ final class ZapTests: XCTestCase {
|
|||||||
let target = ZapTarget.profile(bob.pubkey)
|
let target = ZapTarget.profile(bob.pubkey)
|
||||||
|
|
||||||
let message = "hey bob!"
|
let message = "hey bob!"
|
||||||
let zapreq = make_zap_request_event(keypair: alice, content: message, relays: [], target: target, zap_type: .priv)
|
let mzapreq = make_zap_request_event(keypair: alice, content: message, relays: [], target: target, zap_type: .priv)
|
||||||
|
|
||||||
XCTAssertNotNil(zapreq)
|
XCTAssertNotNil(mzapreq)
|
||||||
guard let zapreq else {
|
guard let mzapreq else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let zapreq = mzapreq.potentially_anon_outer_request.ev
|
||||||
let decrypted = decrypt_private_zap(our_privkey: bob.privkey, zapreq: zapreq, target: target)
|
let decrypted = decrypt_private_zap(our_privkey: bob.privkey, zapreq: zapreq, target: target)
|
||||||
|
|
||||||
XCTAssertNotNil(decrypted)
|
XCTAssertNotNil(decrypted)
|
||||||
|
|||||||
8
devtools/fetch-popular-users
Executable file
8
devtools/fetch-popular-users
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
curl $(curl -s 'https://storage.googleapis.com/storage/v1/b/nostrdb-backups/o?prefix=ndjson' | jq -r '.items | last | .mediaLink') > nostr-directory.json
|
||||||
|
|
||||||
|
jq -rc '.data | {url: .profileImageUrl, pk: .hexPubKey, userName: .userName, twitterFollowers: .user.followers_count, nostrFollowers: .nFollowerCount}' nostr-directory.json | jq -cs 'sort_by(.twitterFollowers + .nostrFollowers) | .[]' | tail -n1000 | tac > popular_users.json
|
||||||
|
|
||||||
|
printf "saved popular_users.json\n" >&2
|
||||||
Reference in New Issue
Block a user