Implement NIP04: Encrypted Direct Messages
Closes #5 This adds encrypted direct message support to damus Changelog-Added: Implement NIP04: Encrypted Direct Messages Signed-off-by: William Casarin <jb55@jb55.com>
This commit is contained in:
@@ -13,6 +13,9 @@
|
|||||||
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; };
|
4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; };
|
||||||
4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */; };
|
4C0A3F95280F6C78000448DE /* ReplyQuoteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */; };
|
||||||
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F96280F8E02000448DE /* ThreadView.swift */; };
|
4C0A3F97280F8E02000448DE /* ThreadView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F96280F8E02000448DE /* ThreadView.swift */; };
|
||||||
|
4C216F32286E388800040376 /* DMChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F31286E388800040376 /* DMChatView.swift */; };
|
||||||
|
4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; };
|
||||||
|
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */; };
|
||||||
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 */; };
|
||||||
@@ -57,6 +60,9 @@
|
|||||||
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C63334F283D40E500B1C9C3 /* HomeModel.swift */; };
|
4C633350283D40E500B1C9C3 /* HomeModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C63334F283D40E500B1C9C3 /* HomeModel.swift */; };
|
||||||
4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C633351283D419F00B1C9C3 /* SignalModel.swift */; };
|
4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C633351283D419F00B1C9C3 /* SignalModel.swift */; };
|
||||||
4C649844285A952100EAE2B3 /* LocalUserConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C649843285A952100EAE2B3 /* LocalUserConfig.swift */; };
|
4C649844285A952100EAE2B3 /* LocalUserConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C649843285A952100EAE2B3 /* LocalUserConfig.swift */; };
|
||||||
|
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */; };
|
||||||
|
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */; };
|
||||||
|
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 4C649880286E0EE300EAE2B3 /* secp256k1 */; };
|
||||||
4C75EFA427FA577B0006080F /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; };
|
4C75EFA427FA577B0006080F /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; };
|
||||||
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA527FF87A20006080F /* Nostr.swift */; };
|
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA527FF87A20006080F /* Nostr.swift */; };
|
||||||
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFAC28049CFB0006080F /* PostButton.swift */; };
|
4C75EFAD28049CFB0006080F /* PostButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFAC28049CFB0006080F /* PostButton.swift */; };
|
||||||
@@ -90,7 +96,6 @@
|
|||||||
4CE6DF0427F7A08200C66700 /* damusUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF0327F7A08200C66700 /* damusUITestsLaunchTests.swift */; };
|
4CE6DF0427F7A08200C66700 /* damusUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF0327F7A08200C66700 /* damusUITestsLaunchTests.swift */; };
|
||||||
4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 4CE6DF1127F7A2B300C66700 /* Starscream */; };
|
4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */ = {isa = PBXBuildFile; productRef = 4CE6DF1127F7A2B300C66700 /* Starscream */; };
|
||||||
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */; };
|
4CE6DF1627F8DEBF00C66700 /* RelayConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CE6DF1527F8DEBF00C66700 /* RelayConnection.swift */; };
|
||||||
4CEE2AEB2805AEA300AB5EEF /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 4CEE2AEA2805AEA300AB5EEF /* secp256k1 */; };
|
|
||||||
4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */; };
|
4CEE2AED2805B22500AB5EEF /* NostrRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AEC2805B22500AB5EEF /* NostrRequest.swift */; };
|
||||||
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */; };
|
4CEE2AF1280B216B00AB5EEF /* EventDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */; };
|
||||||
4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF2280B25C500AB5EEF /* ProfilePicView.swift */; };
|
4CEE2AF3280B25C500AB5EEF /* ProfilePicView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CEE2AF2280B25C500AB5EEF /* ProfilePicView.swift */; };
|
||||||
@@ -124,6 +129,9 @@
|
|||||||
4C0A3F92280F66F5000448DE /* ReplyMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyMap.swift; sourceTree = "<group>"; };
|
4C0A3F92280F66F5000448DE /* ReplyMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyMap.swift; sourceTree = "<group>"; };
|
||||||
4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyQuoteView.swift; sourceTree = "<group>"; };
|
4C0A3F94280F6C78000448DE /* ReplyQuoteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyQuoteView.swift; sourceTree = "<group>"; };
|
||||||
4C0A3F96280F8E02000448DE /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; };
|
4C0A3F96280F8E02000448DE /* ThreadView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadView.swift; sourceTree = "<group>"; };
|
||||||
|
4C216F31286E388800040376 /* DMChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMChatView.swift; sourceTree = "<group>"; };
|
||||||
|
4C216F33286F5ACD00040376 /* DMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMView.swift; sourceTree = "<group>"; };
|
||||||
|
4C216F352870A9A700040376 /* InputDismissKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDismissKeyboard.swift; sourceTree = "<group>"; };
|
||||||
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>"; };
|
||||||
@@ -168,6 +176,8 @@
|
|||||||
4C63334F283D40E500B1C9C3 /* HomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeModel.swift; sourceTree = "<group>"; };
|
4C63334F283D40E500B1C9C3 /* HomeModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeModel.swift; sourceTree = "<group>"; };
|
||||||
4C633351283D419F00B1C9C3 /* SignalModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalModel.swift; sourceTree = "<group>"; };
|
4C633351283D419F00B1C9C3 /* SignalModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignalModel.swift; sourceTree = "<group>"; };
|
||||||
4C649843285A952100EAE2B3 /* LocalUserConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalUserConfig.swift; sourceTree = "<group>"; };
|
4C649843285A952100EAE2B3 /* LocalUserConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalUserConfig.swift; sourceTree = "<group>"; };
|
||||||
|
4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesView.swift; sourceTree = "<group>"; };
|
||||||
|
4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesModel.swift; sourceTree = "<group>"; };
|
||||||
4C75EFA327FA577B0006080F /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = "<group>"; };
|
4C75EFA327FA577B0006080F /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = "<group>"; };
|
||||||
4C75EFA527FF87A20006080F /* Nostr.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nostr.swift; sourceTree = "<group>"; };
|
4C75EFA527FF87A20006080F /* Nostr.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nostr.swift; sourceTree = "<group>"; };
|
||||||
4C75EFA72804823E0006080F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
4C75EFA72804823E0006080F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||||
@@ -219,8 +229,8 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
4CEE2AEB2805AEA300AB5EEF /* secp256k1 in Frameworks */,
|
|
||||||
4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */,
|
4CE6DF1227F7A2B300C66700 /* Starscream in Frameworks */,
|
||||||
|
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@@ -268,6 +278,7 @@
|
|||||||
4C987B56283FD07F0042CE38 /* FollowersModel.swift */,
|
4C987B56283FD07F0042CE38 /* FollowersModel.swift */,
|
||||||
4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */,
|
4C5C7E67284ED36500A22DF5 /* SearchHomeModel.swift */,
|
||||||
4C649843285A952100EAE2B3 /* LocalUserConfig.swift */,
|
4C649843285A952100EAE2B3 /* LocalUserConfig.swift */,
|
||||||
|
4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -308,6 +319,9 @@
|
|||||||
4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */,
|
4C5C7E69284EDE2E00A22DF5 /* SearchResultsView.swift */,
|
||||||
4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */,
|
4CE4F9DD2852768D00C00DD9 /* ConfigView.swift */,
|
||||||
4CE4F9E228528C5200C00DD9 /* AddRelayView.swift */,
|
4CE4F9E228528C5200C00DD9 /* AddRelayView.swift */,
|
||||||
|
4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */,
|
||||||
|
4C216F31286E388800040376 /* DMChatView.swift */,
|
||||||
|
4C216F33286F5ACD00040376 /* DMView.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -343,6 +357,7 @@
|
|||||||
4C477C9D282C3A4800033AA3 /* TipCounter.swift */,
|
4C477C9D282C3A4800033AA3 /* TipCounter.swift */,
|
||||||
4C285C8B28398BC6008A31F1 /* Keys.swift */,
|
4C285C8B28398BC6008A31F1 /* Keys.swift */,
|
||||||
4C90BD19283AA67F008EE7EF /* Bech32.swift */,
|
4C90BD19283AA67F008EE7EF /* Bech32.swift */,
|
||||||
|
4C216F352870A9A700040376 /* InputDismissKeyboard.swift */,
|
||||||
);
|
);
|
||||||
path = Util;
|
path = Util;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -447,7 +462,7 @@
|
|||||||
name = damus;
|
name = damus;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
4CE6DF1127F7A2B300C66700 /* Starscream */,
|
4CE6DF1127F7A2B300C66700 /* Starscream */,
|
||||||
4CEE2AEA2805AEA300AB5EEF /* secp256k1 */,
|
4C649880286E0EE300EAE2B3 /* secp256k1 */,
|
||||||
);
|
);
|
||||||
productName = damus;
|
productName = damus;
|
||||||
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
|
productReference = 4CE6DEE327F7A08100C66700 /* damus.app */;
|
||||||
@@ -523,7 +538,7 @@
|
|||||||
mainGroup = 4CE6DEDA27F7A08100C66700;
|
mainGroup = 4CE6DEDA27F7A08100C66700;
|
||||||
packageReferences = (
|
packageReferences = (
|
||||||
4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */,
|
4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */,
|
||||||
4CEE2AE92805AEA300AB5EEF /* XCRemoteSwiftPackageReference "secp256k1" */,
|
4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */,
|
||||||
);
|
);
|
||||||
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
|
productRefGroup = 4CE6DEE427F7A08100C66700 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@@ -571,9 +586,11 @@
|
|||||||
4C363A8A28236B57006E126D /* MentionView.swift in Sources */,
|
4C363A8A28236B57006E126D /* MentionView.swift in Sources */,
|
||||||
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */,
|
4CE4F8CD281352B30009DFBB /* Notifications.swift in Sources */,
|
||||||
4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */,
|
4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */,
|
||||||
|
4C216F34286F5ACD00040376 /* DMView.swift in Sources */,
|
||||||
4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */,
|
4CE4F9E1285287B800C00DD9 /* TextFieldAlert.swift in Sources */,
|
||||||
4C363AA828297703006E126D /* InsertSort.swift in Sources */,
|
4C363AA828297703006E126D /* InsertSort.swift in Sources */,
|
||||||
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */,
|
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */,
|
||||||
|
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */,
|
||||||
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */,
|
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */,
|
||||||
4C363A8628234FDE006E126D /* ImageCache.swift in Sources */,
|
4C363A8628234FDE006E126D /* ImageCache.swift in Sources */,
|
||||||
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
|
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
|
||||||
@@ -591,6 +608,7 @@
|
|||||||
4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */,
|
4C0A3F8C280F5FCA000448DE /* ChatroomView.swift in Sources */,
|
||||||
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */,
|
4C477C9E282C3A4800033AA3 /* TipCounter.swift in Sources */,
|
||||||
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
|
4C0A3F91280F6528000448DE /* ChatView.swift in Sources */,
|
||||||
|
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
|
||||||
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
|
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
|
||||||
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */,
|
4CE4F9DE2852768D00C00DD9 /* ConfigView.swift in Sources */,
|
||||||
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */,
|
4C285C8E28399BFE008A31F1 /* SaveKeysView.swift in Sources */,
|
||||||
@@ -617,6 +635,8 @@
|
|||||||
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */,
|
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */,
|
||||||
4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */,
|
4C633352283D419F00B1C9C3 /* SignalModel.swift in Sources */,
|
||||||
4C363A94282704FA006E126D /* Post.swift in Sources */,
|
4C363A94282704FA006E126D /* Post.swift in Sources */,
|
||||||
|
4C216F32286E388800040376 /* DMChatView.swift in Sources */,
|
||||||
|
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
|
||||||
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
|
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
|
||||||
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */,
|
||||||
4C363A9C282838B9006E126D /* EventRef.swift in Sources */,
|
4C363A9C282838B9006E126D /* EventRef.swift in Sources */,
|
||||||
@@ -991,6 +1011,14 @@
|
|||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/jb55/secp256k1.swift";
|
||||||
|
requirement = {
|
||||||
|
kind = revision;
|
||||||
|
revision = 40b4b38b3b1c83f7088c76189a742870e0ca06a9;
|
||||||
|
};
|
||||||
|
};
|
||||||
4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */ = {
|
4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */ = {
|
||||||
isa = XCRemoteSwiftPackageReference;
|
isa = XCRemoteSwiftPackageReference;
|
||||||
repositoryURL = "https://github.com/daltoniam/Starscream";
|
repositoryURL = "https://github.com/daltoniam/Starscream";
|
||||||
@@ -999,27 +1027,19 @@
|
|||||||
minimumVersion = 4.0.0;
|
minimumVersion = 4.0.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
4CEE2AE92805AEA300AB5EEF /* XCRemoteSwiftPackageReference "secp256k1" */ = {
|
|
||||||
isa = XCRemoteSwiftPackageReference;
|
|
||||||
repositoryURL = "https://github.com/GigaBitcoin/secp256k1.swift";
|
|
||||||
requirement = {
|
|
||||||
kind = upToNextMajorVersion;
|
|
||||||
minimumVersion = 0.5.0;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
4C649880286E0EE300EAE2B3 /* secp256k1 */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */;
|
||||||
|
productName = secp256k1;
|
||||||
|
};
|
||||||
4CE6DF1127F7A2B300C66700 /* Starscream */ = {
|
4CE6DF1127F7A2B300C66700 /* Starscream */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
package = 4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */;
|
package = 4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */;
|
||||||
productName = Starscream;
|
productName = Starscream;
|
||||||
};
|
};
|
||||||
4CEE2AEA2805AEA300AB5EEF /* secp256k1 */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = 4CEE2AE92805AEA300AB5EEF /* XCRemoteSwiftPackageReference "secp256k1" */;
|
|
||||||
productName = secp256k1;
|
|
||||||
};
|
|
||||||
/* End XCSwiftPackageProductDependency section */
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 4CE6DEDB27F7A08100C66700 /* Project object */;
|
rootObject = 4CE6DEDB27F7A08100C66700 /* Project object */;
|
||||||
|
|||||||
@@ -3,10 +3,9 @@
|
|||||||
{
|
{
|
||||||
"identity" : "secp256k1.swift",
|
"identity" : "secp256k1.swift",
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/GigaBitcoin/secp256k1.swift",
|
"location" : "https://github.com/jb55/secp256k1.swift",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "abe7c8232970c1fd57f4c77590bce2c868df7137",
|
"revision" : "40b4b38b3b1c83f7088c76189a742870e0ca06a9"
|
||||||
"version" : "0.5.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -121,6 +121,9 @@ struct ContentView: View {
|
|||||||
case .notifications:
|
case .notifications:
|
||||||
TimelineView(events: $home.notifications, loading: $home.loading, damus: damus)
|
TimelineView(events: $home.notifications, loading: $home.loading, damus: damus)
|
||||||
.navigationTitle("Notifications")
|
.navigationTitle("Notifications")
|
||||||
|
|
||||||
|
case .dms:
|
||||||
|
DirectMessagesView(damus_state: damus_state!, dms: $home.dms)
|
||||||
|
|
||||||
case .none:
|
case .none:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
@@ -142,7 +145,7 @@ struct ContentView: View {
|
|||||||
var MaybeThreadView: some View {
|
var MaybeThreadView: some View {
|
||||||
Group {
|
Group {
|
||||||
if let evid = self.active_event_id {
|
if let evid = self.active_event_id {
|
||||||
let thread_model = ThreadModel(evid: evid, pool: damus_state!.pool)
|
let thread_model = ThreadModel(evid: evid, pool: damus_state!.pool, privkey: damus_state!.keypair.privkey)
|
||||||
ThreadView(thread: thread_model, damus: damus_state!)
|
ThreadView(thread: thread_model, damus: damus_state!)
|
||||||
} else {
|
} else {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
@@ -163,7 +166,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
if let damus = self.damus_state {
|
if let damus = self.damus_state {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
MainContent(damus: damus)
|
MainContent(damus: damus)
|
||||||
|
|||||||
@@ -204,13 +204,13 @@ func make_contact_relays(_ relays: [RelayDescriptor]) -> [String: RelayInfo] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: tests for this
|
// TODO: tests for this
|
||||||
func is_friend_event(_ ev: NostrEvent, our_pubkey: String, contacts: Contacts) -> Bool
|
func is_friend_event(_ ev: NostrEvent, keypair: Keypair, contacts: Contacts) -> Bool
|
||||||
{
|
{
|
||||||
if !contacts.is_friend(ev.pubkey) {
|
if !contacts.is_friend(ev.pubkey) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !ev.is_reply {
|
if ev.is_reply(keypair.privkey) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
15
damus/Models/DirectMessagesModel.swift
Normal file
15
damus/Models/DirectMessagesModel.swift
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// DirectMessagesModel.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2022-06-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
class DirectMessagesModel: ObservableObject {
|
||||||
|
@Published var events: [(String, [NostrEvent])] = []
|
||||||
|
@Published var loading: Bool = false
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -147,8 +147,8 @@ func interpret_event_refs(blocks: [Block], tags: [[String]]) -> [EventRef] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func event_is_reply(_ ev: NostrEvent) -> Bool {
|
func event_is_reply(_ ev: NostrEvent, privkey: String?) -> Bool {
|
||||||
return ev.event_refs.contains { evref in
|
return ev.event_refs(privkey).contains { evref in
|
||||||
return evref.is_reply != nil
|
return evref.is_reply != nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,91 +9,102 @@ import Foundation
|
|||||||
|
|
||||||
struct NewEventsBits {
|
struct NewEventsBits {
|
||||||
let bits: Int
|
let bits: Int
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
bits = 0
|
bits = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
init (prev: NewEventsBits, setting: Timeline) {
|
init (prev: NewEventsBits, setting: Timeline) {
|
||||||
self.bits = prev.bits | timeline_bit(setting)
|
self.bits = prev.bits | timeline_bit(setting)
|
||||||
}
|
}
|
||||||
|
|
||||||
init (prev: NewEventsBits, unsetting: Timeline) {
|
init (prev: NewEventsBits, unsetting: Timeline) {
|
||||||
self.bits = prev.bits & ~timeline_bit(unsetting)
|
self.bits = prev.bits & ~timeline_bit(unsetting)
|
||||||
}
|
}
|
||||||
|
|
||||||
func is_set(_ timeline: Timeline) -> Bool {
|
func is_set(_ timeline: Timeline) -> Bool {
|
||||||
let notification_bit = timeline_bit(timeline)
|
let notification_bit = timeline_bit(timeline)
|
||||||
return (bits & notification_bit) == notification_bit
|
return (bits & notification_bit) == notification_bit
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class HomeModel: ObservableObject {
|
class HomeModel: ObservableObject {
|
||||||
var damus_state: DamusState
|
var damus_state: DamusState
|
||||||
|
|
||||||
var has_event: [String: Set<String>] = [:]
|
var has_event: [String: Set<String>] = [:]
|
||||||
var last_event_of_kind: [String: [Int: NostrEvent]] = [:]
|
var last_event_of_kind: [String: [Int: NostrEvent]] = [:]
|
||||||
var done_init: Bool = false
|
var done_init: Bool = false
|
||||||
|
|
||||||
let home_subid = UUID().description
|
let home_subid = UUID().description
|
||||||
let contacts_subid = UUID().description
|
let contacts_subid = UUID().description
|
||||||
let notifications_subid = UUID().description
|
let notifications_subid = UUID().description
|
||||||
let dms_subid = UUID().description
|
let dms_subid = UUID().description
|
||||||
let init_subid = UUID().description
|
let init_subid = UUID().description
|
||||||
|
|
||||||
@Published var new_events: NewEventsBits = NewEventsBits()
|
@Published var new_events: NewEventsBits = NewEventsBits()
|
||||||
@Published var notifications: [NostrEvent] = []
|
@Published var notifications: [NostrEvent] = []
|
||||||
|
@Published var dms: [(String, [NostrEvent])] = []
|
||||||
@Published var events: [NostrEvent] = []
|
@Published var events: [NostrEvent] = []
|
||||||
@Published var loading: Bool = false
|
@Published var loading: Bool = false
|
||||||
@Published var signal: SignalModel = SignalModel()
|
@Published var signal: SignalModel = SignalModel()
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.damus_state = DamusState.empty
|
self.damus_state = DamusState.empty
|
||||||
}
|
}
|
||||||
|
|
||||||
init(damus_state: DamusState) {
|
init(damus_state: DamusState) {
|
||||||
self.damus_state = damus_state
|
self.damus_state = damus_state
|
||||||
}
|
}
|
||||||
|
|
||||||
var pool: RelayPool {
|
var pool: RelayPool {
|
||||||
return damus_state.pool
|
return damus_state.pool
|
||||||
}
|
}
|
||||||
|
|
||||||
func has_sub_id_event(sub_id: String, ev_id: String) -> Bool {
|
func has_sub_id_event(sub_id: String, ev_id: String) -> Bool {
|
||||||
if !has_event.keys.contains(sub_id) {
|
if !has_event.keys.contains(sub_id) {
|
||||||
has_event[sub_id] = Set()
|
has_event[sub_id] = Set()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return has_event[sub_id]!.contains(ev_id)
|
return has_event[sub_id]!.contains(ev_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func process_event(sub_id: String, relay_id: String, ev: NostrEvent) {
|
func process_event(sub_id: String, relay_id: String, ev: NostrEvent) {
|
||||||
if has_sub_id_event(sub_id: sub_id, ev_id: ev.id) {
|
if has_sub_id_event(sub_id: sub_id, ev_id: ev.id) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let last_k = get_last_event_of_kind(relay_id: relay_id, kind: ev.kind)
|
let last_k = get_last_event_of_kind(relay_id: relay_id, kind: ev.kind)
|
||||||
if last_k == nil || ev.created_at > last_k!.created_at {
|
if last_k == nil || ev.created_at > last_k!.created_at {
|
||||||
last_event_of_kind[relay_id]?[ev.kind] = ev
|
last_event_of_kind[relay_id]?[ev.kind] = ev
|
||||||
}
|
}
|
||||||
if ev.kind == 1 {
|
|
||||||
|
guard let kind = ev.known_kind else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch kind {
|
||||||
|
case .text:
|
||||||
handle_text_event(sub_id: sub_id, ev)
|
handle_text_event(sub_id: sub_id, ev)
|
||||||
} else if ev.kind == 0 {
|
case .contacts:
|
||||||
handle_metadata_event(ev)
|
|
||||||
} else if ev.kind == 6 {
|
|
||||||
handle_boost_event(sub_id: sub_id, ev)
|
|
||||||
} else if ev.kind == 7 {
|
|
||||||
handle_like_event(ev)
|
|
||||||
} else if ev.kind == 3 {
|
|
||||||
handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
|
handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
|
||||||
|
case .metadata:
|
||||||
|
handle_metadata_event(ev)
|
||||||
|
case .boost:
|
||||||
|
handle_boost_event(sub_id: sub_id, ev)
|
||||||
|
case .like:
|
||||||
|
handle_like_event(ev)
|
||||||
|
case .dm:
|
||||||
|
handle_dm(ev)
|
||||||
|
case .delete:
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_contact_event(sub_id: String, relay_id: String, ev: NostrEvent) {
|
func handle_contact_event(sub_id: String, relay_id: String, ev: NostrEvent) {
|
||||||
process_contact_event(pool: damus_state.pool, contacts: damus_state.contacts, pubkey: damus_state.pubkey, ev: ev)
|
process_contact_event(pool: damus_state.pool, contacts: damus_state.contacts, pubkey: damus_state.pubkey, ev: ev)
|
||||||
|
|
||||||
if sub_id == init_subid {
|
if sub_id == init_subid {
|
||||||
pool.send(.unsubscribe(init_subid), to: [relay_id])
|
pool.send(.unsubscribe(init_subid), to: [relay_id])
|
||||||
if !done_init {
|
if !done_init {
|
||||||
@@ -102,23 +113,23 @@ class HomeModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_boost_event(sub_id: String, _ ev: NostrEvent) {
|
func handle_boost_event(sub_id: String, _ ev: NostrEvent) {
|
||||||
var boost_ev_id = ev.last_refid()?.ref_id
|
var boost_ev_id = ev.last_refid()?.ref_id
|
||||||
|
|
||||||
// CHECK SIGS ON THESE
|
// CHECK SIGS ON THESE
|
||||||
if let inner_ev = ev.inner_event {
|
if let inner_ev = ev.inner_event {
|
||||||
boost_ev_id = inner_ev.id
|
boost_ev_id = inner_ev.id
|
||||||
|
|
||||||
if inner_ev.kind == 1 {
|
if inner_ev.kind == 1 {
|
||||||
handle_text_event(sub_id: sub_id, ev)
|
handle_text_event(sub_id: sub_id, ev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let e = boost_ev_id else {
|
guard let e = boost_ev_id else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch self.damus_state.boosts.add_event(ev, target: e) {
|
switch self.damus_state.boosts.add_event(ev, target: e) {
|
||||||
case .already_counted:
|
case .already_counted:
|
||||||
break
|
break
|
||||||
@@ -127,15 +138,15 @@ class HomeModel: ObservableObject {
|
|||||||
notify(.boosted, boosted)
|
notify(.boosted, boosted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_like_event(_ ev: NostrEvent) {
|
func handle_like_event(_ ev: NostrEvent) {
|
||||||
guard let e = ev.last_refid() else {
|
guard let e = ev.last_refid() else {
|
||||||
// no id ref? invalid like event
|
// no id ref? invalid like event
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// CHECK SIGS ON THESE
|
// CHECK SIGS ON THESE
|
||||||
|
|
||||||
switch damus_state.likes.add_event(ev, target: e.ref_id) {
|
switch damus_state.likes.add_event(ev, target: e.ref_id) {
|
||||||
case .already_counted:
|
case .already_counted:
|
||||||
break
|
break
|
||||||
@@ -144,8 +155,8 @@ class HomeModel: ObservableObject {
|
|||||||
notify(.liked, liked)
|
notify(.liked, liked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func handle_event(relay_id: String, conn_event: NostrConnectionEvent) {
|
func handle_event(relay_id: String, conn_event: NostrConnectionEvent) {
|
||||||
switch conn_event {
|
switch conn_event {
|
||||||
case .ws_event(let ev):
|
case .ws_event(let ev):
|
||||||
@@ -156,7 +167,7 @@ class HomeModel: ObservableObject {
|
|||||||
self.events.insert(wsev, at: 0)
|
self.events.insert(wsev, at: 0)
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
switch ev {
|
switch ev {
|
||||||
case .connected:
|
case .connected:
|
||||||
@@ -182,7 +193,7 @@ class HomeModel: ObservableObject {
|
|||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
update_signal_from_pool(signal: self.signal, pool: self.pool)
|
update_signal_from_pool(signal: self.signal, pool: self.pool)
|
||||||
|
|
||||||
print("ws_event \(ev)")
|
print("ws_event \(ev)")
|
||||||
@@ -191,44 +202,58 @@ class HomeModel: ObservableObject {
|
|||||||
switch ev {
|
switch ev {
|
||||||
case .event(let sub_id, let ev):
|
case .event(let sub_id, let ev):
|
||||||
// globally handle likes
|
// globally handle likes
|
||||||
let always_process = sub_id == notifications_subid || sub_id == contacts_subid || sub_id == home_subid || sub_id == init_subid || ev.known_kind == .like || ev.known_kind == .contacts || ev.known_kind == .metadata
|
let always_process = sub_id == notifications_subid || sub_id == contacts_subid || sub_id == home_subid || sub_id == dms_subid || sub_id == init_subid || ev.known_kind == .like || ev.known_kind == .contacts || ev.known_kind == .metadata
|
||||||
if !always_process {
|
if !always_process {
|
||||||
// TODO: other views like threads might have their own sub ids, so ignore those events... or should we?
|
// TODO: other views like threads might have their own sub ids, so ignore those events... or should we?
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.process_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
|
self.process_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
|
||||||
case .notice(let msg):
|
case .notice(let msg):
|
||||||
//self.events.insert(NostrEvent(content: "NOTICE from \(relay_id): \(msg)", pubkey: "system"), at: 0)
|
//self.events.insert(NostrEvent(content: "NOTICE from \(relay_id): \(msg)", pubkey: "system"), at: 0)
|
||||||
print(msg)
|
print(msg)
|
||||||
|
|
||||||
case .eose:
|
case .eose:
|
||||||
self.loading = false
|
self.loading = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Send the initial filters, just our contact list mostly
|
/// Send the initial filters, just our contact list mostly
|
||||||
func send_initial_filters(relay_id: String) {
|
func send_initial_filters(relay_id: String) {
|
||||||
var filter = NostrFilter.filter_contacts
|
var filter = NostrFilter.filter_contacts
|
||||||
filter.authors = [self.damus_state.pubkey]
|
filter.authors = [self.damus_state.pubkey]
|
||||||
filter.limit = 1
|
filter.limit = 1
|
||||||
|
|
||||||
pool.send(.subscribe(.init(filters: [filter], sub_id: init_subid)), to: [relay_id])
|
pool.send(.subscribe(.init(filters: [filter], sub_id: init_subid)), to: [relay_id])
|
||||||
}
|
}
|
||||||
|
|
||||||
func send_home_filters(relay_id: String?) {
|
func send_home_filters(relay_id: String?) {
|
||||||
// TODO: since times should be based on events from a specific relay
|
// TODO: since times should be based on events from a specific relay
|
||||||
// perhaps we could mark this in the relay pool somehow
|
// perhaps we could mark this in the relay pool somehow
|
||||||
|
|
||||||
var friends = damus_state.contacts.get_friend_list()
|
var friends = damus_state.contacts.get_friend_list()
|
||||||
friends.append(damus_state.pubkey)
|
friends.append(damus_state.pubkey)
|
||||||
|
|
||||||
var contacts_filter = NostrFilter.filter_kinds([0])
|
var contacts_filter = NostrFilter.filter_kinds([0])
|
||||||
contacts_filter.authors = friends
|
contacts_filter.authors = friends
|
||||||
|
|
||||||
|
var dms_filter = NostrFilter.filter_kinds([
|
||||||
|
NostrKind.dm.rawValue,
|
||||||
|
])
|
||||||
|
|
||||||
|
var our_dms_filter = NostrFilter.filter_kinds([
|
||||||
|
NostrKind.dm.rawValue,
|
||||||
|
])
|
||||||
|
|
||||||
|
// friends only?...
|
||||||
|
//dms_filter.authors = friends
|
||||||
|
dms_filter.limit = 500
|
||||||
|
dms_filter.pubkeys = [ damus_state.pubkey ]
|
||||||
|
our_dms_filter.authors = [ damus_state.pubkey ]
|
||||||
|
|
||||||
// TODO: separate likes?
|
// TODO: separate likes?
|
||||||
var home_filter = NostrFilter.filter_kinds([
|
var home_filter = NostrFilter.filter_kinds([
|
||||||
NostrKind.text.rawValue,
|
NostrKind.text.rawValue,
|
||||||
@@ -238,7 +263,7 @@ class HomeModel: ObservableObject {
|
|||||||
// include our pubkey as well even if we're not technically a friend
|
// include our pubkey as well even if we're not technically a friend
|
||||||
home_filter.authors = friends
|
home_filter.authors = friends
|
||||||
home_filter.limit = 500
|
home_filter.limit = 500
|
||||||
|
|
||||||
var notifications_filter = NostrFilter.filter_kinds([
|
var notifications_filter = NostrFilter.filter_kinds([
|
||||||
NostrKind.text.rawValue,
|
NostrKind.text.rawValue,
|
||||||
NostrKind.like.rawValue,
|
NostrKind.like.rawValue,
|
||||||
@@ -250,56 +275,60 @@ class HomeModel: ObservableObject {
|
|||||||
var home_filters = [home_filter]
|
var home_filters = [home_filter]
|
||||||
var notifications_filters = [notifications_filter]
|
var notifications_filters = [notifications_filter]
|
||||||
var contacts_filters = [contacts_filter]
|
var contacts_filters = [contacts_filter]
|
||||||
|
var dms_filters = [dms_filter, our_dms_filter]
|
||||||
|
|
||||||
let last_of_kind = relay_id.flatMap { last_event_of_kind[$0] } ?? [:]
|
let last_of_kind = relay_id.flatMap { last_event_of_kind[$0] } ?? [:]
|
||||||
|
|
||||||
home_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: home_filters)
|
home_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: home_filters)
|
||||||
contacts_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: contacts_filters)
|
contacts_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: contacts_filters)
|
||||||
notifications_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: notifications_filters)
|
notifications_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: notifications_filters)
|
||||||
|
dms_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: dms_filters)
|
||||||
print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters])
|
|
||||||
|
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 = 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])
|
||||||
|
pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)), to: [relay_id])
|
||||||
} else {
|
} else {
|
||||||
pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid)))
|
pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid)))
|
||||||
pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid)))
|
pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid)))
|
||||||
pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid)))
|
pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid)))
|
||||||
|
pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_metadata_event(_ ev: NostrEvent) {
|
func handle_metadata_event(_ ev: NostrEvent) {
|
||||||
process_metadata_event(profiles: damus_state.profiles, ev: ev)
|
process_metadata_event(profiles: damus_state.profiles, ev: ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? {
|
func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? {
|
||||||
guard let m = last_event_of_kind[relay_id] else {
|
guard let m = last_event_of_kind[relay_id] else {
|
||||||
last_event_of_kind[relay_id] = [:]
|
last_event_of_kind[relay_id] = [:]
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return m[kind]
|
return m[kind]
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_last_event(ev: NostrEvent, timeline: Timeline) {
|
func handle_last_event(ev: NostrEvent, timeline: Timeline) {
|
||||||
let last_ev = get_last_event(timeline)
|
let last_ev = get_last_event(timeline)
|
||||||
|
|
||||||
if last_ev == nil || last_ev!.created_at < ev.created_at {
|
if last_ev == nil || last_ev!.created_at < ev.created_at {
|
||||||
save_last_event(ev, timeline: timeline)
|
save_last_event(ev, timeline: timeline)
|
||||||
new_events = NewEventsBits(prev: new_events, setting: timeline)
|
new_events = NewEventsBits(prev: new_events, setting: timeline)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_notification(ev: NostrEvent) {
|
func handle_notification(ev: NostrEvent) {
|
||||||
if !insert_uniq_sorted_event(events: ¬ifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
|
if !insert_uniq_sorted_event(events: ¬ifications, new_ev: ev, cmp: { $0.created_at > $1.created_at }) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
handle_last_event(ev: ev, timeline: .notifications)
|
handle_last_event(ev: ev, timeline: .notifications)
|
||||||
}
|
}
|
||||||
|
|
||||||
func insert_home_event(_ ev: NostrEvent) -> Bool {
|
func insert_home_event(_ ev: NostrEvent) -> Bool {
|
||||||
let ok = insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at })
|
let ok = insert_uniq_sorted_event(events: &self.events, new_ev: ev, cmp: { $0.created_at > $1.created_at })
|
||||||
if ok {
|
if ok {
|
||||||
@@ -307,24 +336,67 @@ class HomeModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func should_hide_event(_ ev: NostrEvent) -> Bool {
|
func should_hide_event(_ ev: NostrEvent) -> Bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
|
func handle_text_event(sub_id: String, _ ev: NostrEvent) {
|
||||||
if should_hide_event(ev) {
|
if should_hide_event(ev) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if sub_id == home_subid {
|
if sub_id == home_subid {
|
||||||
if is_friend_event(ev, our_pubkey: damus_state.pubkey, contacts: damus_state.contacts) {
|
if is_friend_event(ev, keypair: damus_state.keypair, contacts: damus_state.contacts) {
|
||||||
let _ = insert_home_event(ev)
|
let _ = insert_home_event(ev)
|
||||||
}
|
}
|
||||||
} else if sub_id == notifications_subid {
|
} else if sub_id == notifications_subid {
|
||||||
handle_notification(ev: ev)
|
handle_notification(ev: ev)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handle_dm(_ ev: NostrEvent) {
|
||||||
|
|
||||||
|
var inserted = false
|
||||||
|
var found = false
|
||||||
|
let ours = ev.pubkey == self.damus_state.pubkey
|
||||||
|
var i = 0
|
||||||
|
|
||||||
|
var the_pk = ev.pubkey
|
||||||
|
if ours {
|
||||||
|
if let ref_pk = ev.referenced_pubkeys.first {
|
||||||
|
the_pk = ref_pk.ref_id
|
||||||
|
} else {
|
||||||
|
// self dm!?
|
||||||
|
print("TODO: handle self dm?")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (pk, _) in dms {
|
||||||
|
if pk == the_pk {
|
||||||
|
found = true
|
||||||
|
inserted = insert_uniq_sorted_event(events: &(dms[i].1), new_ev: ev) {
|
||||||
|
$0.created_at < $1.created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
inserted = true
|
||||||
|
dms.append((the_pk, [ev]))
|
||||||
|
}
|
||||||
|
|
||||||
|
if inserted {
|
||||||
|
handle_last_event(ev: ev, timeline: .dms)
|
||||||
|
|
||||||
|
dms = dms.sorted { a, b in
|
||||||
|
a.1.last!.created_at > b.1.last!.created_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -332,7 +404,7 @@ func update_signal_from_pool(signal: SignalModel, pool: RelayPool) {
|
|||||||
if signal.max_signal != pool.relays.count {
|
if signal.max_signal != pool.relays.count {
|
||||||
signal.max_signal = pool.relays.count
|
signal.max_signal = pool.relays.count
|
||||||
}
|
}
|
||||||
|
|
||||||
if signal.signal != pool.num_connecting {
|
if signal.signal != pool.num_connecting {
|
||||||
signal.signal = signal.max_signal - pool.num_connecting
|
signal.signal = signal.max_signal - pool.num_connecting
|
||||||
}
|
}
|
||||||
@@ -342,7 +414,7 @@ func add_contact_if_friend(contacts: Contacts, ev: NostrEvent) {
|
|||||||
if !contacts.is_friend(ev.pubkey) {
|
if !contacts.is_friend(ev.pubkey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
contacts.add_friend_contact(ev)
|
contacts.add_friend_contact(ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,9 +422,9 @@ func load_our_contacts(contacts: Contacts, our_pubkey: String, ev: NostrEvent) {
|
|||||||
guard ev.pubkey == our_pubkey else {
|
guard ev.pubkey == our_pubkey else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
contacts.event = ev
|
contacts.event = ev
|
||||||
|
|
||||||
// our contacts
|
// our contacts
|
||||||
for tag in ev.tags {
|
for tag in ev.tags {
|
||||||
if tag.count > 1 && tag[0] == "p" {
|
if tag.count > 1 && tag[0] == "p" {
|
||||||
@@ -398,7 +470,7 @@ func print_filter(_ f: NostrFilter) {
|
|||||||
abbrev_field("until", f.until),
|
abbrev_field("until", f.until),
|
||||||
abbrev_field("limit", f.limit)
|
abbrev_field("limit", f.limit)
|
||||||
].filter({ !$0.isEmpty }).joined(separator: ",")
|
].filter({ !$0.isEmpty }).joined(separator: ",")
|
||||||
|
|
||||||
print("Filter(\(fmt))")
|
print("Filter(\(fmt))")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,7 +499,7 @@ func process_metadata_event(profiles: Profiles, ev: NostrEvent) {
|
|||||||
|
|
||||||
let tprof = TimestampedProfile(profile: profile, timestamp: ev.created_at)
|
let tprof = TimestampedProfile(profile: profile, timestamp: ev.created_at)
|
||||||
profiles.add(id: ev.pubkey, profile: tprof)
|
profiles.add(id: ev.pubkey, profile: tprof)
|
||||||
|
|
||||||
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,11 +513,11 @@ func load_our_relays(our_pubkey: String, pool: RelayPool, ev: NostrEvent) {
|
|||||||
guard ev.pubkey == our_pubkey else {
|
guard ev.pubkey == our_pubkey else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let decoded = decode_json_relays(ev.content) else {
|
guard let decoded = decode_json_relays(ev.content) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for key in decoded.keys {
|
for key in decoded.keys {
|
||||||
if let url = URL(string: key) {
|
if let url = URL(string: key) {
|
||||||
if let _ = try? pool.add_relay(url, info: decoded[key]!) {
|
if let _ = try? pool.add_relay(url, info: decoded[key]!) {
|
||||||
@@ -460,11 +532,11 @@ func remove_bootstrap_nodes(_ damus_state: DamusState) {
|
|||||||
guard let contacts = damus_state.contacts.event else {
|
guard let contacts = damus_state.contacts.event else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let relays = decode_json_relays(contacts.content) else {
|
guard let relays = decode_json_relays(contacts.content) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let descriptors = relays.reduce(into: []) { arr, kv in
|
let descriptors = relays.reduce(into: []) { arr, kv in
|
||||||
guard let url = URL(string: kv.key) else {
|
guard let url = URL(string: kv.key) else {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ enum InitialEvent {
|
|||||||
|
|
||||||
/// manages the lifetime of a thread
|
/// manages the lifetime of a thread
|
||||||
class ThreadModel: ObservableObject {
|
class ThreadModel: ObservableObject {
|
||||||
|
let privkey: String?
|
||||||
|
let kind: Int
|
||||||
@Published var initial_event: InitialEvent
|
@Published var initial_event: InitialEvent
|
||||||
@Published var events: [NostrEvent] = []
|
@Published var events: [NostrEvent] = []
|
||||||
@Published var event_map: [String: Int] = [:]
|
@Published var event_map: [String: Int] = [:]
|
||||||
@@ -54,14 +56,25 @@ class ThreadModel: ObservableObject {
|
|||||||
let pool: RelayPool
|
let pool: RelayPool
|
||||||
var sub_id = UUID().description
|
var sub_id = UUID().description
|
||||||
|
|
||||||
init(evid: String, pool: RelayPool) {
|
init(evid: String, pool: RelayPool, privkey: String?) {
|
||||||
self.pool = pool
|
self.pool = pool
|
||||||
self.initial_event = .event_id(evid)
|
self.initial_event = .event_id(evid)
|
||||||
|
self.privkey = privkey
|
||||||
|
self.kind = NostrKind.text.rawValue
|
||||||
}
|
}
|
||||||
|
|
||||||
init(event: NostrEvent, pool: RelayPool) {
|
init(event: NostrEvent, pool: RelayPool, privkey: String?) {
|
||||||
self.pool = pool
|
self.pool = pool
|
||||||
self.initial_event = .event(event)
|
self.initial_event = .event(event)
|
||||||
|
self.privkey = privkey
|
||||||
|
self.kind = NostrKind.text.rawValue
|
||||||
|
}
|
||||||
|
|
||||||
|
init(event: NostrEvent, pool: RelayPool, privkey: String?, kind: Int) {
|
||||||
|
self.pool = pool
|
||||||
|
self.initial_event = .event(event)
|
||||||
|
self.privkey = privkey
|
||||||
|
self.kind = kind
|
||||||
}
|
}
|
||||||
|
|
||||||
func unsubscribe() {
|
func unsubscribe() {
|
||||||
@@ -89,7 +102,7 @@ class ThreadModel: ObservableObject {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func set_active_event(_ ev: NostrEvent) {
|
func set_active_event(_ ev: NostrEvent, privkey: String?) {
|
||||||
if should_resubscribe(ev) {
|
if should_resubscribe(ev) {
|
||||||
unsubscribe()
|
unsubscribe()
|
||||||
self.initial_event = .event(ev)
|
self.initial_event = .event(ev)
|
||||||
@@ -97,14 +110,14 @@ class ThreadModel: ObservableObject {
|
|||||||
} else {
|
} else {
|
||||||
self.initial_event = .event(ev)
|
self.initial_event = .event(ev)
|
||||||
if events.count == 0 {
|
if events.count == 0 {
|
||||||
add_event(ev)
|
add_event(ev, privkey: privkey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func subscribe() {
|
func subscribe() {
|
||||||
var ref_events = NostrFilter.filter_kinds([1,5,6,7])
|
var ref_events = NostrFilter.filter_kinds([self.kind,5,6,7])
|
||||||
var events_filter = NostrFilter.filter_kinds([1])
|
var events_filter = NostrFilter.filter_kinds([self.kind])
|
||||||
//var likes_filter = NostrFilter.filter_kinds(7])
|
//var likes_filter = NostrFilter.filter_kinds(7])
|
||||||
|
|
||||||
// TODO: add referenced relays
|
// TODO: add referenced relays
|
||||||
@@ -134,12 +147,12 @@ class ThreadModel: ObservableObject {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func add_event(_ ev: NostrEvent) {
|
func add_event(_ ev: NostrEvent, privkey: String?) {
|
||||||
if event_map[ev.id] != nil {
|
if event_map[ev.id] != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for reply in ev.direct_replies() {
|
for reply in ev.direct_replies(privkey) {
|
||||||
self.replies.add(id: ev.id, reply_id: reply.ref_id)
|
self.replies.add(id: ev.id, reply_id: reply.ref_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +171,7 @@ class ThreadModel: ObservableObject {
|
|||||||
if let evid = self.initial_event.is_event_id {
|
if let evid = self.initial_event.is_event_id {
|
||||||
if ev.id == evid {
|
if ev.id == evid {
|
||||||
// this should trigger a resubscribe...
|
// this should trigger a resubscribe...
|
||||||
set_active_event(ev)
|
set_active_event(ev, privkey: privkey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,7 +180,7 @@ class ThreadModel: ObservableObject {
|
|||||||
func handle_event(relay_id: String, ev: NostrConnectionEvent) {
|
func handle_event(relay_id: String, ev: NostrConnectionEvent) {
|
||||||
let done = handle_subid_event(pool: pool, sub_id: sub_id, relay_id: relay_id, ev: ev) { ev in
|
let done = handle_subid_event(pool: pool, sub_id: sub_id, relay_id: relay_id, ev: ev) { ev in
|
||||||
if ev.known_kind == .text {
|
if ev.known_kind == .text {
|
||||||
self.add_event(ev)
|
self.add_event(ev, privkey: self.privkey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import CommonCrypto
|
import CommonCrypto
|
||||||
import secp256k1
|
import secp256k1
|
||||||
|
import secp256k1_implementation
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
struct OtherEvent {
|
struct OtherEvent {
|
||||||
let event_id: String
|
let event_id: String
|
||||||
@@ -23,7 +25,7 @@ struct ReferencedId: Identifiable, Hashable {
|
|||||||
let ref_id: String
|
let ref_id: String
|
||||||
let relay_id: String?
|
let relay_id: String?
|
||||||
let key: String
|
let key: String
|
||||||
|
|
||||||
var id: String {
|
var id: String {
|
||||||
return ref_id
|
return ref_id
|
||||||
}
|
}
|
||||||
@@ -53,24 +55,74 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible {
|
|||||||
let created_at: Int64
|
let created_at: Int64
|
||||||
let kind: Int
|
let kind: Int
|
||||||
let content: String
|
let content: String
|
||||||
|
|
||||||
lazy var blocks: [Block] = {
|
private var _blocks: [Block]? = nil
|
||||||
return parse_mentions(content: self.content, tags: self.tags)
|
func blocks(_ privkey: String?) -> [Block] {
|
||||||
}()
|
if let bs = _blocks {
|
||||||
|
return bs
|
||||||
|
}
|
||||||
|
let blocks = parse_mentions(content: self.get_content(privkey), tags: self.tags)
|
||||||
|
self._blocks = blocks
|
||||||
|
return blocks
|
||||||
|
}
|
||||||
|
|
||||||
lazy var inner_event: NostrEvent? = {
|
lazy var inner_event: NostrEvent? = {
|
||||||
return event_from_json(dat: self.content)
|
return event_from_json(dat: self.content)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
lazy var event_refs: [EventRef] = {
|
private var _event_refs: [EventRef]? = nil
|
||||||
return interpret_event_refs(blocks: self.blocks, tags: self.tags)
|
func event_refs(_ privkey: String?) -> [EventRef] {
|
||||||
}()
|
if let rs = _event_refs {
|
||||||
|
return rs
|
||||||
|
}
|
||||||
|
let refs = interpret_event_refs(blocks: self.blocks(privkey), tags: self.tags)
|
||||||
|
self._event_refs = refs
|
||||||
|
return refs
|
||||||
|
}
|
||||||
|
|
||||||
|
var decrypted_content: String? = nil
|
||||||
|
|
||||||
|
func decrypted(privkey: String?) -> String? {
|
||||||
|
if let decrypted_content = decrypted_content {
|
||||||
|
return decrypted_content
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let key = privkey else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let our_pubkey = privkey_to_pubkey(privkey: key) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var pubkey = self.pubkey
|
||||||
|
// This is our DM, we need to use the pubkey of the person we're talking to instead
|
||||||
|
if our_pubkey == pubkey {
|
||||||
|
guard let refkey = self.referenced_pubkeys.first else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pubkey = refkey.ref_id
|
||||||
|
}
|
||||||
|
|
||||||
|
let dec = decrypt_dm(key, pubkey: pubkey, content: self.content)
|
||||||
|
self.decrypted_content = dec
|
||||||
|
|
||||||
|
return dec
|
||||||
|
}
|
||||||
|
|
||||||
|
func get_content(_ privkey: String?) -> String {
|
||||||
|
if known_kind == .dm {
|
||||||
|
return decrypted(privkey: privkey) ?? "*failed to decrypt content*"
|
||||||
|
}
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
var description: String {
|
var description: String {
|
||||||
let p = pow.map { String($0) } ?? "?"
|
let p = pow.map { String($0) } ?? "?"
|
||||||
return "NostrEvent { id: \(id) pubkey \(pubkey) kind \(kind) tags \(tags) pow \(p) content '\(content)' }"
|
return "NostrEvent { id: \(id) pubkey \(pubkey) kind \(kind) tags \(tags) pow \(p) content '\(content)' }"
|
||||||
}
|
}
|
||||||
|
|
||||||
var known_kind: NostrKind? {
|
var known_kind: NostrKind? {
|
||||||
return NostrKind.init(rawValue: kind)
|
return NostrKind.init(rawValue: kind)
|
||||||
}
|
}
|
||||||
@@ -82,7 +134,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible {
|
|||||||
private func get_referenced_ids(key: String) -> [ReferencedId] {
|
private func get_referenced_ids(key: String) -> [ReferencedId] {
|
||||||
return damus.get_referenced_ids(tags: self.tags, key: key)
|
return damus.get_referenced_ids(tags: self.tags, key: key)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func is_root_event() -> Bool {
|
public func is_root_event() -> Bool {
|
||||||
for tag in tags {
|
for tag in tags {
|
||||||
if tag.count >= 1 && tag[0] == "e" {
|
if tag.count >= 1 && tag[0] == "e" {
|
||||||
@@ -91,15 +143,15 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
public func direct_replies() -> [ReferencedId] {
|
public func direct_replies(_ privkey: String?) -> [ReferencedId] {
|
||||||
return event_refs.reduce(into: []) { acc, evref in
|
return event_refs(privkey).reduce(into: []) { acc, evref in
|
||||||
if let direct_reply = evref.is_direct_reply {
|
if let direct_reply = evref.is_direct_reply {
|
||||||
acc.append(direct_reply)
|
acc.append(direct_reply)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func last_refid() -> ReferencedId? {
|
public func last_refid() -> ReferencedId? {
|
||||||
var mlast: Int? = nil
|
var mlast: Int? = nil
|
||||||
var i: Int = 0
|
var i: Int = 0
|
||||||
@@ -109,14 +161,14 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible {
|
|||||||
}
|
}
|
||||||
i += 1
|
i += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let last = mlast else {
|
guard let last = mlast else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return tag_to_refid(tags[last])
|
return tag_to_refid(tags[last])
|
||||||
}
|
}
|
||||||
|
|
||||||
public func references(id: String, key: String) -> Bool {
|
public func references(id: String, key: String) -> Bool {
|
||||||
for tag in tags {
|
for tag in tags {
|
||||||
if tag.count >= 2 && tag[0] == key {
|
if tag.count >= 2 && tag[0] == key {
|
||||||
@@ -129,18 +181,18 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
public var is_reply: Bool {
|
func is_reply(_ privkey: String?) -> Bool {
|
||||||
return event_is_reply(self)
|
return event_is_reply(self, privkey: privkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var referenced_ids: [ReferencedId] {
|
public var referenced_ids: [ReferencedId] {
|
||||||
return get_referenced_ids(key: "e")
|
return get_referenced_ids(key: "e")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func count_ids() -> Int {
|
public func count_ids() -> Int {
|
||||||
return count_refs("e")
|
return count_refs("e")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func count_refs(_ type: String) -> Int {
|
public func count_refs(_ type: String) -> Int {
|
||||||
var count: Int = 0
|
var count: Int = 0
|
||||||
for tag in tags {
|
for tag in tags {
|
||||||
@@ -150,7 +202,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible {
|
|||||||
}
|
}
|
||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
public var referenced_pubkeys: [ReferencedId] {
|
public var referenced_pubkeys: [ReferencedId] {
|
||||||
return get_referenced_ids(key: "p")
|
return get_referenced_ids(key: "p")
|
||||||
}
|
}
|
||||||
@@ -165,7 +217,7 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible {
|
|||||||
public var is_local: Bool {
|
public var is_local: Bool {
|
||||||
return (self.flags & 1) != 0
|
return (self.flags & 1) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
init(content: String, pubkey: String, kind: Int = 1, tags: [[String]] = []) {
|
init(content: String, pubkey: String, kind: Int = 1, tags: [[String]] = []) {
|
||||||
self.id = ""
|
self.id = ""
|
||||||
self.sig = ""
|
self.sig = ""
|
||||||
@@ -176,12 +228,12 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible {
|
|||||||
self.tags = tags
|
self.tags = tags
|
||||||
self.created_at = Int64(Date().timeIntervalSince1970)
|
self.created_at = Int64(Date().timeIntervalSince1970)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(from: NostrEvent) {
|
init(from: NostrEvent, content: String? = nil) {
|
||||||
self.id = from.id
|
self.id = from.id
|
||||||
self.sig = from.sig
|
self.sig = from.sig
|
||||||
|
|
||||||
self.content = from.content
|
self.content = content ?? from.content
|
||||||
self.pubkey = from.pubkey
|
self.pubkey = from.pubkey
|
||||||
self.kind = from.kind
|
self.kind = from.kind
|
||||||
self.tags = from.tags
|
self.tags = from.tags
|
||||||
@@ -224,13 +276,13 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sign_event(privkey: String, ev: NostrEvent) -> String {
|
func sign_event(privkey: String, ev: NostrEvent) -> String {
|
||||||
let priv_key_bytes = try! privkey.byteArray()
|
let priv_key_bytes = try! privkey.bytes
|
||||||
let key = try! secp256k1.Signing.PrivateKey(rawRepresentation: priv_key_bytes)
|
let key = try! secp256k1.Signing.PrivateKey(rawRepresentation: priv_key_bytes)
|
||||||
|
|
||||||
// Extra params for custom signing
|
// Extra params for custom signing
|
||||||
|
|
||||||
var aux_rand = random_bytes(count: 64)
|
var aux_rand = random_bytes(count: 64)
|
||||||
var digest = try! ev.id.byteArray()
|
var digest = try! ev.id.bytes
|
||||||
|
|
||||||
// API allows for signing variable length messages
|
// API allows for signing variable length messages
|
||||||
let signature = try! key.schnorr.signature(message: &digest, auxiliaryRand: &aux_rand)
|
let signature = try! key.schnorr.signature(message: &digest, auxiliaryRand: &aux_rand)
|
||||||
@@ -343,12 +395,12 @@ func tag_to_refid(_ tag: [String]) -> ReferencedId? {
|
|||||||
if tag.count == 1 {
|
if tag.count == 1 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var relay_id: String? = nil
|
var relay_id: String? = nil
|
||||||
if tag.count > 2 {
|
if tag.count > 2 {
|
||||||
relay_id = tag[2]
|
relay_id = tag[2]
|
||||||
}
|
}
|
||||||
|
|
||||||
return ReferencedId(ref_id: tag[1], relay_id: relay_id, key: tag[0])
|
return ReferencedId(ref_id: tag[1], relay_id: relay_id, key: tag[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,7 +432,7 @@ func make_first_contact_event(keypair: Keypair) -> NostrEvent? {
|
|||||||
guard let privkey = keypair.privkey else {
|
guard let privkey = keypair.privkey else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let rw_relay_info = RelayInfo(read: true, write: true)
|
let rw_relay_info = RelayInfo(read: true, write: true)
|
||||||
let relays: [String: RelayInfo] = ["wss://relay.damus.io": rw_relay_info]
|
let relays: [String: RelayInfo] = ["wss://relay.damus.io": rw_relay_info]
|
||||||
let relay_json = encode_json(relays)!
|
let relay_json = encode_json(relays)!
|
||||||
@@ -402,13 +454,13 @@ func make_metadata_event(keypair: Keypair, metadata: NostrMetadata) -> NostrEven
|
|||||||
guard let privkey = keypair.privkey else {
|
guard let privkey = keypair.privkey else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let metadata_json = encode_json(metadata)!
|
let metadata_json = encode_json(metadata)!
|
||||||
let ev = NostrEvent(content: metadata_json,
|
let ev = NostrEvent(content: metadata_json,
|
||||||
pubkey: keypair.pubkey,
|
pubkey: keypair.pubkey,
|
||||||
kind: NostrKind.metadata.rawValue,
|
kind: NostrKind.metadata.rawValue,
|
||||||
tags: [])
|
tags: [])
|
||||||
|
|
||||||
ev.calculate_id()
|
ev.calculate_id()
|
||||||
ev.sign(privkey: privkey)
|
ev.sign(privkey: privkey)
|
||||||
return ev
|
return ev
|
||||||
@@ -418,7 +470,7 @@ func make_boost_event(pubkey: String, privkey: String, boosted: NostrEvent) -> N
|
|||||||
var tags: [[String]] = boosted.tags.filter { tag in tag.count >= 2 && (tag[0] == "e" || tag[0] == "p") }
|
var tags: [[String]] = boosted.tags.filter { tag in tag.count >= 2 && (tag[0] == "e" || tag[0] == "p") }
|
||||||
tags.append(["e", boosted.id])
|
tags.append(["e", boosted.id])
|
||||||
tags.append(["p", boosted.pubkey])
|
tags.append(["p", boosted.pubkey])
|
||||||
|
|
||||||
let ev = NostrEvent(content: event_to_json(ev: boosted), pubkey: pubkey, kind: 6, tags: tags)
|
let ev = NostrEvent(content: event_to_json(ev: boosted), pubkey: pubkey, kind: 6, tags: tags)
|
||||||
ev.calculate_id()
|
ev.calculate_id()
|
||||||
ev.sign(privkey: privkey)
|
ev.sign(privkey: privkey)
|
||||||
@@ -432,13 +484,13 @@ func make_like_event(pubkey: String, privkey: String, liked: NostrEvent) -> Nost
|
|||||||
let ev = NostrEvent(content: "", pubkey: pubkey, kind: 7, tags: tags)
|
let ev = NostrEvent(content: "", pubkey: pubkey, kind: 7, tags: tags)
|
||||||
ev.calculate_id()
|
ev.calculate_id()
|
||||||
ev.sign(privkey: privkey)
|
ev.sign(privkey: privkey)
|
||||||
|
|
||||||
return ev
|
return ev
|
||||||
}
|
}
|
||||||
|
|
||||||
func gather_reply_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
|
func gather_reply_ids(our_pubkey: String, from: NostrEvent) -> [ReferencedId] {
|
||||||
var ids = get_referenced_ids(tags: from.tags, key: "e").first.map { [$0] } ?? []
|
var ids = get_referenced_ids(tags: from.tags, key: "e").first.map { [$0] } ?? []
|
||||||
|
|
||||||
ids.append(ReferencedId(ref_id: from.id, relay_id: nil, key: "e"))
|
ids.append(ReferencedId(ref_id: from.id, relay_id: nil, key: "e"))
|
||||||
ids.append(contentsOf: from.referenced_pubkeys.filter { $0.ref_id != our_pubkey })
|
ids.append(contentsOf: from.referenced_pubkeys.filter { $0.ref_id != our_pubkey })
|
||||||
if from.pubkey != our_pubkey {
|
if from.pubkey != our_pubkey {
|
||||||
@@ -461,3 +513,152 @@ func event_to_json(ev: NostrEvent) -> String {
|
|||||||
}
|
}
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decrypt_dm(_ privkey: String?, pubkey: String, content: String) -> String? {
|
||||||
|
guard let privkey = privkey else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: pubkey) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let dat = decode_dm_base64(content) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let dat = aes_decrypt(data: dat.content, iv: dat.iv, shared_sec: shared_sec) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return String(data: dat, encoding: .utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func get_shared_secret(privkey: String, pubkey: String) -> [UInt8]? {
|
||||||
|
guard let privkey_bytes = try? privkey.bytes else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard var pk_bytes = try? pubkey.bytes else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
pk_bytes.insert(2, at: 0)
|
||||||
|
|
||||||
|
var publicKey = secp256k1_pubkey()
|
||||||
|
var shared_secret = [UInt8](repeating: 0, count: 32)
|
||||||
|
|
||||||
|
var ok =
|
||||||
|
secp256k1_ec_pubkey_parse(
|
||||||
|
secp256k1.Context.raw,
|
||||||
|
&publicKey,
|
||||||
|
pk_bytes,
|
||||||
|
pk_bytes.count) != 0
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = secp256k1_ecdh(
|
||||||
|
secp256k1.Context.raw,
|
||||||
|
&shared_secret,
|
||||||
|
&publicKey,
|
||||||
|
privkey_bytes, {(output,x32,_,_) in
|
||||||
|
memcpy(output,x32,32)
|
||||||
|
return 1
|
||||||
|
}, nil) != 0
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return shared_secret
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DirectMessageBase64 {
|
||||||
|
let content: [UInt8]
|
||||||
|
let iv: [UInt8]
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode_dm_base64(content: [UInt8], iv: [UInt8]) -> String {
|
||||||
|
let content_b64 = base64_encode(content)
|
||||||
|
let iv_b64 = base64_encode(iv)
|
||||||
|
return content_b64 + "?iv=" + iv_b64
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode_dm_base64(_ all: String) -> DirectMessageBase64? {
|
||||||
|
let splits = Array(all.split(separator: "?"))
|
||||||
|
|
||||||
|
if splits.count != 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let content = base64_decode(String(splits[0])) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var sec = String(splits[1])
|
||||||
|
if !sec.hasPrefix("iv=") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sec = String(sec.dropFirst(3))
|
||||||
|
guard let iv = base64_decode(sec) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return DirectMessageBase64(content: content, iv: iv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func base64_encode(_ content: [UInt8]) -> String {
|
||||||
|
return Data(content).base64EncodedString()
|
||||||
|
}
|
||||||
|
|
||||||
|
func base64_decode(_ content: String) -> [UInt8]? {
|
||||||
|
guard let dat = Data(base64Encoded: content) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return dat.bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func aes_decrypt(data: [UInt8], iv: [UInt8], shared_sec: [UInt8]) -> Data? {
|
||||||
|
return aes_operation(operation: CCOperation(kCCDecrypt), data: data, iv: iv, shared_sec: shared_sec)
|
||||||
|
}
|
||||||
|
|
||||||
|
func aes_encrypt(data: [UInt8], iv: [UInt8], shared_sec: [UInt8]) -> Data? {
|
||||||
|
return aes_operation(operation: CCOperation(kCCEncrypt), data: data, iv: iv, shared_sec: shared_sec)
|
||||||
|
}
|
||||||
|
|
||||||
|
func aes_operation(operation: CCOperation, data: [UInt8], iv: [UInt8], shared_sec: [UInt8]) -> Data? {
|
||||||
|
let data_len = data.count
|
||||||
|
let bsize = kCCBlockSizeAES128
|
||||||
|
let len = Int(data_len) + bsize
|
||||||
|
var decrypted_data = [UInt8](repeating: 0, count: len)
|
||||||
|
|
||||||
|
let key_length = size_t(kCCKeySizeAES256)
|
||||||
|
if shared_sec.count != key_length {
|
||||||
|
assert(false, "unexpected shared_sec len: \(shared_sec.count) != 32")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let algorithm: CCAlgorithm = UInt32(kCCAlgorithmAES128)
|
||||||
|
let options: CCOptions = UInt32(kCCOptionPKCS7Padding)
|
||||||
|
|
||||||
|
var num_bytes_decrypted :size_t = 0
|
||||||
|
|
||||||
|
let status = CCCrypt(operation, /*op:*/
|
||||||
|
algorithm, /*alg:*/
|
||||||
|
options, /*options:*/
|
||||||
|
shared_sec, /*key:*/
|
||||||
|
key_length, /*keyLength:*/
|
||||||
|
iv, /*iv:*/
|
||||||
|
data, /*dataIn:*/
|
||||||
|
data_len, /*dataInLength:*/
|
||||||
|
&decrypted_data,/*dataOut:*/
|
||||||
|
len,/*dataOutAvailable:*/
|
||||||
|
&num_bytes_decrypted/*dataOutMoved:*/
|
||||||
|
)
|
||||||
|
|
||||||
|
if UInt32(status) != UInt32(kCCSuccess) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return Data(bytes: decrypted_data, count: num_bytes_decrypted)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ enum NostrKind: Int {
|
|||||||
case metadata = 0
|
case metadata = 0
|
||||||
case text = 1
|
case text = 1
|
||||||
case contacts = 3
|
case contacts = 3
|
||||||
|
case dm = 4
|
||||||
case delete = 5
|
case delete = 5
|
||||||
case boost = 6
|
case boost = 6
|
||||||
case like = 7
|
case like = 7
|
||||||
|
|||||||
39
damus/Util/InputDismissKeyboard.swift
Normal file
39
damus/Util/InputDismissKeyboard.swift
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
//
|
||||||
|
// InputDismissKeyboard.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2022-07-02.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public extension View {
|
||||||
|
func dismissKeyboardOnTap() -> some View {
|
||||||
|
modifier(DismissKeyboardOnTap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct DismissKeyboardOnTap: ViewModifier {
|
||||||
|
public func body(content: Content) -> some View {
|
||||||
|
#if os(macOS)
|
||||||
|
return content
|
||||||
|
#else
|
||||||
|
return content.gesture(tapGesture)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private var tapGesture: some Gesture {
|
||||||
|
TapGesture().onEnded(endEditing)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func endEditing() {
|
||||||
|
UIApplication.shared.connectedScenes
|
||||||
|
.filter {$0.activationState == .foregroundActive}
|
||||||
|
.map {$0 as? UIWindowScene}
|
||||||
|
.compactMap({$0})
|
||||||
|
.first?.windows
|
||||||
|
.filter {$0.isKeyWindow}
|
||||||
|
.first?.endEditing(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,7 +61,7 @@ func bech32_pubkey(_ pubkey: String) -> String? {
|
|||||||
func generate_new_keypair() -> Keypair {
|
func generate_new_keypair() -> Keypair {
|
||||||
let key = try! secp256k1.Signing.PrivateKey()
|
let key = try! secp256k1.Signing.PrivateKey()
|
||||||
let privkey = hex_encode(key.rawRepresentation)
|
let privkey = hex_encode(key.rawRepresentation)
|
||||||
let pubkey = hex_encode(Data(key.publicKey.xonlyKeyBytes))
|
let pubkey = hex_encode(Data(key.publicKey.xonly.bytes))
|
||||||
print("generating privkey:\(privkey) pubkey:\(pubkey)")
|
print("generating privkey:\(privkey) pubkey:\(pubkey)")
|
||||||
return Keypair(pubkey: pubkey, privkey: privkey)
|
return Keypair(pubkey: pubkey, privkey: privkey)
|
||||||
}
|
}
|
||||||
@@ -73,7 +73,7 @@ func privkey_to_pubkey(privkey: String) -> String? {
|
|||||||
guard let key = try? secp256k1.Signing.PrivateKey(rawRepresentation: sec) else {
|
guard let key = try? secp256k1.Signing.PrivateKey(rawRepresentation: sec) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return hex_encode(Data(key.publicKey.xonlyKeyBytes))
|
return hex_encode(Data(key.publicKey.xonly.bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
func save_pubkey(pubkey: String) {
|
func save_pubkey(pubkey: String) {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ struct ChatView: View {
|
|||||||
let prev_ev: NostrEvent?
|
let prev_ev: NostrEvent?
|
||||||
let next_ev: NostrEvent?
|
let next_ev: NostrEvent?
|
||||||
|
|
||||||
let damus: DamusState
|
let damus_state: DamusState
|
||||||
|
|
||||||
@EnvironmentObject var thread: ThreadModel
|
@EnvironmentObject var thread: ThreadModel
|
||||||
|
|
||||||
@@ -45,16 +45,7 @@ struct ChatView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func prev_reply_is_same() -> String? {
|
func prev_reply_is_same() -> String? {
|
||||||
if let prev = prev_ev {
|
return damus.prev_reply_is_same(event: event, prev_ev: prev_ev, replies: thread.replies)
|
||||||
if let prev_reply_id = thread.replies.lookup(prev.id) {
|
|
||||||
if let cur_reply_id = thread.replies.lookup(event.id) {
|
|
||||||
if prev_reply_id != cur_reply_id {
|
|
||||||
return cur_reply_id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func reply_is_new() -> String? {
|
func reply_is_new() -> String? {
|
||||||
@@ -71,7 +62,7 @@ struct ChatView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ReplyDescription: some View {
|
var ReplyDescription: some View {
|
||||||
Text("\(reply_desc(profiles: damus.profiles, event: event))")
|
Text("\(reply_desc(profiles: damus_state.profiles, event: event))")
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
.frame(alignment: .leading)
|
.frame(alignment: .leading)
|
||||||
@@ -83,7 +74,7 @@ struct ChatView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
VStack {
|
VStack {
|
||||||
if is_active || just_started {
|
if is_active || just_started {
|
||||||
ProfilePicView(pubkey: event.pubkey, size: 32, highlight: is_active ? .main : .none, image_cache: damus.image_cache, profiles: damus.profiles)
|
ProfilePicView(pubkey: event.pubkey, size: 32, highlight: is_active ? .main : .none, image_cache: damus_state.image_cache, profiles: damus_state.profiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -94,7 +85,7 @@ struct ChatView: View {
|
|||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
if just_started {
|
if just_started {
|
||||||
HStack {
|
HStack {
|
||||||
ProfileName(pubkey: event.pubkey, profile: damus.profiles.lookup(id: event.pubkey))
|
ProfileName(pubkey: event.pubkey, profile: damus_state.profiles.lookup(id: event.pubkey))
|
||||||
.foregroundColor(colorScheme == .dark ? id_to_color(event.pubkey) : Color.black)
|
.foregroundColor(colorScheme == .dark ? id_to_color(event.pubkey) : Color.black)
|
||||||
//.shadow(color: Color.black, radius: 2)
|
//.shadow(color: Color.black, radius: 2)
|
||||||
Text("\(format_relative_time(event.created_at))")
|
Text("\(format_relative_time(event.created_at))")
|
||||||
@@ -104,17 +95,17 @@ struct ChatView: View {
|
|||||||
|
|
||||||
if let ref_id = thread.replies.lookup(event.id) {
|
if let ref_id = thread.replies.lookup(event.id) {
|
||||||
if !is_reply_to_prev() {
|
if !is_reply_to_prev() {
|
||||||
ReplyQuoteView(quoter: event, event_id: ref_id, image_cache: damus.image_cache, profiles: damus.profiles)
|
ReplyQuoteView(privkey: damus_state.keypair.privkey, quoter: event, event_id: ref_id, image_cache: damus_state.image_cache, profiles: damus_state.profiles)
|
||||||
.environmentObject(thread)
|
.environmentObject(thread)
|
||||||
ReplyDescription
|
ReplyDescription
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NoteContentView(event: event, profiles: damus.profiles, content: event.content)
|
NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, content: event.content)
|
||||||
|
|
||||||
if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey {
|
if is_active || next_ev == nil || next_ev!.pubkey != event.pubkey {
|
||||||
let bar = make_actionbar_model(ev: event, damus: damus)
|
let bar = make_actionbar_model(ev: event, damus: damus_state)
|
||||||
EventActionBar(damus_state: damus, event: event, bar: bar)
|
EventActionBar(damus_state: damus_state, event: event, bar: bar)
|
||||||
}
|
}
|
||||||
|
|
||||||
//Spacer()
|
//Spacer()
|
||||||
@@ -154,3 +145,16 @@ struct ChatView_Previews: PreviewProvider {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
func prev_reply_is_same(event: NostrEvent, prev_ev: NostrEvent?, replies: ReplyMap) -> String? {
|
||||||
|
if let prev = prev_ev {
|
||||||
|
if let prev_reply_id = replies.lookup(prev.id) {
|
||||||
|
if let cur_reply_id = replies.lookup(event.id) {
|
||||||
|
if prev_reply_id != cur_reply_id {
|
||||||
|
return cur_reply_id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ struct ChatroomView: View {
|
|||||||
ChatView(event: thread.events[ind],
|
ChatView(event: thread.events[ind],
|
||||||
prev_ev: ind > 0 ? thread.events[ind-1] : nil,
|
prev_ev: ind > 0 ? thread.events[ind-1] : nil,
|
||||||
next_ev: ind == count-1 ? nil : thread.events[ind+1],
|
next_ev: ind == count-1 ? nil : thread.events[ind+1],
|
||||||
damus: damus
|
damus_state: damus
|
||||||
)
|
)
|
||||||
.event_context_menu(ev)
|
.event_context_menu(ev)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
@@ -29,7 +29,7 @@ struct ChatroomView: View {
|
|||||||
//dismiss()
|
//dismiss()
|
||||||
toggle_thread_view()
|
toggle_thread_view()
|
||||||
} else {
|
} else {
|
||||||
thread.set_active_event(ev)
|
thread.set_active_event(ev, privkey: damus.keypair.privkey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.environmentObject(thread)
|
.environmentObject(thread)
|
||||||
@@ -39,7 +39,7 @@ struct ChatroomView: View {
|
|||||||
.onReceive(NotificationCenter.default.publisher(for: .select_quote)) { notif in
|
.onReceive(NotificationCenter.default.publisher(for: .select_quote)) { notif in
|
||||||
let ev = notif.object as! NostrEvent
|
let ev = notif.object as! NostrEvent
|
||||||
if ev.id != thread.initial_event.id {
|
if ev.id != thread.initial_event.id {
|
||||||
thread.set_active_event(ev)
|
thread.set_active_event(ev, privkey: damus.keypair.privkey)
|
||||||
}
|
}
|
||||||
scroll_to_event(scroller: scroller, id: ev.id, delay: 0, animate: true, anchor: .top)
|
scroll_to_event(scroller: scroller, id: ev.id, delay: 0, animate: true, anchor: .top)
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@ struct ChatroomView_Previews: PreviewProvider {
|
|||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let state = test_damus_state()
|
let state = test_damus_state()
|
||||||
ChatroomView(damus: state)
|
ChatroomView(damus: state)
|
||||||
.environmentObject(ThreadModel(evid: "&849ab9bb263ed2819db06e05f1a1a3b72878464e8c7146718a2fc1bf1912f893", pool: state.pool))
|
.environmentObject(ThreadModel(evid: "&849ab9bb263ed2819db06e05f1a1a3b72878464e8c7146718a2fc1bf1912f893", pool: state.pool, privkey: state.keypair.privkey))
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
155
damus/Views/DMChatView.swift
Normal file
155
damus/Views/DMChatView.swift
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
//
|
||||||
|
// DMChatView.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2022-06-30.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct DMChatView: View {
|
||||||
|
let damus_state: DamusState
|
||||||
|
let pubkey: String
|
||||||
|
@Binding var events: [NostrEvent]
|
||||||
|
@State var message: String = ""
|
||||||
|
|
||||||
|
var Messages: some View {
|
||||||
|
ScrollViewReader { scroller in
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
ForEach(Array(zip(events, events.indices)), id: \.0.id) { (ev, ind) in
|
||||||
|
DMView(event: events[ind], damus_state: damus_state)
|
||||||
|
.event_context_menu(ev)
|
||||||
|
}
|
||||||
|
Color.white.opacity(0)
|
||||||
|
.id("endblock")
|
||||||
|
.frame(height: 80)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
scroller.scrollTo("endblock")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var Header: some View {
|
||||||
|
let profile = damus_state.profiles.lookup(id: pubkey)
|
||||||
|
let pmodel = ProfileModel(pubkey: pubkey, damus: damus_state)
|
||||||
|
let fmodel = FollowersModel(damus_state: damus_state, target: pubkey)
|
||||||
|
let profile_page = ProfileView(damus_state: damus_state, profile: pmodel, followers: fmodel)
|
||||||
|
return NavigationLink(destination: profile_page) {
|
||||||
|
HStack {
|
||||||
|
ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, image_cache: damus_state.image_cache, profiles: damus_state.profiles)
|
||||||
|
|
||||||
|
ProfileName(pubkey: pubkey, profile: profile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
}
|
||||||
|
|
||||||
|
var InputField: some View {
|
||||||
|
TextField("New Message", text: $message)
|
||||||
|
.padding([.leading], 12)
|
||||||
|
.padding([.top, .bottom], 8)
|
||||||
|
.background {
|
||||||
|
InputBackground()
|
||||||
|
}
|
||||||
|
.foregroundColor(Color.primary)
|
||||||
|
.cornerRadius(20)
|
||||||
|
.padding([.leading, .top, .bottom], 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
|
func InputBackground() -> some View {
|
||||||
|
if colorScheme == .dark {
|
||||||
|
return Color.black.brightness(0.1)
|
||||||
|
} else {
|
||||||
|
return Color.gray.brightness(0.35)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BackgroundColor() -> some View {
|
||||||
|
if colorScheme == .dark {
|
||||||
|
return Color.black.opacity(0.9)
|
||||||
|
} else {
|
||||||
|
return Color.white.opacity(0.9)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var Footer: some View {
|
||||||
|
ZStack {
|
||||||
|
BackgroundColor()
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
InputField
|
||||||
|
|
||||||
|
Button(role: .none, action: send_message) {
|
||||||
|
Label("", systemImage: "arrow.right.circle")
|
||||||
|
.font(.title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: 70)
|
||||||
|
}
|
||||||
|
|
||||||
|
func send_message() {
|
||||||
|
guard let dm = create_dm(message, to_pk: pubkey, keypair: damus_state.keypair) else {
|
||||||
|
print("error creating dm")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
message = ""
|
||||||
|
|
||||||
|
damus_state.pool.send(.event(dm))
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
|
Messages
|
||||||
|
.padding([.top, .leading, .trailing], 10)
|
||||||
|
.dismissKeyboardOnTap()
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Footer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toolbar { Header }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DMChatView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
let ev = NostrEvent(content: "hi", pubkey: "pubkey", kind: 1, tags: [])
|
||||||
|
let evs = Binding<[NostrEvent]>.init(
|
||||||
|
get: { [ev] },
|
||||||
|
set: { _ in })
|
||||||
|
|
||||||
|
DMChatView(damus_state: test_damus_state(), pubkey: "pubkey", events: evs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func create_dm(_ message: String, to_pk: String, keypair: Keypair) -> NostrEvent?
|
||||||
|
{
|
||||||
|
guard let privkey = keypair.privkey else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let tags = [["p", to_pk]]
|
||||||
|
let iv = random_bytes(count: 16).bytes
|
||||||
|
guard let shared_sec = get_shared_secret(privkey: privkey, pubkey: to_pk) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let utf8_message = Data(message.utf8).bytes
|
||||||
|
guard let enc_message = aes_encrypt(data: utf8_message, iv: iv, shared_sec: shared_sec) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let enc_content = encode_dm_base64(content: enc_message.bytes, iv: iv)
|
||||||
|
let ev = NostrEvent(content: enc_content, pubkey: keypair.pubkey, kind: 4, tags: tags)
|
||||||
|
ev.calculate_id()
|
||||||
|
ev.sign(privkey: privkey)
|
||||||
|
return ev
|
||||||
|
}
|
||||||
39
damus/Views/DMView.swift
Normal file
39
damus/Views/DMView.swift
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
//
|
||||||
|
// DMView.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2022-07-01.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct DMView: View {
|
||||||
|
let event: NostrEvent
|
||||||
|
let damus_state: DamusState
|
||||||
|
|
||||||
|
var is_ours: Bool {
|
||||||
|
event.pubkey == damus_state.pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack {
|
||||||
|
if is_ours {
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
|
||||||
|
NoteContentView(privkey: damus_state.keypair.privkey, event: event, profiles: damus_state.profiles, content: event.get_content(damus_state.keypair.privkey))
|
||||||
|
.foregroundColor(is_ours ? Color.white : Color.primary)
|
||||||
|
.padding(10)
|
||||||
|
.background(is_ours ? Color.accentColor : Color.secondary.opacity(0.15))
|
||||||
|
.cornerRadius(8.0)
|
||||||
|
.tint(is_ours ? Color.white : Color.accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DMView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
let ev = NostrEvent(content: "Hey there *buddy*, want to grab some drinks later? 🍻", pubkey: "pubkey", kind: 1, tags: [])
|
||||||
|
DMView(event: ev, damus_state: test_damus_state())
|
||||||
|
}
|
||||||
|
}
|
||||||
52
damus/Views/DirectMessagesView.swift
Normal file
52
damus/Views/DirectMessagesView.swift
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
//
|
||||||
|
// DirectMessagesView.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2022-06-29.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct DirectMessagesView: View {
|
||||||
|
let damus_state: DamusState
|
||||||
|
@Binding var dms: [(String, [NostrEvent])]
|
||||||
|
|
||||||
|
var MainContent: some View {
|
||||||
|
ScrollView {
|
||||||
|
VStack {
|
||||||
|
ForEach(dms, id: \.0) { tup in
|
||||||
|
let evs = Binding<[NostrEvent]>.init(
|
||||||
|
get: { tup.1 },
|
||||||
|
set: { _ in }
|
||||||
|
)
|
||||||
|
let chat = DMChatView(damus_state: damus_state, pubkey: tup.0, events: evs)
|
||||||
|
NavigationLink(destination: chat) {
|
||||||
|
EventView(damus: damus_state, event: tup.1.last!, pubkey: tup.0)
|
||||||
|
}
|
||||||
|
.buttonStyle(PlainButtonStyle())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
MainContent
|
||||||
|
.navigationTitle("Encrypted DMs")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DirectMessagesView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
let ev = NostrEvent(content: "encrypted stuff",
|
||||||
|
pubkey: "pubkey",
|
||||||
|
kind: 4,
|
||||||
|
tags: [])
|
||||||
|
let dms = Binding<[(String, [NostrEvent])]>.init(
|
||||||
|
get: {
|
||||||
|
return [ ("pubkey", [ ev ]) ]
|
||||||
|
},
|
||||||
|
set: { _ in }
|
||||||
|
)
|
||||||
|
DirectMessagesView(damus_state: test_damus_state(), dms: dms)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -73,7 +73,7 @@ struct EventDetailView: View {
|
|||||||
if thread.initial_event.id == ev.id {
|
if thread.initial_event.id == ev.id {
|
||||||
toggle_thread_view()
|
toggle_thread_view()
|
||||||
} else {
|
} else {
|
||||||
thread.set_active_event(ev)
|
thread.set_active_event(ev, privkey: damus.keypair.privkey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear() {
|
.onAppear() {
|
||||||
@@ -88,7 +88,12 @@ struct EventDetailView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollViewReader { proxy in
|
ScrollViewReader { proxy in
|
||||||
ScrollView {
|
ScrollView {
|
||||||
let collapsed_events = calculated_collapsed_events(collapsed: self.collapsed, active: thread.event, events: thread.events)
|
let collapsed_events = calculated_collapsed_events(
|
||||||
|
privkey: damus.keypair.privkey,
|
||||||
|
collapsed: self.collapsed,
|
||||||
|
active: thread.event,
|
||||||
|
events: thread.events
|
||||||
|
)
|
||||||
ForEach(collapsed_events, id: \.id) { cev in
|
ForEach(collapsed_events, id: \.id) { cev in
|
||||||
CollapsedEventView(cev, scroller: proxy)
|
CollapsedEventView(cev, scroller: proxy)
|
||||||
}
|
}
|
||||||
@@ -112,7 +117,7 @@ struct EventDetailView_Previews: PreviewProvider {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/// Find the entire reply path for the active event
|
/// Find the entire reply path for the active event
|
||||||
func make_reply_map(active: NostrEvent, events: [NostrEvent]) -> [String: ()]
|
func make_reply_map(active: NostrEvent, events: [NostrEvent], privkey: String?) -> [String: ()]
|
||||||
{
|
{
|
||||||
let event_map: [String: Int] = zip(events,0...events.count).reduce(into: [:]) { (acc, arg1) in
|
let event_map: [String: Int] = zip(events,0...events.count).reduce(into: [:]) { (acc, arg1) in
|
||||||
let (ev, i) = arg1
|
let (ev, i) = arg1
|
||||||
@@ -129,7 +134,8 @@ func make_reply_map(active: NostrEvent, events: [NostrEvent]) -> [String: ()]
|
|||||||
|
|
||||||
for ev in events {
|
for ev in events {
|
||||||
/// does this event reply to the active event?
|
/// does this event reply to the active event?
|
||||||
for ev_ref in ev.event_refs {
|
let ev_refs = ev.event_refs(privkey)
|
||||||
|
for ev_ref in ev_refs {
|
||||||
if let reply = ev_ref.is_reply {
|
if let reply = ev_ref.is_reply {
|
||||||
if reply.ref_id == active.id {
|
if reply.ref_id == active.id {
|
||||||
is_reply[ev.id] = ()
|
is_reply[ev.id] = ()
|
||||||
@@ -139,7 +145,8 @@ func make_reply_map(active: NostrEvent, events: [NostrEvent]) -> [String: ()]
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// does the active event reply to this event?
|
/// does the active event reply to this event?
|
||||||
for active_ref in active.event_refs {
|
let active_refs = active.event_refs(privkey)
|
||||||
|
for active_ref in active_refs {
|
||||||
if let reply = active_ref.is_reply {
|
if let reply = active_ref.is_reply {
|
||||||
if reply.ref_id == ev.id {
|
if reply.ref_id == ev.id {
|
||||||
is_reply[ev.id] = ()
|
is_reply[ev.id] = ()
|
||||||
@@ -194,14 +201,14 @@ func determine_highlight(reply_map: [String: ()], current: NostrEvent, active: N
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func calculated_collapsed_events(collapsed: Bool, active: NostrEvent?, events: [NostrEvent]) -> [CollapsedEvent] {
|
func calculated_collapsed_events(privkey: String?, collapsed: Bool, active: NostrEvent?, events: [NostrEvent]) -> [CollapsedEvent] {
|
||||||
var count: Int = 0
|
var count: Int = 0
|
||||||
|
|
||||||
guard let active = active else {
|
guard let active = active else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
let reply_map = make_reply_map(active: active, events: events)
|
let reply_map = make_reply_map(active: active, events: events, privkey: privkey)
|
||||||
|
|
||||||
if !collapsed {
|
if !collapsed {
|
||||||
return events.reduce(into: []) { acc, ev in
|
return events.reduce(into: []) { acc, ev in
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ enum Highlight {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
var is_none: Bool {
|
var is_none: Bool {
|
||||||
if case .none = self {
|
if case .none = self {
|
||||||
return true
|
return true
|
||||||
@@ -41,9 +41,34 @@ struct EventView: View {
|
|||||||
let highlight: Highlight
|
let highlight: Highlight
|
||||||
let has_action_bar: Bool
|
let has_action_bar: Bool
|
||||||
let damus: DamusState
|
let damus: DamusState
|
||||||
|
let pubkey: String
|
||||||
|
|
||||||
@EnvironmentObject var action_bar: ActionBarModel
|
@EnvironmentObject var action_bar: ActionBarModel
|
||||||
|
|
||||||
|
init(event: NostrEvent, highlight: Highlight, has_action_bar: Bool, damus: DamusState) {
|
||||||
|
self.event = event
|
||||||
|
self.highlight = highlight
|
||||||
|
self.has_action_bar = has_action_bar
|
||||||
|
self.damus = damus
|
||||||
|
self.pubkey = event.pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
init(damus: DamusState, event: NostrEvent) {
|
||||||
|
self.event = event
|
||||||
|
self.highlight = .none
|
||||||
|
self.has_action_bar = false
|
||||||
|
self.damus = damus
|
||||||
|
self.pubkey = event.pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
init(damus: DamusState, event: NostrEvent, pubkey: String) {
|
||||||
|
self.event = event
|
||||||
|
self.highlight = .none
|
||||||
|
self.has_action_bar = false
|
||||||
|
self.damus = damus
|
||||||
|
self.pubkey = pubkey
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
return Group {
|
return Group {
|
||||||
if event.known_kind == .boost, let inner_ev = event.inner_event {
|
if event.known_kind == .boost, let inner_ev = event.inner_event {
|
||||||
@@ -51,7 +76,7 @@ struct EventView: View {
|
|||||||
HStack {
|
HStack {
|
||||||
Label("", systemImage: "arrow.2.squarepath")
|
Label("", systemImage: "arrow.2.squarepath")
|
||||||
.foregroundColor(Color.gray)
|
.foregroundColor(Color.gray)
|
||||||
ProfileName(pubkey: event.pubkey, profile: damus.profiles.lookup(id: event.pubkey))
|
ProfileName(pubkey: pubkey, profile: damus.profiles.lookup(id: pubkey))
|
||||||
.foregroundColor(Color.gray)
|
.foregroundColor(Color.gray)
|
||||||
Text(" Boosted")
|
Text(" Boosted")
|
||||||
.foregroundColor(Color.gray)
|
.foregroundColor(Color.gray)
|
||||||
@@ -63,16 +88,17 @@ struct EventView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TextEvent(_ event: NostrEvent) -> some View {
|
func TextEvent(_ event: NostrEvent) -> some View {
|
||||||
|
let content = event.get_content(damus.keypair.privkey)
|
||||||
return HStack(alignment: .top) {
|
return HStack(alignment: .top) {
|
||||||
let profile = damus.profiles.lookup(id: event.pubkey)
|
let profile = damus.profiles.lookup(id: pubkey)
|
||||||
VStack {
|
VStack {
|
||||||
let pmodel = ProfileModel(pubkey: event.pubkey, damus: damus)
|
let pmodel = ProfileModel(pubkey: pubkey, damus: damus)
|
||||||
let pv = ProfileView(damus_state: damus, profile: pmodel, followers: FollowersModel(damus_state: damus, target: event.pubkey))
|
let pv = ProfileView(damus_state: damus, profile: pmodel, followers: FollowersModel(damus_state: damus, target: pubkey))
|
||||||
|
|
||||||
NavigationLink(destination: pv) {
|
NavigationLink(destination: pv) {
|
||||||
ProfilePicView(pubkey: event.pubkey, size: PFP_SIZE, highlight: highlight, image_cache: damus.image_cache, profiles: damus.profiles)
|
ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: highlight, image_cache: damus.image_cache, profiles: damus.profiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -80,19 +106,19 @@ struct EventView: View {
|
|||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
ProfileName(pubkey: event.pubkey, profile: profile)
|
ProfileName(pubkey: pubkey, profile: profile)
|
||||||
Text("\(format_relative_time(event.created_at))")
|
Text("\(format_relative_time(event.created_at))")
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.is_reply {
|
if event.is_reply(damus.keypair.privkey) {
|
||||||
Text("\(reply_desc(profiles: damus.profiles, event: event))")
|
Text("\(reply_desc(profiles: damus.profiles, event: event))")
|
||||||
.font(.footnote)
|
.font(.footnote)
|
||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
}
|
}
|
||||||
|
|
||||||
NoteContentView(event: event, profiles: damus.profiles, content: event.content)
|
NoteContentView(privkey: damus.keypair.privkey, event: event, profiles: damus.profiles, content: content)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.textSelection(.enabled)
|
.textSelection(.enabled)
|
||||||
|
|
||||||
@@ -122,7 +148,7 @@ extension View {
|
|||||||
} label: {
|
} label: {
|
||||||
Label("Copy Text", systemImage: "doc.on.doc")
|
Label("Copy Text", systemImage: "doc.on.doc")
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
UIPasteboard.general.string = "@" + event.pubkey
|
UIPasteboard.general.string = "@" + event.pubkey
|
||||||
} label: {
|
} label: {
|
||||||
@@ -134,20 +160,20 @@ extension View {
|
|||||||
} label: {
|
} label: {
|
||||||
Label("Copy Note ID", systemImage: "tag")
|
Label("Copy Note ID", systemImage: "tag")
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
UIPasteboard.general.string = event_to_json(ev: event)
|
UIPasteboard.general.string = event_to_json(ev: event)
|
||||||
} label: {
|
} label: {
|
||||||
Label("Copy Note", systemImage: "note")
|
Label("Copy Note", systemImage: "note")
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
NotificationCenter.default.post(name: .broadcast_event, object: event)
|
NotificationCenter.default.post(name: .broadcast_event, object: event)
|
||||||
} label: {
|
} label: {
|
||||||
Label("Broadcast", systemImage: "globe")
|
Label("Broadcast", systemImage: "globe")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,16 +186,16 @@ func reply_desc(profiles: Profiles, event: NostrEvent) -> String {
|
|||||||
let desc = make_reply_description(event.tags)
|
let desc = make_reply_description(event.tags)
|
||||||
let pubkeys = desc.pubkeys
|
let pubkeys = desc.pubkeys
|
||||||
let n = desc.others
|
let n = desc.others
|
||||||
|
|
||||||
if desc.pubkeys.count == 0 {
|
if desc.pubkeys.count == 0 {
|
||||||
return "Reply to self"
|
return "Reply to self"
|
||||||
}
|
}
|
||||||
|
|
||||||
let names: [String] = pubkeys.map {
|
let names: [String] = pubkeys.map {
|
||||||
let prof = profiles.lookup(id: $0)
|
let prof = profiles.lookup(id: $0)
|
||||||
return Profile.displayName(profile: prof, pubkey: $0)
|
return Profile.displayName(profile: prof, pubkey: $0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if names.count == 2 {
|
if names.count == 2 {
|
||||||
if n > 2 {
|
if n > 2 {
|
||||||
let and_other = reply_others_desc(n: n, n_pubkeys: pubkeys.count)
|
let and_other = reply_others_desc(n: n, n_pubkeys: pubkeys.count)
|
||||||
@@ -177,7 +203,7 @@ func reply_desc(profiles: Profiles, event: NostrEvent) -> String {
|
|||||||
}
|
}
|
||||||
return "Replying to \(names[0]) & \(names[1])"
|
return "Replying to \(names[0]) & \(names[1])"
|
||||||
}
|
}
|
||||||
|
|
||||||
let and_other = reply_others_desc(n: n, n_pubkeys: pubkeys.count)
|
let and_other = reply_others_desc(n: n, n_pubkeys: pubkeys.count)
|
||||||
return "Replying to \(names[0])\(and_other)"
|
return "Replying to \(names[0])\(and_other)"
|
||||||
}
|
}
|
||||||
@@ -197,7 +223,7 @@ func make_actionbar_model(ev: NostrEvent, damus: DamusState) -> ActionBarModel {
|
|||||||
let our_like = damus.likes.our_events[ev.id]
|
let our_like = damus.likes.our_events[ev.id]
|
||||||
let our_boost = damus.boosts.our_events[ev.id]
|
let our_boost = damus.boosts.our_events[ev.id]
|
||||||
let our_tip = damus.tips.our_tips[ev.id]
|
let our_tip = damus.tips.our_tips[ev.id]
|
||||||
|
|
||||||
return ActionBarModel(likes: likes ?? 0,
|
return ActionBarModel(likes: likes ?? 0,
|
||||||
boosts: boosts ?? 0,
|
boosts: boosts ?? 0,
|
||||||
tips: tips ?? 0,
|
tips: tips ?? 0,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ enum Timeline: String, CustomStringConvertible {
|
|||||||
case home
|
case home
|
||||||
case notifications
|
case notifications
|
||||||
case search
|
case search
|
||||||
|
case dms
|
||||||
|
|
||||||
var description: String {
|
var description: String {
|
||||||
return self.rawValue
|
return self.rawValue
|
||||||
@@ -76,6 +77,7 @@ struct TabBar: View {
|
|||||||
Divider()
|
Divider()
|
||||||
HStack {
|
HStack {
|
||||||
TabButton(timeline: .home, img: "house", selected: $selected, new_events: $new_events, action: action)
|
TabButton(timeline: .home, img: "house", selected: $selected, new_events: $new_events, action: action)
|
||||||
|
TabButton(timeline: .dms, img: "bubble.left.and.bubble.right", selected: $selected, new_events: $new_events, action: action)
|
||||||
TabButton(timeline: .search, img: "magnifyingglass.circle", selected: $selected, new_events: $new_events, action: action)
|
TabButton(timeline: .search, img: "magnifyingglass.circle", selected: $selected, new_events: $new_events, action: action)
|
||||||
TabButton(timeline: .notifications, img: "bell", selected: $selected, new_events: $new_events, action: action)
|
TabButton(timeline: .notifications, img: "bell", selected: $selected, new_events: $new_events, action: action)
|
||||||
}
|
}
|
||||||
@@ -83,3 +85,5 @@ struct TabBar: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,9 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
|
|
||||||
func render_note_content(ev: NostrEvent, profiles: Profiles) -> String {
|
func render_note_content(ev: NostrEvent, profiles: Profiles, privkey: String?) -> String {
|
||||||
return ev.blocks.reduce("") { str, block in
|
let blocks = ev.blocks(privkey)
|
||||||
|
return blocks.reduce("") { str, block in
|
||||||
switch block {
|
switch block {
|
||||||
case .mention(let m):
|
case .mention(let m):
|
||||||
return str + mention_str(m, profiles: profiles)
|
return str + mention_str(m, profiles: profiles)
|
||||||
@@ -22,6 +23,7 @@ func render_note_content(ev: NostrEvent, profiles: Profiles) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct NoteContentView: View {
|
struct NoteContentView: View {
|
||||||
|
let privkey: String?
|
||||||
let event: NostrEvent
|
let event: NostrEvent
|
||||||
let profiles: Profiles
|
let profiles: Profiles
|
||||||
|
|
||||||
@@ -31,8 +33,8 @@ struct NoteContentView: View {
|
|||||||
let md_opts: AttributedString.MarkdownParsingOptions =
|
let md_opts: AttributedString.MarkdownParsingOptions =
|
||||||
.init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
|
.init(interpretedSyntax: .inlineOnlyPreservingWhitespace)
|
||||||
|
|
||||||
guard let txt = try? AttributedString(markdown: content, options: md_opts) else {
|
guard var txt = try? AttributedString(markdown: content, options: md_opts) else {
|
||||||
return Text(event.content)
|
return Text(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Text(txt)
|
return Text(txt)
|
||||||
@@ -41,15 +43,16 @@ struct NoteContentView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
MainContent()
|
MainContent()
|
||||||
.onAppear() {
|
.onAppear() {
|
||||||
self.content = render_note_content(ev: event, profiles: profiles)
|
self.content = render_note_content(ev: event, profiles: profiles, privkey: privkey)
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.profile_updated)) { notif in
|
.onReceive(handle_notify(.profile_updated)) { notif in
|
||||||
let profile = notif.object as! ProfileUpdate
|
let profile = notif.object as! ProfileUpdate
|
||||||
for block in event.blocks {
|
let blocks = event.blocks(privkey)
|
||||||
|
for block in blocks {
|
||||||
switch block {
|
switch block {
|
||||||
case .mention(let m):
|
case .mention(let m):
|
||||||
if m.type == .pubkey && m.ref.ref_id == profile.pubkey {
|
if m.type == .pubkey && m.ref.ref_id == profile.pubkey {
|
||||||
content = render_note_content(ev: event, profiles: profiles)
|
content = render_note_content(ev: event, profiles: profiles, privkey: privkey)
|
||||||
}
|
}
|
||||||
case .text: return
|
case .text: return
|
||||||
case .hashtag: return
|
case .hashtag: return
|
||||||
|
|||||||
@@ -45,6 +45,27 @@ func follow_btn_enabled_state(_ fs: FollowState) -> Bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ProfileNameView: View {
|
||||||
|
let pubkey: String
|
||||||
|
let profile: Profile?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if let real_name = profile?.display_name {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(real_name)
|
||||||
|
.font(.title)
|
||||||
|
ProfileName(pubkey: pubkey, profile: profile, prefix: "@")
|
||||||
|
.font(.callout)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ProfileName(pubkey: pubkey, profile: profile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ProfileView: View {
|
struct ProfileView: View {
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
|
|
||||||
@@ -60,19 +81,8 @@ struct ProfileView: View {
|
|||||||
|
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
ProfilePicView(pubkey: profile.pubkey, size: PFP_SIZE, highlight: .custom(Color.black, 2), image_cache: damus_state.image_cache, profiles: damus_state.profiles)
|
ProfilePicView(pubkey: profile.pubkey, size: PFP_SIZE, highlight: .custom(Color.black, 2), image_cache: damus_state.image_cache, profiles: damus_state.profiles)
|
||||||
|
|
||||||
if let real_name = data?.display_name {
|
ProfileNameView(pubkey: profile.pubkey, profile: data)
|
||||||
VStack(alignment: .leading) {
|
|
||||||
Text(real_name)
|
|
||||||
.font(.title)
|
|
||||||
ProfileName(pubkey: profile.pubkey, profile: data, prefix: "@")
|
|
||||||
.font(.callout)
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ProfileName(pubkey: profile.pubkey, profile: data)
|
|
||||||
}
|
|
||||||
//.border(Color.green)
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ReplyQuoteView: View {
|
struct ReplyQuoteView: View {
|
||||||
|
let privkey: String?
|
||||||
let quoter: NostrEvent
|
let quoter: NostrEvent
|
||||||
let event_id: String
|
let event_id: String
|
||||||
let image_cache: ImageCache
|
let image_cache: ImageCache
|
||||||
@@ -31,7 +32,7 @@ struct ReplyQuoteView: View {
|
|||||||
.foregroundColor(.gray)
|
.foregroundColor(.gray)
|
||||||
}
|
}
|
||||||
|
|
||||||
NoteContentView(event: event, profiles: profiles, content: event.content)
|
NoteContentView(privkey: privkey, event: event, profiles: profiles, content: event.content)
|
||||||
.font(.callout)
|
.font(.callout)
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ struct ReplyQuoteView_Previews: PreviewProvider {
|
|||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
let s = test_damus_state()
|
let s = test_damus_state()
|
||||||
let quoter = NostrEvent(content: "a\nb\nc", pubkey: "pubkey")
|
let quoter = NostrEvent(content: "a\nb\nc", pubkey: "pubkey")
|
||||||
ReplyQuoteView(quoter: quoter, event_id: "pubkey2", image_cache: s.image_cache, profiles: s.profiles)
|
ReplyQuoteView(privkey: s.keypair.privkey, quoter: quoter, event_id: "pubkey2", image_cache: s.image_cache, profiles: s.profiles)
|
||||||
.environmentObject(ThreadModel(event: quoter, pool: s.pool))
|
.environmentObject(ThreadModel(event: quoter, pool: s.pool, privkey: s.keypair.privkey))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ struct InnerTimelineView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
LazyVStack {
|
LazyVStack {
|
||||||
ForEach(events, id: \.id) { (ev: NostrEvent) in
|
ForEach(events, id: \.id) { (ev: NostrEvent) in
|
||||||
let tv = ThreadView(thread: ThreadModel(event: ev, pool: damus.pool), damus: damus)
|
let tv = ThreadView(thread: ThreadModel(event: ev, pool: damus.pool, privkey: damus.keypair.privkey), damus: damus)
|
||||||
|
|
||||||
NavigationLink(destination: tv) {
|
NavigationLink(destination: tv) {
|
||||||
EventView(event: ev, highlight: .none, has_action_bar: true, damus: damus)
|
EventView(event: ev, highlight: .none, has_action_bar: true, damus: damus)
|
||||||
|
|||||||
Reference in New Issue
Block a user