Browse Source

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>
profiles-everywhere
William Casarin 3 years ago
parent
commit
c122035851
  1. 52
      damus.xcodeproj/project.pbxproj
  2. 5
      damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
  3. 7
      damus/ContentView.swift
  4. 4
      damus/Models/Contacts.swift
  5. 15
      damus/Models/DirectMessagesModel.swift
  6. 4
      damus/Models/EventRef.swift
  7. 90
      damus/Models/HomeModel.swift
  8. 33
      damus/Models/ThreadModel.swift
  9. 229
      damus/Nostr/NostrEvent.swift
  10. 1
      damus/Nostr/NostrKind.swift
  11. 39
      damus/Util/InputDismissKeyboard.swift
  12. 4
      damus/Util/Keys.swift
  13. 40
      damus/Views/ChatView.swift
  14. 8
      damus/Views/ChatroomView.swift
  15. 155
      damus/Views/DMChatView.swift
  16. 39
      damus/Views/DMView.swift
  17. 52
      damus/Views/DirectMessagesView.swift
  18. 21
      damus/Views/EventDetailView.swift
  19. 42
      damus/Views/EventView.swift
  20. 4
      damus/Views/MainTabView.swift
  21. 17
      damus/Views/NoteContentView.swift
  22. 34
      damus/Views/ProfileView.swift
  23. 7
      damus/Views/ReplyQuoteView.swift
  24. 2
      damus/Views/TimelineView.swift

52
damus.xcodeproj/project.pbxproj

@ -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,35 +1011,35 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */
4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */ = { 4C64987F286E0EE300EAE2B3 /* XCRemoteSwiftPackageReference "secp256k1" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/daltoniam/Starscream"; repositoryURL = "https://github.com/jb55/secp256k1.swift";
requirement = { requirement = {
kind = upToNextMajorVersion; kind = revision;
minimumVersion = 4.0.0; revision = 40b4b38b3b1c83f7088c76189a742870e0ca06a9;
}; };
}; };
4CEE2AE92805AEA300AB5EEF /* XCRemoteSwiftPackageReference "secp256k1" */ = { 4CE6DF1027F7A2B300C66700 /* XCRemoteSwiftPackageReference "Starscream" */ = {
isa = XCRemoteSwiftPackageReference; isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/GigaBitcoin/secp256k1.swift"; repositoryURL = "https://github.com/daltoniam/Starscream";
requirement = { requirement = {
kind = upToNextMajorVersion; kind = upToNextMajorVersion;
minimumVersion = 0.5.0; minimumVersion = 4.0.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 */;

5
damus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

@ -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"
} }
}, },
{ {

7
damus/ContentView.swift

@ -122,6 +122,9 @@ struct ContentView: View {
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)

4
damus/Models/Contacts.swift

@ -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

@ -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
}

4
damus/Models/EventRef.swift

@ -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
} }
} }

90
damus/Models/HomeModel.swift

@ -44,6 +44,7 @@ class HomeModel: ObservableObject {
@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()
@ -78,16 +79,26 @@ class HomeModel: ObservableObject {
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_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev)
case .metadata:
handle_metadata_event(ev) handle_metadata_event(ev)
} else if ev.kind == 6 { case .boost:
handle_boost_event(sub_id: sub_id, ev) handle_boost_event(sub_id: sub_id, ev)
} else if ev.kind == 7 { case .like:
handle_like_event(ev) handle_like_event(ev)
} else if ev.kind == 3 { case .dm:
handle_contact_event(sub_id: sub_id, relay_id: relay_id, ev: ev) handle_dm(ev)
case .delete:
break
} }
} }
@ -191,7 +202,7 @@ 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
@ -229,6 +240,20 @@ class HomeModel: ObservableObject {
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,
@ -250,23 +275,27 @@ 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)))
} }
} }
@ -318,13 +347,56 @@ class HomeModel: ObservableObject {
} }
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
}
}
}
} }

33
damus/Models/ThreadModel.swift

@ -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)
} }
} }

229
damus/Nostr/NostrEvent.swift

@ -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
@ -54,17 +56,67 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible {
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) } ?? "?"
@ -92,8 +144,8 @@ 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)
} }
@ -129,8 +181,8 @@ 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] {
@ -177,11 +229,11 @@ class NostrEvent: Codable, Identifiable, CustomStringConvertible {
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)
@ -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)
}

1
damus/Nostr/NostrKind.swift

@ -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

@ -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)
}
}

4
damus/Util/Keys.swift

@ -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) {

40
damus/Views/ChatView.swift

@ -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
}

8
damus/Views/ChatroomView.swift

@ -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

@ -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

@ -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

@ -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)
}
}

21
damus/Views/EventDetailView.swift

@ -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

42
damus/Views/EventView.swift

@ -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)
@ -65,14 +90,15 @@ 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)

4
damus/Views/MainTabView.swift

@ -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 {
} }
} }

17
damus/Views/NoteContentView.swift

@ -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

34
damus/Views/ProfileView.swift

@ -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
@ -61,18 +82,7 @@ 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()

7
damus/Views/ReplyQuoteView.swift

@ -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))
} }
} }

2
damus/Views/TimelineView.swift

@ -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)

Loading…
Cancel
Save