mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Further work on Id Blinding
Renamed the setter for the SOGS 'Server' object for consistency Updated the Curve25519Kit repo to use an Oxen fork Updated the MockDataGenerator to accomodate the latest changes Updated the ConversationVC to better support getting replaced when the conversion from blinded to unblinded happens while on that screen Added a cache for the mapping between blinded ids and standard ids (gets cached whenever a valid match is found) Added a migration to remove the old 'authToken, 'lastMessageServerId' and 'lastDeletionServerId' collections (redundant in SOGS V4)
This commit is contained in:
parent
3e97782d18
commit
a26ee12f8d
24 changed files with 618 additions and 132 deletions
3
Podfile
3
Podfile
|
@ -24,8 +24,7 @@ abstract_target 'GlobalDependencies' do
|
||||||
|
|
||||||
# Dependencies to be included only in all extensions/frameworks
|
# Dependencies to be included only in all extensions/frameworks
|
||||||
abstract_target 'FrameworkAndExtensionDependencies' do
|
abstract_target 'FrameworkAndExtensionDependencies' do
|
||||||
# TODO: Swap this to use an oxen-io fork
|
pod 'Curve25519Kit', git: 'https://github.com/oxen-io/session-ios-curve-25519-kit.git', branch: 'session-version'
|
||||||
pod 'Curve25519Kit', git: 'https://github.com/mpretty-cyro/session-ios-curve-25519-kit.git', branch: 'session'
|
|
||||||
pod 'SignalCoreKit', git: 'https://github.com/oxen-io/session-ios-core-kit', branch: 'session-version'
|
pod 'SignalCoreKit', git: 'https://github.com/oxen-io/session-ios-core-kit', branch: 'session-version'
|
||||||
|
|
||||||
target 'SessionNotificationServiceExtension'
|
target 'SessionNotificationServiceExtension'
|
||||||
|
|
12
Podfile.lock
12
Podfile.lock
|
@ -123,7 +123,7 @@ PODS:
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- AFNetworking
|
- AFNetworking
|
||||||
- CryptoSwift
|
- CryptoSwift
|
||||||
- Curve25519Kit (from `https://github.com/mpretty-cyro/session-ios-curve-25519-kit.git`, branch `session`)
|
- Curve25519Kit (from `https://github.com/oxen-io/session-ios-curve-25519-kit.git`, branch `session-version`)
|
||||||
- Mantle (from `https://github.com/signalapp/Mantle`, branch `signal-master`)
|
- Mantle (from `https://github.com/signalapp/Mantle`, branch `signal-master`)
|
||||||
- Nimble
|
- Nimble
|
||||||
- NVActivityIndicatorView
|
- NVActivityIndicatorView
|
||||||
|
@ -156,8 +156,8 @@ SPEC REPOS:
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
Curve25519Kit:
|
Curve25519Kit:
|
||||||
:branch: session
|
:branch: session-version
|
||||||
:git: https://github.com/mpretty-cyro/session-ios-curve-25519-kit.git
|
:git: https://github.com/oxen-io/session-ios-curve-25519-kit.git
|
||||||
Mantle:
|
Mantle:
|
||||||
:branch: signal-master
|
:branch: signal-master
|
||||||
:git: https://github.com/signalapp/Mantle
|
:git: https://github.com/signalapp/Mantle
|
||||||
|
@ -175,8 +175,8 @@ EXTERNAL SOURCES:
|
||||||
|
|
||||||
CHECKOUT OPTIONS:
|
CHECKOUT OPTIONS:
|
||||||
Curve25519Kit:
|
Curve25519Kit:
|
||||||
:commit: a23049232dc6c18928cdacfbcef287dad954c5c6
|
:commit: b79c2ace600bfd3784e9c33cf1f254b121312edc
|
||||||
:git: https://github.com/mpretty-cyro/session-ios-curve-25519-kit.git
|
:git: https://github.com/oxen-io/session-ios-curve-25519-kit.git
|
||||||
Mantle:
|
Mantle:
|
||||||
:commit: e7e46253bb01ce39525d90aa69ed9e85e758bfc4
|
:commit: e7e46253bb01ce39525d90aa69ed9e85e758bfc4
|
||||||
:git: https://github.com/signalapp/Mantle
|
:git: https://github.com/signalapp/Mantle
|
||||||
|
@ -214,6 +214,6 @@ SPEC CHECKSUMS:
|
||||||
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
|
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
|
||||||
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb
|
||||||
|
|
||||||
PODFILE CHECKSUM: 918ef11baf24eac2df681cd6a3781f536f9d384a
|
PODFILE CHECKSUM: 2cc64d50f25c3b1627c3e958ae50e25fead25564
|
||||||
|
|
||||||
COCOAPODS: 1.11.2
|
COCOAPODS: 1.11.2
|
||||||
|
|
|
@ -771,6 +771,8 @@
|
||||||
F5765D284BC6ECAC0C1D33F0 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4A93ECA93B3DE800CC7D7F6 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework */; };
|
F5765D284BC6ECAC0C1D33F0 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4A93ECA93B3DE800CC7D7F6 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework */; };
|
||||||
FC3BD9881A30A790005B96BB /* Social.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC3BD9871A30A790005B96BB /* Social.framework */; };
|
FC3BD9881A30A790005B96BB /* Social.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC3BD9871A30A790005B96BB /* Social.framework */; };
|
||||||
FCB11D8C1A129A76002F93FB /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCB11D8B1A129A76002F93FB /* CoreMedia.framework */; };
|
FCB11D8C1A129A76002F93FB /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCB11D8B1A129A76002F93FB /* CoreMedia.framework */; };
|
||||||
|
FD0BA51B27CD88EC00CC6805 /* BlindedIdMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0BA51A27CD88EC00CC6805 /* BlindedIdMapping.swift */; };
|
||||||
|
FD0BA51D27CDC34600CC6805 /* SOGSV4Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD0BA51C27CDC34600CC6805 /* SOGSV4Migration.swift */; };
|
||||||
FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */; };
|
FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */; };
|
||||||
FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */; };
|
FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */; };
|
||||||
FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201D27B0D87C00FEA984 /* SessionId.swift */; };
|
FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201D27B0D87C00FEA984 /* SessionId.swift */; };
|
||||||
|
@ -1906,6 +1908,8 @@
|
||||||
F9BBF530D71905BA9007675F /* Pods-SessionShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionShareExtension/Pods-SessionShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
|
F9BBF530D71905BA9007675F /* Pods-SessionShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionShareExtension/Pods-SessionShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
FC3BD9871A30A790005B96BB /* Social.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Social.framework; path = System/Library/Frameworks/Social.framework; sourceTree = SDKROOT; };
|
FC3BD9871A30A790005B96BB /* Social.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Social.framework; path = System/Library/Frameworks/Social.framework; sourceTree = SDKROOT; };
|
||||||
FCB11D8B1A129A76002F93FB /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; };
|
FCB11D8B1A129A76002F93FB /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; };
|
||||||
|
FD0BA51A27CD88EC00CC6805 /* BlindedIdMapping.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlindedIdMapping.swift; sourceTree = "<group>"; };
|
||||||
|
FD0BA51C27CDC34600CC6805 /* SOGSV4Migration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSV4Migration.swift; sourceTree = "<group>"; };
|
||||||
FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = "<group>"; };
|
FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = "<group>"; };
|
||||||
FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = "<group>"; };
|
FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = "<group>"; };
|
||||||
FD5D201D27B0D87C00FEA984 /* SessionId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionId.swift; sourceTree = "<group>"; };
|
FD5D201D27B0D87C00FEA984 /* SessionId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionId.swift; sourceTree = "<group>"; };
|
||||||
|
@ -2564,6 +2568,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
B8B32020258B1A650020074B /* Contact.swift */,
|
B8B32020258B1A650020074B /* Contact.swift */,
|
||||||
|
FD0BA51A27CD88EC00CC6805 /* BlindedIdMapping.swift */,
|
||||||
);
|
);
|
||||||
path = Contacts;
|
path = Contacts;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -3237,6 +3242,7 @@
|
||||||
children = (
|
children = (
|
||||||
B8B32044258C117C0020074B /* ContactsMigration.swift */,
|
B8B32044258C117C0020074B /* ContactsMigration.swift */,
|
||||||
FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */,
|
FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */,
|
||||||
|
FD0BA51C27CDC34600CC6805 /* SOGSV4Migration.swift */,
|
||||||
C38EF271255B6D79007E1867 /* OWSDatabaseMigration.h */,
|
C38EF271255B6D79007E1867 /* OWSDatabaseMigration.h */,
|
||||||
C38EF270255B6D79007E1867 /* OWSDatabaseMigration.m */,
|
C38EF270255B6D79007E1867 /* OWSDatabaseMigration.m */,
|
||||||
C38EF26F255B6D79007E1867 /* OWSDatabaseMigrationRunner.h */,
|
C38EF26F255B6D79007E1867 /* OWSDatabaseMigrationRunner.h */,
|
||||||
|
@ -4988,6 +4994,7 @@
|
||||||
C33FDC98255A582000E217F9 /* SwiftSingletons.swift in Sources */,
|
C33FDC98255A582000E217F9 /* SwiftSingletons.swift in Sources */,
|
||||||
C33FDC27255A581F00E217F9 /* YapDatabase+Promise.swift in Sources */,
|
C33FDC27255A581F00E217F9 /* YapDatabase+Promise.swift in Sources */,
|
||||||
C33FDCD3255A582000E217F9 /* GroupUtilities.swift in Sources */,
|
C33FDCD3255A582000E217F9 /* GroupUtilities.swift in Sources */,
|
||||||
|
FD0BA51D27CDC34600CC6805 /* SOGSV4Migration.swift in Sources */,
|
||||||
FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */,
|
FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */,
|
||||||
C38EF326255B6DBF007E1867 /* ConversationStyle.swift in Sources */,
|
C38EF326255B6DBF007E1867 /* ConversationStyle.swift in Sources */,
|
||||||
C38EF3B8255B6DE7007E1867 /* ImageEditorTextViewController.swift in Sources */,
|
C38EF3B8255B6DE7007E1867 /* ImageEditorTextViewController.swift in Sources */,
|
||||||
|
@ -5117,6 +5124,7 @@
|
||||||
C3C2A74D2553A39700C340D1 /* VisibleMessage.swift in Sources */,
|
C3C2A74D2553A39700C340D1 /* VisibleMessage.swift in Sources */,
|
||||||
C32C5AAD256DBE8F003C73A2 /* TSInfoMessage.m in Sources */,
|
C32C5AAD256DBE8F003C73A2 /* TSInfoMessage.m in Sources */,
|
||||||
FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */,
|
FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */,
|
||||||
|
FD0BA51B27CD88EC00CC6805 /* BlindedIdMapping.swift in Sources */,
|
||||||
FDC4386927B4E6B800C60D73 /* String+Utlities.swift in Sources */,
|
FDC4386927B4E6B800C60D73 /* String+Utlities.swift in Sources */,
|
||||||
C32C5A13256DB7A5003C73A2 /* PushNotificationAPI.swift in Sources */,
|
C32C5A13256DB7A5003C73A2 /* PushNotificationAPI.swift in Sources */,
|
||||||
FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */,
|
FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */,
|
||||||
|
|
|
@ -2,6 +2,7 @@ import UIKit
|
||||||
import CoreServices
|
import CoreServices
|
||||||
import Photos
|
import Photos
|
||||||
import PhotosUI
|
import PhotosUI
|
||||||
|
import Sodium
|
||||||
import SessionUtilitiesKit
|
import SessionUtilitiesKit
|
||||||
import SignalUtilitiesKit
|
import SignalUtilitiesKit
|
||||||
|
|
||||||
|
@ -813,6 +814,83 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
|
||||||
userDetailsSheet.modalTransitionStyle = .crossDissolve
|
userDetailsSheet.modalTransitionStyle = .crossDissolve
|
||||||
present(userDetailsSheet, animated: true, completion: nil)
|
present(userDetailsSheet, animated: true, completion: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func startThread(with sessionId: String, openGroupServer: String, openGroupPublicKey: String) {
|
||||||
|
// If the sessionId is blinded then check if there is an existing un-blinded thread with the contact
|
||||||
|
if SessionId.Prefix(from: sessionId) == .blinded {
|
||||||
|
// TODO: Ensure the above case isn't going to be an issue due to legacy messages?
|
||||||
|
// Unfortunately the whole point of id-blinding is to make it hard to reverse-engineer a standard
|
||||||
|
// sessionId, as a result in order to see if there is an unblinded contact for this blindedId we
|
||||||
|
// can only really generate blinded ids for each contact and check if any match
|
||||||
|
//
|
||||||
|
// Due to this we have made a few optimisations to try and early-out as often as possible, first
|
||||||
|
// we try to retrieve a direct cached mapping
|
||||||
|
if let mapping: BlindedIdMapping = Storage.shared.getBlindedIdMapping(with: sessionId) {
|
||||||
|
let thread: TSContactThread = TSContactThread.getOrCreateThread(contactSessionID: mapping.sessionId)
|
||||||
|
let conversationVC: ConversationVC = ConversationVC(thread: thread)
|
||||||
|
|
||||||
|
self.navigationController?.pushViewController(conversationVC, animated: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var didFindContact: Bool = false
|
||||||
|
|
||||||
|
// Then we try loop through all approved contact threads to see if one of those contacts can be blinded to match
|
||||||
|
ContactUtilities.enumerateApprovedContactThreads { contactThread, contact, stop in
|
||||||
|
guard Sodium().sessionId(contact.sessionID, matchesBlindedId: sessionId, serverPublicKey: openGroupPublicKey) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the mapping
|
||||||
|
let mapping: BlindedIdMapping = BlindedIdMapping(blindedId: sessionId, sessionId: contact.sessionID, serverPublicKey: openGroupPublicKey)
|
||||||
|
Storage.shared.cacheBlindedIdMapping(mapping)
|
||||||
|
|
||||||
|
// Open the existing thread
|
||||||
|
let conversationVC: ConversationVC = ConversationVC(thread: contactThread)
|
||||||
|
self.navigationController?.pushViewController(conversationVC, animated: true)
|
||||||
|
|
||||||
|
didFindContact = true
|
||||||
|
stop.pointee = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't continue if we found the contact
|
||||||
|
guard !didFindContact else { return }
|
||||||
|
|
||||||
|
// Lastly loop through existing id mappings (in case the user is looking at a different SOGS but once had
|
||||||
|
// a thread with this contact in a different SOGS and had cached the mapping)
|
||||||
|
Storage.shared.enumerateBlindedIdMapping { mapping, stop in
|
||||||
|
guard mapping.serverPublicKey != openGroupPublicKey else { return }
|
||||||
|
guard Sodium().sessionId(mapping.sessionId, matchesBlindedId: sessionId, serverPublicKey: openGroupPublicKey) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the new mapping
|
||||||
|
let thread: TSContactThread = TSContactThread.getOrCreateThread(contactSessionID: mapping.sessionId)
|
||||||
|
let newMapping: BlindedIdMapping = BlindedIdMapping(blindedId: sessionId, sessionId: mapping.sessionId, serverPublicKey: openGroupPublicKey)
|
||||||
|
Storage.shared.cacheBlindedIdMapping(newMapping)
|
||||||
|
|
||||||
|
// Open the existing thread
|
||||||
|
let conversationVC: ConversationVC = ConversationVC(thread: thread)
|
||||||
|
self.navigationController?.pushViewController(conversationVC, animated: true)
|
||||||
|
|
||||||
|
didFindContact = true
|
||||||
|
stop.pointee = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't continue if we found the contact
|
||||||
|
guard !didFindContact else { return }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just create a new thread with the provided sessionId
|
||||||
|
let thread = TSContactThread.getOrCreateThread(
|
||||||
|
contactSessionID: sessionId,
|
||||||
|
openGroupServer: openGroupServer,
|
||||||
|
openGroupPublicKey: openGroupPublicKey
|
||||||
|
)
|
||||||
|
let conversationVC: ConversationVC = ConversationVC(thread: thread)
|
||||||
|
|
||||||
|
self.navigationController?.pushViewController(conversationVC, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Voice Message Playback
|
// MARK: Voice Message Playback
|
||||||
@objc func handleAudioDidFinishPlayingNotification(_ notification: Notification) {
|
@objc func handleAudioDidFinishPlayingNotification(_ notification: Notification) {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import UIKit
|
||||||
import SessionUIKit
|
import SessionUIKit
|
||||||
import SessionMessagingKit
|
import SessionMessagingKit
|
||||||
|
|
||||||
|
@ -13,9 +14,8 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
||||||
let focusedMessageID: String? // This is used for global search
|
let focusedMessageID: String? // This is used for global search
|
||||||
var focusedMessageIndexPath: IndexPath?
|
var focusedMessageIndexPath: IndexPath?
|
||||||
var unreadViewItems: [ConversationViewItem] = []
|
var unreadViewItems: [ConversationViewItem] = []
|
||||||
var scrollButtonBottomConstraint: NSLayoutConstraint?
|
var isReplacingThread: Bool = false
|
||||||
var scrollButtonMessageRequestsBottomConstraint: NSLayoutConstraint?
|
|
||||||
var messageRequestsViewBotomConstraint: NSLayoutConstraint?
|
|
||||||
// Search
|
// Search
|
||||||
var isShowingSearchUI = false
|
var isShowingSearchUI = false
|
||||||
var lastSearchedText: String?
|
var lastSearchedText: String?
|
||||||
|
@ -40,7 +40,11 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
||||||
var audioSession: OWSAudioSession { Environment.shared.audioSession }
|
var audioSession: OWSAudioSession { Environment.shared.audioSession }
|
||||||
var dbConnection: YapDatabaseConnection { OWSPrimaryStorage.shared().uiDatabaseConnection }
|
var dbConnection: YapDatabaseConnection { OWSPrimaryStorage.shared().uiDatabaseConnection }
|
||||||
var viewItems: [ConversationViewItem] { viewModel.viewState.viewItems }
|
var viewItems: [ConversationViewItem] { viewModel.viewState.viewItems }
|
||||||
override var canBecomeFirstResponder: Bool { true }
|
|
||||||
|
override var canBecomeFirstResponder: Bool {
|
||||||
|
// Need to return false during the swap between threads to prevent keyboard dismissal
|
||||||
|
!isReplacingThread
|
||||||
|
}
|
||||||
|
|
||||||
override var inputAccessoryView: UIView? {
|
override var inputAccessoryView: UIView? {
|
||||||
if let thread = thread as? TSGroupThread, thread.groupModel.groupType == .closedGroup && !thread.isCurrentUserMemberInGroup() {
|
if let thread = thread as? TSGroupThread, thread.groupModel.groupType == .closedGroup && !thread.isCurrentUserMemberInGroup() {
|
||||||
|
@ -102,6 +106,10 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
||||||
|
|
||||||
private static let messageRequestButtonHeight: CGFloat = 34
|
private static let messageRequestButtonHeight: CGFloat = 34
|
||||||
|
|
||||||
|
var scrollButtonBottomConstraint: NSLayoutConstraint?
|
||||||
|
var scrollButtonMessageRequestsBottomConstraint: NSLayoutConstraint?
|
||||||
|
var messageRequestsViewBotomConstraint: NSLayoutConstraint?
|
||||||
|
|
||||||
lazy var titleView: ConversationTitleView = {
|
lazy var titleView: ConversationTitleView = {
|
||||||
let result = ConversationTitleView(thread: thread)
|
let result = ConversationTitleView(thread: thread)
|
||||||
result.delegate = self
|
result.delegate = self
|
||||||
|
@ -363,6 +371,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
||||||
notificationCenter.addObserver(self, selector: #selector(handleGroupUpdatedNotification), name: .groupThreadUpdated, object: nil)
|
notificationCenter.addObserver(self, selector: #selector(handleGroupUpdatedNotification), name: .groupThreadUpdated, object: nil)
|
||||||
notificationCenter.addObserver(self, selector: #selector(sendScreenshotNotificationIfNeeded), name: UIApplication.userDidTakeScreenshotNotification, object: nil)
|
notificationCenter.addObserver(self, selector: #selector(sendScreenshotNotificationIfNeeded), name: UIApplication.userDidTakeScreenshotNotification, object: nil)
|
||||||
notificationCenter.addObserver(self, selector: #selector(handleMessageSentStatusChanged), name: .messageSentStatusDidChange, object: nil)
|
notificationCenter.addObserver(self, selector: #selector(handleMessageSentStatusChanged), name: .messageSentStatusDidChange, object: nil)
|
||||||
|
notificationCenter.addObserver(self, selector: #selector(handleContactThreadReplaced(_:)), name: .contactThreadReplaced, object: nil)
|
||||||
// Mentions
|
// Mentions
|
||||||
MentionsManager.populateUserPublicKeyCacheIfNeeded(for: thread.uniqueId!)
|
MentionsManager.populateUserPublicKeyCacheIfNeeded(for: thread.uniqueId!)
|
||||||
// Draft
|
// Draft
|
||||||
|
@ -428,6 +437,11 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
||||||
|
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
super.viewWillDisappear(animated)
|
super.viewWillDisappear(animated)
|
||||||
|
|
||||||
|
// Don't set the draft or resign the first responder if we are replacing the thread (want the keyboard
|
||||||
|
// to appear to remain focussed)
|
||||||
|
guard !isReplacingThread else { return }
|
||||||
|
|
||||||
let text = snInputView.text
|
let text = snInputView.text
|
||||||
Storage.write { transaction in
|
Storage.write { transaction in
|
||||||
self.thread.setDraft(text, transaction: transaction)
|
self.thread.setDraft(text, transaction: transaction)
|
||||||
|
@ -693,6 +707,90 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func handleContactThreadReplaced(_ notification: Notification) {
|
||||||
|
// Ensure the current thread is one of the removed ones
|
||||||
|
guard let newThreadId: String = notification.userInfo?[NotificationUserInfoKey.threadId] as? String else { return }
|
||||||
|
guard let removedThreadIds: [String] = notification.userInfo?[NotificationUserInfoKey.removedThreadIds] as? [String] else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let threadId: String = thread.uniqueId, removedThreadIds.contains(threadId) else { return }
|
||||||
|
|
||||||
|
// Then look to swap the current ConversationVC with a replacement one with the new thread
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard let navController: UINavigationController = self.navigationController else { return }
|
||||||
|
guard let viewControllerIndex: Int = navController.viewControllers.firstIndex(of: self) else { return }
|
||||||
|
guard let newThread: TSContactThread = TSContactThread.fetch(uniqueId: newThreadId) else { return }
|
||||||
|
|
||||||
|
// Let the view controller know we are replacing the thread
|
||||||
|
self.isReplacingThread = true
|
||||||
|
|
||||||
|
// Create the new ConversationVC and swap the old one out for it
|
||||||
|
let conversationVC: ConversationVC = ConversationVC(thread: newThread)
|
||||||
|
let currentlyOnThisScreen: Bool = (navController.topViewController == self)
|
||||||
|
|
||||||
|
navController.viewControllers = [
|
||||||
|
(viewControllerIndex == 0 ?
|
||||||
|
[] :
|
||||||
|
navController.viewControllers[0..<viewControllerIndex]
|
||||||
|
),
|
||||||
|
[conversationVC],
|
||||||
|
(viewControllerIndex == (navController.viewControllers.count - 1) ?
|
||||||
|
[] :
|
||||||
|
navController.viewControllers[(viewControllerIndex + 1)..<navController.viewControllers.count]
|
||||||
|
)
|
||||||
|
].flatMap { $0 }
|
||||||
|
|
||||||
|
// If the top vew controller isn't the current one then we need to make sure to swap out child ones as well
|
||||||
|
if !currentlyOnThisScreen {
|
||||||
|
let maybeSettingsViewController: UIViewController? = navController
|
||||||
|
.viewControllers[viewControllerIndex..<navController.viewControllers.count]
|
||||||
|
.first(where: { $0 is OWSConversationSettingsViewController })
|
||||||
|
|
||||||
|
// Update the settings screen (if there is one)
|
||||||
|
if let settingsViewController: OWSConversationSettingsViewController = maybeSettingsViewController as? OWSConversationSettingsViewController {
|
||||||
|
settingsViewController.configure(with: newThread, uiDatabaseConnection: OWSPrimaryStorage.shared().uiDatabaseConnection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to minimise painful UX issues by keeping the 'first responder' state, current input text and
|
||||||
|
// cursor position (Unfortunately there doesn't seem to be a way to prevent the keyboard from
|
||||||
|
// flickering during the swap but other than that it's relatively seamless)
|
||||||
|
if self.snInputView.inputTextViewIsFirstResponder {
|
||||||
|
conversationVC.isReplacingThread = true
|
||||||
|
conversationVC.snInputView.frame = self.snInputView.frame
|
||||||
|
conversationVC.snInputView.text = self.snInputView.text
|
||||||
|
conversationVC.snInputView.selectedRange = self.snInputView.selectedRange
|
||||||
|
|
||||||
|
// Make the current snInputView invisible and add the new one the the UI
|
||||||
|
self.snInputView.alpha = 0
|
||||||
|
self.snInputView.superview?.addSubview(conversationVC.snInputView)
|
||||||
|
|
||||||
|
// Add the old first responder to the window so it the keyboard won't get dismissed when the
|
||||||
|
// OS removes it's parent view from the view hierarchy due to the view controller swap
|
||||||
|
var maybeOldFirstResponderView: UIView?
|
||||||
|
|
||||||
|
if let oldFirstResponderView: UIView = UIResponder.currentFirstResponder() as? UIView {
|
||||||
|
maybeOldFirstResponderView = oldFirstResponderView
|
||||||
|
self.view.window?.addSubview(oldFirstResponderView)
|
||||||
|
}
|
||||||
|
|
||||||
|
// On the next run loop setup the first responder state for the new screen and remove the
|
||||||
|
// old first responder from the window
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
conversationVC.isReplacingThread = false
|
||||||
|
maybeOldFirstResponderView?.resignFirstResponder()
|
||||||
|
maybeOldFirstResponderView?.removeFromSuperview()
|
||||||
|
conversationVC.snInputView.removeFromSuperview()
|
||||||
|
|
||||||
|
_ = conversationVC.becomeFirstResponder()
|
||||||
|
conversationVC.snInputView.inputTextViewBecomeFirstResponder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: General
|
// MARK: General
|
||||||
@objc func addOrRemoveBlockedBanner() {
|
@objc func addOrRemoveBlockedBanner() {
|
||||||
func detach() {
|
func detach() {
|
||||||
|
|
|
@ -24,6 +24,13 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
|
||||||
set { inputTextView.text = newValue }
|
set { inputTextView.text = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var selectedRange: NSRange {
|
||||||
|
get { inputTextView.selectedRange }
|
||||||
|
set { inputTextView.selectedRange = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
var inputTextViewIsFirstResponder: Bool { inputTextView.isFirstResponder }
|
||||||
|
|
||||||
var enabledMessageTypes: MessageTypes = .all {
|
var enabledMessageTypes: MessageTypes = .all {
|
||||||
didSet {
|
didSet {
|
||||||
setEnabledMessageTypes(enabledMessageTypes, message: nil)
|
setEnabledMessageTypes(enabledMessageTypes, message: nil)
|
||||||
|
@ -336,6 +343,10 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
|
||||||
override func resignFirstResponder() -> Bool {
|
override func resignFirstResponder() -> Bool {
|
||||||
inputTextView.resignFirstResponder()
|
inputTextView.resignFirstResponder()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func inputTextViewBecomeFirstResponder() {
|
||||||
|
inputTextView.becomeFirstResponder()
|
||||||
|
}
|
||||||
|
|
||||||
func handleLongPress() {
|
func handleLongPress() {
|
||||||
// Not relevant in this case
|
// Not relevant in this case
|
||||||
|
|
|
@ -66,5 +66,5 @@ protocol MessageCellDelegate : AnyObject {
|
||||||
func openURL(_ url: URL)
|
func openURL(_ url: URL)
|
||||||
func handleReplyButtonTapped(for viewItem: ConversationViewItem)
|
func handleReplyButtonTapped(for viewItem: ConversationViewItem)
|
||||||
func showUserDetails(for sessionID: String)
|
func showUserDetails(for sessionID: String)
|
||||||
func startThread(with sessionID: String, openGroupServer: String, openGroupPublicKey: String)
|
func startThread(with sessionId: String, openGroupServer: String, openGroupPublicKey: String)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,24 @@
|
||||||
|
|
||||||
enum ContactUtilities {
|
enum ContactUtilities {
|
||||||
|
private static func approvedContact(in threadObject: Any, using transaction: Any) -> Contact? {
|
||||||
|
guard let thread: TSContactThread = threadObject as? TSContactThread else { return nil }
|
||||||
|
guard thread.shouldBeVisible else { return nil }
|
||||||
|
guard let contact: Contact = Storage.shared.getContact(with: thread.contactSessionID(), using: transaction) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard contact.didApproveMe else { return nil }
|
||||||
|
|
||||||
|
return contact
|
||||||
|
}
|
||||||
|
|
||||||
static func getAllContacts() -> [String] {
|
static func getAllContacts() -> [String] {
|
||||||
// Collect all contacts
|
// Collect all contacts
|
||||||
var result: [String] = []
|
var result: [Contact] = []
|
||||||
Storage.read { transaction in
|
Storage.read { transaction in
|
||||||
TSContactThread.enumerateCollectionObjects(with: transaction) { object, _ in
|
TSContactThread.enumerateCollectionObjects(with: transaction) { object, _ in
|
||||||
guard
|
guard let contact: Contact = approvedContact(in: object, using: transaction) else { return }
|
||||||
let thread: TSContactThread = object as? TSContactThread,
|
|
||||||
thread.shouldBeVisible,
|
|
||||||
Storage.shared.getContact(
|
|
||||||
with: thread.contactSessionID(),
|
|
||||||
using: transaction
|
|
||||||
)?.didApproveMe == true
|
|
||||||
else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result.append(thread.contactSessionID())
|
result.append(contact)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func getDisplayName(for publicKey: String) -> String {
|
func getDisplayName(for publicKey: String) -> String {
|
||||||
|
@ -25,11 +26,24 @@ enum ContactUtilities {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the current user
|
// Remove the current user
|
||||||
if let index = result.firstIndex(of: getUserHexEncodedPublicKey()) {
|
if let index = result.firstIndex(where: { $0.sessionID == getUserHexEncodedPublicKey() }) {
|
||||||
result.remove(at: index)
|
result.remove(at: index)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort alphabetically
|
// Sort alphabetically
|
||||||
return result.sorted { getDisplayName(for: $0) < getDisplayName(for: $1) }
|
return result
|
||||||
|
.map { contact -> String in (contact.displayName(for: .regular) ?? contact.sessionID) }
|
||||||
|
.sorted()
|
||||||
|
}
|
||||||
|
|
||||||
|
static func enumerateApprovedContactThreads(with block: @escaping (TSContactThread, Contact, UnsafeMutablePointer<ObjCBool>) -> ()) {
|
||||||
|
Storage.read { transaction in
|
||||||
|
TSContactThread.enumerateCollectionObjects(with: transaction) { object, stop in
|
||||||
|
guard let contactThread: TSContactThread = object as? TSContactThread else { return }
|
||||||
|
guard let contact: Contact = approvedContact(in: object, using: transaction) else { return }
|
||||||
|
|
||||||
|
block(contactThread, contact, stop)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -189,7 +189,8 @@ enum MockDataGenerator {
|
||||||
image: nil,
|
image: nil,
|
||||||
groupId: groupId,
|
groupId: groupId,
|
||||||
groupType: .closedGroup,
|
groupType: .closedGroup,
|
||||||
adminIds: [members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId]
|
adminIds: [members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId],
|
||||||
|
moderatorIds: [members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId]
|
||||||
)
|
)
|
||||||
let thread = TSGroupThread.getOrCreateThread(with: group, transaction: transaction)
|
let thread = TSGroupThread.getOrCreateThread(with: group, transaction: transaction)
|
||||||
thread.shouldBeVisible = true
|
thread.shouldBeVisible = true
|
||||||
|
@ -232,23 +233,49 @@ enum MockDataGenerator {
|
||||||
let randomGroupPublicKey: String = KeyPairUtilities.generate(from: data).x25519KeyPair.hexEncodedPublicKey
|
let randomGroupPublicKey: String = KeyPairUtilities.generate(from: data).x25519KeyPair.hexEncodedPublicKey
|
||||||
let serverNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
|
let serverNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
|
||||||
let roomNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
|
let roomNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
|
||||||
|
let groupDescriptionLength: Int = ((10..<50).randomElement(using: &ogThreadRandomGenerator) ?? 0)
|
||||||
let serverName: String = (0..<serverNameLength)
|
let serverName: String = (0..<serverNameLength)
|
||||||
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
|
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
|
||||||
.joined()
|
.joined()
|
||||||
let roomName: String = (0..<roomNameLength)
|
let roomName: String = (0..<roomNameLength)
|
||||||
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
|
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
|
||||||
.joined()
|
.joined()
|
||||||
|
let groupDescription: String = (0..<groupDescriptionLength)
|
||||||
|
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
|
||||||
|
.joined()
|
||||||
|
|
||||||
// Create the open group model and the thread
|
// Create the open group model and the thread
|
||||||
let openGroup: OpenGroupV2 = OpenGroupV2(server: serverName, room: roomName, name: roomName, publicKey: randomGroupPublicKey, imageID: nil)
|
let openGroup: OpenGroup = OpenGroup(
|
||||||
|
server: serverName,
|
||||||
|
room: roomName,
|
||||||
|
publicKey: randomGroupPublicKey,
|
||||||
|
name: roomName,
|
||||||
|
groupDescription: groupDescription,
|
||||||
|
imageID: nil,
|
||||||
|
infoUpdates: 0
|
||||||
|
)
|
||||||
let groupId: Data = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroup.id)
|
let groupId: Data = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroup.id)
|
||||||
let model = TSGroupModel(title: openGroup.name, memberIds: [ userSessionId ], image: nil, groupId: groupId, groupType: .openGroup, adminIds: [])
|
let model = TSGroupModel(title: openGroup.name, memberIds: [ userSessionId ], image: nil, groupId: groupId, groupType: .openGroup, adminIds: [], moderatorIds: [])
|
||||||
|
|
||||||
let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction)
|
let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction)
|
||||||
thread.shouldBeVisible = true
|
thread.shouldBeVisible = true
|
||||||
thread.save(with: transaction)
|
thread.save(with: transaction)
|
||||||
|
|
||||||
Storage.shared.setV2OpenGroup(openGroup, for: thread.uniqueId!, using: transaction)
|
Storage.shared.setOpenGroup(openGroup, for: thread.uniqueId!, using: transaction)
|
||||||
|
|
||||||
|
// Generate the 'Server' object
|
||||||
|
let hasBlinding: Bool = Bool.random(using: &dmThreadRandomGenerator)
|
||||||
|
|
||||||
|
let server: OpenGroupAPI.Server = OpenGroupAPI.Server(
|
||||||
|
name: serverName,
|
||||||
|
capabilities: OpenGroupAPI.Capabilities(
|
||||||
|
capabilities: [.sogs]
|
||||||
|
.appending(hasBlinding ? [.blind] : []),
|
||||||
|
missing: nil
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Storage.shared.setOpenGroupServer(server, using: transaction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
40
SessionMessagingKit/Contacts/BlindedIdMapping.swift
Normal file
40
SessionMessagingKit/Contacts/BlindedIdMapping.swift
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@objc(SNBlindedIdMapping)
|
||||||
|
public class BlindedIdMapping: NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
||||||
|
@objc public let blindedId: String
|
||||||
|
@objc public let sessionId: String
|
||||||
|
@objc public let serverPublicKey: String
|
||||||
|
|
||||||
|
// MARK: - Initialization
|
||||||
|
|
||||||
|
@objc public init(blindedId: String, sessionId: String, serverPublicKey: String) {
|
||||||
|
self.blindedId = blindedId
|
||||||
|
self.sessionId = sessionId
|
||||||
|
self.serverPublicKey = serverPublicKey
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
private override init() { preconditionFailure("Use init(blindedId:sessionId:) instead.") }
|
||||||
|
|
||||||
|
// MARK: - Coding
|
||||||
|
|
||||||
|
public required init?(coder: NSCoder) {
|
||||||
|
guard let blindedId: String = coder.decodeObject(forKey: "blindedId") as! String? else { return nil }
|
||||||
|
guard let sessionId: String = coder.decodeObject(forKey: "sessionId") as! String? else { return nil }
|
||||||
|
guard let serverPublicKey: String = coder.decodeObject(forKey: "serverPublicKey") as! String? else { return nil }
|
||||||
|
|
||||||
|
self.blindedId = blindedId
|
||||||
|
self.sessionId = sessionId
|
||||||
|
self.serverPublicKey = serverPublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
public func encode(with coder: NSCoder) {
|
||||||
|
coder.encode(blindedId, forKey: "blindedId")
|
||||||
|
coder.encode(sessionId, forKey: "sessionId")
|
||||||
|
coder.encode(serverPublicKey, forKey: "serverPublicKey")
|
||||||
|
}
|
||||||
|
}
|
|
@ -68,4 +68,44 @@ extension Storage {
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Blinded Id cache
|
||||||
|
|
||||||
|
private static let blindedIdCacheCollection = "BlindedIdCacheCollection"
|
||||||
|
|
||||||
|
public func getBlindedIdMapping(with blindedId: String) -> BlindedIdMapping? {
|
||||||
|
var result: BlindedIdMapping?
|
||||||
|
Storage.read { transaction in
|
||||||
|
result = self.getBlindedIdMapping(with: blindedId, using: transaction)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getBlindedIdMapping(with blindedId: String, using transaction: YapDatabaseReadTransaction) -> BlindedIdMapping? {
|
||||||
|
return transaction.object(forKey: blindedId, inCollection: Storage.blindedIdCacheCollection) as? BlindedIdMapping
|
||||||
|
}
|
||||||
|
|
||||||
|
public func cacheBlindedIdMapping(_ mapping: BlindedIdMapping) {
|
||||||
|
Storage.write { transaction in
|
||||||
|
self.cacheBlindedIdMapping(mapping, using: transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func cacheBlindedIdMapping(_ mapping: BlindedIdMapping, using transaction: YapDatabaseReadWriteTransaction) {
|
||||||
|
transaction.setObject(mapping, forKey: mapping.blindedId, inCollection: Storage.blindedIdCacheCollection)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func enumerateBlindedIdMapping(with block: @escaping (BlindedIdMapping, UnsafeMutablePointer<ObjCBool>) -> ()) {
|
||||||
|
Storage.read { transaction in
|
||||||
|
self.enumerateBlindedIdMapping(with: block, transaction: transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func enumerateBlindedIdMapping(with block: @escaping (BlindedIdMapping, UnsafeMutablePointer<ObjCBool>) -> (), transaction: YapDatabaseReadTransaction) {
|
||||||
|
transaction.enumerateRows(inCollection: Storage.blindedIdCacheCollection) { _, object, _, stop in
|
||||||
|
guard let mapping = object as? BlindedIdMapping else { return }
|
||||||
|
|
||||||
|
block(mapping, stop)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,35 +2,6 @@ import PromiseKit
|
||||||
import Sodium
|
import Sodium
|
||||||
|
|
||||||
extension Storage {
|
extension Storage {
|
||||||
|
|
||||||
public func getAllMessageRequestThreads() -> [String: TSContactThread] {
|
|
||||||
var result: [String: TSContactThread] = [:]
|
|
||||||
|
|
||||||
Storage.read { transaction in
|
|
||||||
result = self.getAllMessageRequestThreads(using: transaction)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
public func getAllMessageRequestThreads(using transaction: YapDatabaseReadTransaction) -> [String: TSContactThread] {
|
|
||||||
var result = [String: TSContactThread]()
|
|
||||||
|
|
||||||
// FIXME: We might be able to optimise this further by filtering the SQL query `WHERE uniqueId LIKE '_c15'
|
|
||||||
let blindedThreadPrefix: String = TSContactThread.threadID(fromContactSessionID: SessionId.Prefix.blinded.rawValue)
|
|
||||||
|
|
||||||
transaction.enumerateKeysAndObjects(
|
|
||||||
inCollection: TSContactThread.collection(),
|
|
||||||
using: { threadID, object, _ in
|
|
||||||
guard let contactThread = object as? TSContactThread else { return }
|
|
||||||
result[threadID] = contactThread
|
|
||||||
},
|
|
||||||
withFilter: { key -> Bool in key.starts(with: blindedThreadPrefix) }
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the ID of the thread.
|
/// Returns the ID of the thread.
|
||||||
public func getOrCreateThread(for publicKey: String, groupPublicKey: String?, openGroupID: String?, using transaction: Any) -> String? {
|
public func getOrCreateThread(for publicKey: String, groupPublicKey: String?, openGroupID: String?, using transaction: Any) -> String? {
|
||||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||||
|
@ -180,5 +151,35 @@ extension Storage {
|
||||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||||
transaction.setObject(receivedMessageTimestamps, forKey: "receivedMessageTimestamps", inCollection: Storage.receivedMessageTimestampsCollection)
|
transaction.setObject(receivedMessageTimestamps, forKey: "receivedMessageTimestamps", inCollection: Storage.receivedMessageTimestampsCollection)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Message Request Handling
|
||||||
|
|
||||||
|
public func getAllMessageRequestThreads() -> [String: TSContactThread] {
|
||||||
|
var result: [String: TSContactThread] = [:]
|
||||||
|
|
||||||
|
Storage.read { transaction in
|
||||||
|
result = self.getAllMessageRequestThreads(using: transaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getAllMessageRequestThreads(using transaction: YapDatabaseReadTransaction) -> [String: TSContactThread] {
|
||||||
|
var result = [String: TSContactThread]()
|
||||||
|
|
||||||
|
// FIXME: We might be able to optimise this further by filtering the SQL query `WHERE uniqueId LIKE '_c15'
|
||||||
|
let blindedThreadPrefix: String = TSContactThread.threadID(fromContactSessionID: SessionId.Prefix.blinded.rawValue)
|
||||||
|
|
||||||
|
transaction.enumerateKeysAndObjects(
|
||||||
|
inCollection: TSContactThread.collection(),
|
||||||
|
using: { threadID, object, _ in
|
||||||
|
guard let contactThread = object as? TSContactThread else { return }
|
||||||
|
result[threadID] = contactThread
|
||||||
|
},
|
||||||
|
withFilter: { key -> Bool in key.starts(with: blindedThreadPrefix) }
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,35 +55,9 @@ extension Storage {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
public func storeOpenGroupServer(_ server: OpenGroupAPI.Server, using transaction: Any) {
|
public func setOpenGroupServer(_ server: OpenGroupAPI.Server, using transaction: Any) {
|
||||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(server, forKey: "SOGS.\(server.name)", inCollection: Storage.openGroupCollection)
|
(transaction as! YapDatabaseReadWriteTransaction).setObject(server, forKey: "SOGS.\(server.name)", inCollection: Storage.openGroupCollection)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Authorization
|
|
||||||
|
|
||||||
private static let authTokenCollection = "SNAuthTokenCollection"
|
|
||||||
|
|
||||||
public func getAuthToken(for room: String, on server: String) -> String? {
|
|
||||||
let collection = Storage.authTokenCollection
|
|
||||||
let key = "\(server).\(room)"
|
|
||||||
var result: String? = nil
|
|
||||||
Storage.read { transaction in
|
|
||||||
result = transaction.object(forKey: key, inCollection: collection) as? String
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setAuthToken(for room: String, on server: String, to newValue: String, using transaction: Any) {
|
|
||||||
let collection = Storage.authTokenCollection
|
|
||||||
let key = "\(server).\(room)"
|
|
||||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: key, inCollection: collection)
|
|
||||||
}
|
|
||||||
|
|
||||||
public func removeAuthToken(for room: String, on server: String, using transaction: Any) {
|
|
||||||
let collection = Storage.authTokenCollection
|
|
||||||
let key = "\(server).\(room)"
|
|
||||||
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: key, inCollection: collection)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -109,12 +83,12 @@ extension Storage {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Last Message Server ID
|
// MARK: - Open Group Sequence Number
|
||||||
|
|
||||||
public static let lastMessageServerIDCollection = "SNLastMessageServerIDCollection"
|
public static let openGroupSequenceNumberCollection = "SNOpenGroupSequenceNumberCollection"
|
||||||
|
|
||||||
public func getLastMessageServerID(for room: String, on server: String) -> Int64? {
|
public func getOpenGroupSequenceNumber(for room: String, on server: String) -> Int64? {
|
||||||
let collection = Storage.lastMessageServerIDCollection
|
let collection = Storage.openGroupSequenceNumberCollection
|
||||||
let key = "\(server).\(room)"
|
let key = "\(server).\(room)"
|
||||||
var result: Int64? = nil
|
var result: Int64? = nil
|
||||||
Storage.read { transaction in
|
Storage.read { transaction in
|
||||||
|
@ -123,48 +97,41 @@ extension Storage {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setLastMessageServerID(for room: String, on server: String, to newValue: Int64, using transaction: Any) {
|
public func setOpenGroupSequenceNumber(for room: String, on server: String, to newValue: Int64, using transaction: Any) {
|
||||||
let collection = Storage.lastMessageServerIDCollection
|
let collection = Storage.openGroupSequenceNumberCollection
|
||||||
let key = "\(server).\(room)"
|
let key = "\(server).\(room)"
|
||||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: key, inCollection: collection)
|
(transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: key, inCollection: collection)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func removeLastMessageServerID(for room: String, on server: String, using transaction: Any) {
|
public func removeOpenGroupSequenceNumber(for room: String, on server: String, using transaction: Any) {
|
||||||
let collection = Storage.lastMessageServerIDCollection
|
let collection = Storage.openGroupSequenceNumberCollection
|
||||||
let key = "\(server).\(room)"
|
let key = "\(server).\(room)"
|
||||||
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: key, inCollection: collection)
|
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: key, inCollection: collection)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - -- Open Group Inbox Latest Message Id
|
||||||
|
|
||||||
|
public static let openGroupInboxLatestMessageIdCollection = "SNOpenGroupInboxLatestMessageIdCollection"
|
||||||
|
|
||||||
|
public func getOpenGroupInboxLatestMessageId(for server: String) -> Int64? {
|
||||||
// MARK: - Last Deletion Server ID
|
let collection = Storage.openGroupInboxLatestMessageIdCollection
|
||||||
|
|
||||||
public static let lastDeletionServerIDCollection = "SNLastDeletionServerIDCollection"
|
|
||||||
|
|
||||||
public func getLastDeletionServerID(for room: String, on server: String) -> Int64? {
|
|
||||||
let collection = Storage.lastDeletionServerIDCollection
|
|
||||||
let key = "\(server).\(room)"
|
|
||||||
var result: Int64? = nil
|
var result: Int64? = nil
|
||||||
Storage.read { transaction in
|
Storage.read { transaction in
|
||||||
result = transaction.object(forKey: key, inCollection: collection) as? Int64
|
result = transaction.object(forKey: server, inCollection: collection) as? Int64
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setLastDeletionServerID(for room: String, on server: String, to newValue: Int64, using transaction: Any) {
|
public func setOpenGroupInboxLatestMessageId(for server: String, to newValue: Int64, using transaction: Any) {
|
||||||
let collection = Storage.lastDeletionServerIDCollection
|
let collection = Storage.openGroupInboxLatestMessageIdCollection
|
||||||
let key = "\(server).\(room)"
|
(transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: server, inCollection: collection)
|
||||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: key, inCollection: collection)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func removeLastDeletionServerID(for room: String, on server: String, using transaction: Any) {
|
public func removeOpenGroupInboxLatestMessageId(for server: String, using transaction: Any) {
|
||||||
let collection = Storage.lastDeletionServerIDCollection
|
let collection = Storage.openGroupInboxLatestMessageIdCollection
|
||||||
let key = "\(server).\(room)"
|
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: server, inCollection: collection)
|
||||||
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: key, inCollection: collection)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Metadata
|
// MARK: - Metadata
|
||||||
|
|
||||||
private static let openGroupUserCountCollection = "SNOpenGroupUserCountCollection"
|
private static let openGroupUserCountCollection = "SNOpenGroupUserCountCollection"
|
||||||
|
|
|
@ -79,6 +79,10 @@ NSString *NSStringFromOWSInteractionType(OWSInteractionType value);
|
||||||
|
|
||||||
- (void)updateTimestamp:(uint64_t)timestamp;
|
- (void)updateTimestamp:(uint64_t)timestamp;
|
||||||
|
|
||||||
|
#pragma mark Message Request Thread Migration
|
||||||
|
|
||||||
|
- (void)moveToThreadWithId:(NSString *)threadId;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|
|
@ -269,6 +269,12 @@ NSString *NSStringFromOWSInteractionType(OWSInteractionType value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#pragma mark - Message Request Thread Migration
|
||||||
|
|
||||||
|
- (void)moveToThreadWithId:(NSString *)threadId {
|
||||||
|
_uniqueThreadId = threadId;
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|
|
@ -35,6 +35,13 @@ extension OpenGroupAPI {
|
||||||
|
|
||||||
public let capabilities: [Capability]
|
public let capabilities: [Capability]
|
||||||
public let missing: [Capability]?
|
public let missing: [Capability]?
|
||||||
|
|
||||||
|
// MARK: - Initialization
|
||||||
|
|
||||||
|
public init(capabilities: [Capability], missing: [Capability]? = nil) {
|
||||||
|
self.capabilities = capabilities
|
||||||
|
self.missing = missing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -93,7 +93,6 @@ public final class OpenGroupManager: NSObject {
|
||||||
}
|
}
|
||||||
storage.updateMessageIDCollectionByPruningMessagesWithIDs(messageIDs, using: transaction)
|
storage.updateMessageIDCollectionByPruningMessagesWithIDs(messageIDs, using: transaction)
|
||||||
Storage.shared.removeReceivedMessageTimestamps(messageTimestamps, using: transaction)
|
Storage.shared.removeReceivedMessageTimestamps(messageTimestamps, using: transaction)
|
||||||
let _ = OpenGroupAPI.legacyDeleteAuthToken(for: openGroup.room, on: openGroup.server)
|
|
||||||
Storage.shared.removeOpenGroupSequenceNumber(for: openGroup.room, on: openGroup.server, using: transaction)
|
Storage.shared.removeOpenGroupSequenceNumber(for: openGroup.room, on: openGroup.server, using: transaction)
|
||||||
|
|
||||||
thread.removeAllThreadInteractions(with: transaction)
|
thread.removeAllThreadInteractions(with: transaction)
|
||||||
|
@ -119,7 +118,7 @@ public final class OpenGroupManager: NSObject {
|
||||||
capabilities: capabilities
|
capabilities: capabilities
|
||||||
)
|
)
|
||||||
|
|
||||||
dependencies.storage.storeOpenGroupServer(updatedServer, using: transaction)
|
dependencies.storage.setOpenGroupServer(updatedServer, using: transaction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import Foundation
|
||||||
|
import Sodium
|
||||||
import SignalCoreKit
|
import SignalCoreKit
|
||||||
import SessionSnodeKit
|
import SessionSnodeKit
|
||||||
|
|
||||||
|
@ -238,8 +240,10 @@ extension MessageReceiver {
|
||||||
thread.remove(with: transaction)
|
thread.remove(with: transaction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else if SessionId.Prefix(from: sessionID) != .blinded {
|
||||||
// Otherwise create and save the thread
|
// Otherwise create and save the thread (if the contact isn't a blinded contact - we don't want to
|
||||||
|
// auto-create threads for blinded contacts if they have no messages)
|
||||||
|
// TODO: See what this will do with blinded->unblinded conversations?
|
||||||
let thread = TSContactThread.getOrCreateThread(withContactSessionID: sessionID, transaction: transaction)
|
let thread = TSContactThread.getOrCreateThread(withContactSessionID: sessionID, transaction: transaction)
|
||||||
thread.shouldBeVisible = true
|
thread.shouldBeVisible = true
|
||||||
thread.save(with: transaction)
|
thread.save(with: transaction)
|
||||||
|
@ -839,26 +843,130 @@ extension MessageReceiver {
|
||||||
|
|
||||||
public static func handleMessageRequestResponse(_ message: MessageRequestResponse, using transaction: Any) {
|
public static func handleMessageRequestResponse(_ message: MessageRequestResponse, using transaction: Any) {
|
||||||
let userPublicKey = getUserHexEncodedPublicKey()
|
let userPublicKey = getUserHexEncodedPublicKey()
|
||||||
|
var blindedContactIds: [String] = []
|
||||||
|
var blindedThreadIds: [String] = []
|
||||||
|
|
||||||
// Ignore messages which were sent from the current user
|
// Ignore messages which were sent from the current user
|
||||||
guard message.sender != userPublicKey else { return }
|
guard message.sender != userPublicKey else { return }
|
||||||
guard let senderId: String = message.sender else { return }
|
guard let senderId: String = message.sender else { return }
|
||||||
|
guard let transaction: YapDatabaseReadWriteTransaction = transaction as? YapDatabaseReadWriteTransaction else {
|
||||||
// Get the existing thead and notify the user
|
return
|
||||||
if let transaction: YapDatabaseReadWriteTransaction = transaction as? YapDatabaseReadWriteTransaction, let thread: TSContactThread = TSContactThread.getWithContactSessionID(senderId, transaction: transaction) {
|
|
||||||
let infoMessage = TSInfoMessage(
|
|
||||||
timestamp: (message.sentTimestamp ?? NSDate.ows_millisecondTimeStamp()),
|
|
||||||
in: thread,
|
|
||||||
messageType: .messageRequestAccepted
|
|
||||||
)
|
|
||||||
infoMessage.save(with: transaction)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prep the unblinded thread
|
||||||
|
let unblindedThreadId: String = TSContactThread.threadID(fromContactSessionID: senderId)
|
||||||
|
let unblindedThread: TSContactThread = TSContactThread.getOrCreateThread(withContactSessionID: senderId, transaction: transaction)
|
||||||
|
|
||||||
|
// Need to handle a `MessageRequestResponse` sent to a blinded thread (ie. check if the sender matches
|
||||||
|
// the blinded ids of any threads)
|
||||||
|
let messageRequestThreads: [String: TSContactThread] = Storage.shared.getAllMessageRequestThreads(using: transaction)
|
||||||
|
|
||||||
|
if !messageRequestThreads.isEmpty {
|
||||||
|
var interactionsToMove: [TSInteraction] = []
|
||||||
|
var threadsToDelete: [TSContactThread] = []
|
||||||
|
|
||||||
|
// Loop through all blinded threads and extract any interactions relating to the user accepting
|
||||||
|
// the message request
|
||||||
|
for blindedThread in messageRequestThreads.values {
|
||||||
|
let blindedId: String = blindedThread.contactSessionID()
|
||||||
|
|
||||||
|
// If the sessionId matches the blindedId then this thread needs to be converted to an un-blinded thread
|
||||||
|
guard let serverPublicKey: String = blindedThread.originalOpenGroupPublicKey else { continue }
|
||||||
|
guard Sodium().sessionId(senderId, matchesBlindedId: blindedId, serverPublicKey: serverPublicKey) else { continue }
|
||||||
|
guard let blindedThreadId: String = blindedThread.uniqueId else { continue }
|
||||||
|
guard let view: YapDatabaseAutoViewTransaction = transaction.ext(TSMessageDatabaseViewExtensionName) as? YapDatabaseAutoViewTransaction else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the mapping
|
||||||
|
let mapping: BlindedIdMapping = BlindedIdMapping(blindedId: blindedId, sessionId: senderId, serverPublicKey: serverPublicKey)
|
||||||
|
Storage.shared.cacheBlindedIdMapping(mapping, using: transaction)
|
||||||
|
|
||||||
|
// Add the `blindedId` to an array so we can remove them at the end of processing
|
||||||
|
blindedContactIds.append(blindedId)
|
||||||
|
blindedThreadIds.append(blindedThreadId)
|
||||||
|
|
||||||
|
// Loop through all of the interactions and add them to a list to be moved to the new thread
|
||||||
|
view.enumerateRows(inGroup: blindedThreadId) { _, _, object, _, _, _ in
|
||||||
|
guard let interaction: TSInteraction = object as? TSInteraction else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
interactionsToMove.append(interaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
threadsToDelete.append(blindedThread)
|
||||||
|
|
||||||
|
// TODO: Pending jobs???
|
||||||
|
// Storage.shared.getAllPendingJobs(of: <#T##Job.Type#>)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the interactions by their `sortId` (which looks to be a global sort id for all interactions) just in case
|
||||||
|
// the behaviour changes in the future and the value can get reset (this way we process the interactions in the
|
||||||
|
// correct order regardless of how many threads they came from)
|
||||||
|
let sortedInteractionsToMove: [TSInteraction] = interactionsToMove
|
||||||
|
.sorted { lhs, rhs -> Bool in lhs.sortId < rhs.sortId }
|
||||||
|
|
||||||
|
// Note: Unfortunately we need to move the interactions separately from enumerating them to avoid mutating the
|
||||||
|
// `TSMessageDatabaseViewExtensionName` while enumerating it (this does mean paying the cost of looping a second time)
|
||||||
|
for interaction in sortedInteractionsToMove {
|
||||||
|
interaction.moveToThread(withId: unblindedThreadId)
|
||||||
|
interaction.save(with: transaction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete the old threads
|
||||||
|
for thread in threadsToDelete {
|
||||||
|
// TODO: This isn't updating the HomeVC... Race condition??? (Seems to not happen when stepping through with breakpoints)
|
||||||
|
thread.removeAllThreadInteractions(with: transaction)
|
||||||
|
thread.remove(with: transaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the `didApproveMe` state of the sender
|
||||||
updateContactApprovalStatusIfNeeded(
|
updateContactApprovalStatusIfNeeded(
|
||||||
senderSessionId: senderId,
|
senderSessionId: senderId,
|
||||||
threadId: nil,
|
threadId: nil,
|
||||||
forceConfigSync: true,
|
forceConfigSync: blindedContactIds.isEmpty, // Sync here if there are no blinded contacts
|
||||||
using: transaction
|
using: transaction
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// If there were blinded contacts then we should remove them
|
||||||
|
if !blindedContactIds.isEmpty {
|
||||||
|
// Delete all of the processed blinded contacts (shouldn't need them anymore and don't want them taking up
|
||||||
|
// space in the config message)
|
||||||
|
for blindedId in blindedContactIds {
|
||||||
|
// TODO: OWSBlockingManager...???
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should assume the 'sender' is a newly created contact and hence need to update it's `isApproved` state
|
||||||
|
updateContactApprovalStatusIfNeeded(
|
||||||
|
senderSessionId: userPublicKey,
|
||||||
|
threadId: unblindedThreadId,
|
||||||
|
forceConfigSync: true,
|
||||||
|
using: transaction
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify the user of their approval (Note: This will always appear in the un-blinded thread)
|
||||||
|
// Note: We want to do this last as it'll mean the un-blinded thread gets updated and the contact approval status
|
||||||
|
// will have been updated at this point (which will mean the `TSThread.isMessageRequest` will return correctly
|
||||||
|
// after this is saved
|
||||||
|
let infoMessage = TSInfoMessage(
|
||||||
|
timestamp: (message.sentTimestamp ?? NSDate.ows_millisecondTimeStamp()),
|
||||||
|
in: unblindedThread,
|
||||||
|
messageType: .messageRequestAccepted
|
||||||
|
)
|
||||||
|
infoMessage.save(with: transaction)
|
||||||
|
|
||||||
|
// Finally we need to send a notification that the thread was replaced so we can handle the case where the
|
||||||
|
// user might currently have the replaced thread open (only need to do this if we actually had blindedIds)
|
||||||
|
if !blindedThreadIds.isEmpty {
|
||||||
|
let userInfo: [NotificationUserInfoKey: Any] = [
|
||||||
|
.threadId: unblindedThreadId,
|
||||||
|
.removedThreadIds: blindedThreadIds
|
||||||
|
]
|
||||||
|
|
||||||
|
NotificationCenter.default.post(name: .contactThreadReplaced, object: nil, userInfo: userInfo)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,7 @@ public protocol SessionMessagingKitStorageProtocol {
|
||||||
func setUserCount(to newValue: UInt64, forOpenGroupWithID openGroupID: String, using transaction: Any)
|
func setUserCount(to newValue: UInt64, forOpenGroupWithID openGroupID: String, using transaction: Any)
|
||||||
|
|
||||||
func getOpenGroupServer(name: String) -> OpenGroupAPI.Server?
|
func getOpenGroupServer(name: String) -> OpenGroupAPI.Server?
|
||||||
func storeOpenGroupServer(_ server: OpenGroupAPI.Server, using transaction: Any)
|
func setOpenGroupServer(_ server: OpenGroupAPI.Server, using transaction: Any)
|
||||||
|
|
||||||
// MARK: - -- Open Group Public Keys
|
// MARK: - -- Open Group Public Keys
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ public extension Notification.Name {
|
||||||
static let groupThreadUpdated = Notification.Name("groupThreadUpdated")
|
static let groupThreadUpdated = Notification.Name("groupThreadUpdated")
|
||||||
static let muteSettingUpdated = Notification.Name("muteSettingUpdated")
|
static let muteSettingUpdated = Notification.Name("muteSettingUpdated")
|
||||||
static let messageSentStatusDidChange = Notification.Name("messageSentStatusDidChange")
|
static let messageSentStatusDidChange = Notification.Name("messageSentStatusDidChange")
|
||||||
|
static let contactThreadReplaced = Notification.Name("contactThreadReplaced")
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc public extension NSNotification {
|
@objc public extension NSNotification {
|
||||||
|
@ -12,3 +13,8 @@ public extension Notification.Name {
|
||||||
@objc static let muteSettingUpdated = Notification.Name.muteSettingUpdated.rawValue as NSString
|
@objc static let muteSettingUpdated = Notification.Name.muteSettingUpdated.rawValue as NSString
|
||||||
@objc static let messageSentStatusDidChange = Notification.Name.messageSentStatusDidChange.rawValue as NSString
|
@objc static let messageSentStatusDidChange = Notification.Name.messageSentStatusDidChange.rawValue as NSString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum NotificationUserInfoKey: String {
|
||||||
|
case threadId
|
||||||
|
case removedThreadIds
|
||||||
|
}
|
||||||
|
|
|
@ -10,13 +10,24 @@ extern NSString *const TSContactThreadPrefix;
|
||||||
|
|
||||||
@interface TSContactThread : TSThread
|
@interface TSContactThread : TSThread
|
||||||
|
|
||||||
|
@property (nonatomic, nullable) NSString *originalOpenGroupServer;
|
||||||
|
@property (nonatomic, nullable) NSString *originalOpenGroupPublicKey;
|
||||||
|
|
||||||
- (instancetype)initWithContactSessionID:(NSString *)contactSessionID;
|
- (instancetype)initWithContactSessionID:(NSString *)contactSessionID;
|
||||||
|
|
||||||
+ (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID NS_SWIFT_NAME(getOrCreateThread(contactSessionID:));
|
+ (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID NS_SWIFT_NAME(getOrCreateThread(contactSessionID:));
|
||||||
|
+ (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID
|
||||||
|
openGroupServer:(NSString *)openGroupServer
|
||||||
|
openGroupPublicKey:(NSString *)openGroupPublicKey NS_SWIFT_NAME(getOrCreateThread(contactSessionID:openGroupServer:openGroupPublicKey:));
|
||||||
|
|
||||||
+ (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID
|
+ (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID
|
||||||
transaction:(YapDatabaseReadWriteTransaction *)transaction;
|
transaction:(YapDatabaseReadWriteTransaction *)transaction;
|
||||||
|
|
||||||
|
+ (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID
|
||||||
|
openGroupServer:(NSString *)openGroupServer
|
||||||
|
openGroupPublicKey:(NSString *)openGroupPublicKey
|
||||||
|
transaction:(YapDatabaseReadWriteTransaction *)transaction;
|
||||||
|
|
||||||
// Unlike getOrCreateThreadWithContactSessionID, this will _NOT_ create a thread if one does not already exist.
|
// Unlike getOrCreateThreadWithContactSessionID, this will _NOT_ create a thread if one does not already exist.
|
||||||
+ (nullable instancetype)getThreadWithContactSessionID:(NSString *)contactSessionID transaction:(YapDatabaseReadTransaction *)transaction;
|
+ (nullable instancetype)getThreadWithContactSessionID:(NSString *)contactSessionID transaction:(YapDatabaseReadTransaction *)transaction;
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,23 @@ NSString *const TSContactThreadPrefix = @"c";
|
||||||
return thread;
|
return thread;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
+ (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID
|
||||||
|
openGroupServer:(NSString *)openGroupServer
|
||||||
|
openGroupPublicKey:(NSString *)openGroupPublicKey
|
||||||
|
transaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||||
|
{
|
||||||
|
TSContactThread *thread = [self fetchObjectWithUniqueID:[self threadIDFromContactSessionID:contactSessionID] transaction:transaction];
|
||||||
|
|
||||||
|
if (!thread) {
|
||||||
|
thread = [[TSContactThread alloc] initWithContactSessionID:contactSessionID];
|
||||||
|
thread.originalOpenGroupServer = openGroupServer;
|
||||||
|
thread.originalOpenGroupPublicKey = openGroupPublicKey;
|
||||||
|
[thread saveWithTransaction:transaction];
|
||||||
|
}
|
||||||
|
|
||||||
|
return thread;
|
||||||
|
}
|
||||||
|
|
||||||
+ (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID
|
+ (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID
|
||||||
{
|
{
|
||||||
__block TSContactThread *thread;
|
__block TSContactThread *thread;
|
||||||
|
@ -43,6 +60,18 @@ NSString *const TSContactThreadPrefix = @"c";
|
||||||
return thread;
|
return thread;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
+ (instancetype)getOrCreateThreadWithContactSessionID:(NSString *)contactSessionID
|
||||||
|
openGroupServer:(NSString *)openGroupServer
|
||||||
|
openGroupPublicKey:(NSString *)openGroupPublicKey
|
||||||
|
{
|
||||||
|
__block TSContactThread *thread;
|
||||||
|
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||||
|
thread = [self getOrCreateThreadWithContactSessionID:contactSessionID openGroupServer:openGroupServer openGroupPublicKey:openGroupPublicKey transaction:transaction];
|
||||||
|
}];
|
||||||
|
|
||||||
|
return thread;
|
||||||
|
}
|
||||||
|
|
||||||
+ (nullable instancetype)getThreadWithContactSessionID:(NSString *)contactSessionID transaction:(YapDatabaseReadTransaction *)transaction;
|
+ (nullable instancetype)getThreadWithContactSessionID:(NSString *)contactSessionID transaction:(YapDatabaseReadTransaction *)transaction;
|
||||||
{
|
{
|
||||||
return [TSContactThread fetchObjectWithUniqueID:[self threadIDFromContactSessionID:contactSessionID] transaction:transaction];
|
return [TSContactThread fetchObjectWithUniqueID:[self threadIDFromContactSessionID:contactSessionID] transaction:transaction];
|
||||||
|
|
|
@ -86,7 +86,7 @@ class TestStorage: SessionMessagingKitStorageProtocol, Mockable {
|
||||||
func getOpenGroup(for threadID: String) -> OpenGroup? { return (mockData[.openGroup] as? OpenGroup) }
|
func getOpenGroup(for threadID: String) -> OpenGroup? { return (mockData[.openGroup] as? OpenGroup) }
|
||||||
func setOpenGroup(_ openGroup: OpenGroup, for threadID: String, using transaction: Any) { mockData[.openGroup] = openGroup }
|
func setOpenGroup(_ openGroup: OpenGroup, for threadID: String, using transaction: Any) { mockData[.openGroup] = openGroup }
|
||||||
func getOpenGroupServer(name: String) -> OpenGroupAPI.Server? { return mockData[.openGroupServer] as? OpenGroupAPI.Server }
|
func getOpenGroupServer(name: String) -> OpenGroupAPI.Server? { return mockData[.openGroupServer] as? OpenGroupAPI.Server }
|
||||||
func storeOpenGroupServer(_ server: OpenGroupAPI.Server, using transaction: Any) { mockData[.openGroupServer] = server }
|
func setOpenGroupServer(_ server: OpenGroupAPI.Server, using transaction: Any) { mockData[.openGroupServer] = server }
|
||||||
|
|
||||||
func getUserCount(forOpenGroupWithID openGroupID: String) -> UInt64? {
|
func getUserCount(forOpenGroupWithID openGroupID: String) -> UInt64? {
|
||||||
return (mockData[.openGroupUserCount] as? UInt64)
|
return (mockData[.openGroupUserCount] as? UInt64)
|
||||||
|
|
33
SignalUtilitiesKit/Database/Migrations/SOGSV4Migration.swift
Normal file
33
SignalUtilitiesKit/Database/Migrations/SOGSV4Migration.swift
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@objc(SNSOGSV4Migration)
|
||||||
|
public class SOGSV4Migration: OWSDatabaseMigration {
|
||||||
|
|
||||||
|
@objc
|
||||||
|
class func migrationId() -> String {
|
||||||
|
return "003"
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func runUp(completion: @escaping OWSDatabaseMigrationCompletion) {
|
||||||
|
self.doMigrationAsync(completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func doMigrationAsync(completion: @escaping OWSDatabaseMigrationCompletion) {
|
||||||
|
// These collections became redundant in SOGS V4
|
||||||
|
let lastMessageServerIDCollection: String = "SNLastMessageServerIDCollection"
|
||||||
|
let lastDeletionServerIDCollection: String = "SNLastDeletionServerIDCollection"
|
||||||
|
let authTokenCollection: String = "SNAuthTokenCollection"
|
||||||
|
|
||||||
|
Storage.write(with: { transaction in
|
||||||
|
transaction.removeAllObjects(inCollection: lastMessageServerIDCollection)
|
||||||
|
transaction.removeAllObjects(inCollection: lastDeletionServerIDCollection)
|
||||||
|
transaction.removeAllObjects(inCollection: authTokenCollection)
|
||||||
|
|
||||||
|
self.save(with: transaction) // Intentionally capture self
|
||||||
|
}, completion: {
|
||||||
|
completion()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue