diff --git a/SignalServiceKit/src/Contacts/TSThread.m b/SignalServiceKit/src/Contacts/TSThread.m index 4ed7a2dfe..3e6ea4e1f 100644 --- a/SignalServiceKit/src/Contacts/TSThread.m +++ b/SignalServiceKit/src/Contacts/TSThread.m @@ -216,7 +216,7 @@ ConversationColorName const kConversationColorName_Default = ConversationColorNa if (!IsNoteToSelfEnabled()) { return NO; } - return [LKSessionProtocol isMessageNoteToSelf:self]; + return [LKSessionProtocol isThreadNoteToSelf:self]; } #pragma mark - To be subclassed. diff --git a/SignalServiceKit/src/Loki/Protocol/Friend Requests/FriendRequestProtocol.swift b/SignalServiceKit/src/Loki/Protocol/Friend Requests/FriendRequestProtocol.swift index d290be3b8..f4444e39a 100644 --- a/SignalServiceKit/src/Loki/Protocol/Friend Requests/FriendRequestProtocol.swift +++ b/SignalServiceKit/src/Loki/Protocol/Friend Requests/FriendRequestProtocol.swift @@ -15,19 +15,25 @@ public final class FriendRequestProtocol : NSObject { internal static var storage: OWSPrimaryStorage { OWSPrimaryStorage.shared() } + // MARK: - Enums + // FIXME: Better naming :( + @objc public enum FriendRequestUIState : Int { + case friends, received, sent, none + } + // MARK: - General @objc(shouldInputBarBeEnabledForThread:) public static func shouldInputBarBeEnabled(for thread: TSThread) -> Bool { // Friend requests have nothing to do with groups, so if this isn't a contact thread the input bar should be enabled guard let thread = thread as? TSContactThread else { return true } // If this is a note to self, the input bar should be enabled - if SessionProtocol.isMessageNoteToSelf(thread) { return true } + if thread.isNoteToSelf() { return true } let contactID = thread.contactIdentifier() var friendRequestStatuses: [LKFriendRequestStatus] = [] storage.dbReadConnection.read { transaction in - let linkedDeviceThreads = LokiDatabaseUtilities.getLinkedDeviceThreads(for: contactID, in: transaction) - friendRequestStatuses = linkedDeviceThreads.map { device in - return storage.getFriendRequestStatus(for: device.contactIdentifier(), transaction: transaction) + let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: contactID, in: transaction) + friendRequestStatuses = linkedDevices.map { device in + return storage.getFriendRequestStatus(for: device, transaction: transaction) } } // If the current user is friends with any of the other user's devices, the input bar should be enabled @@ -43,13 +49,13 @@ public final class FriendRequestProtocol : NSObject { // Friend requests have nothing to do with groups, so if this isn't a contact thread the attachment button should be enabled guard let thread = thread as? TSContactThread else { return true } // If this is a note to self, the attachment button should be enabled - if SessionProtocol.isMessageNoteToSelf(thread) { return true } + if thread.isNoteToSelf() { return true } let contactID = thread.contactIdentifier() var friendRequestStatuses: [LKFriendRequestStatus] = [] storage.dbReadConnection.read { transaction in - let linkedDeviceThreads = LokiDatabaseUtilities.getLinkedDeviceThreads(for: contactID, in: transaction) - friendRequestStatuses = linkedDeviceThreads.map { thread in - storage.getFriendRequestStatus(for: thread.contactIdentifier(), transaction: transaction) + let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: contactID, in: transaction) + friendRequestStatuses = linkedDevices.map { device in + storage.getFriendRequestStatus(for: device, transaction: transaction) } } // If the current user is friends with any of the other user's devices, the attachment button should be enabled @@ -58,6 +64,27 @@ public final class FriendRequestProtocol : NSObject { return false } + @objc(getFriendRequestUIStateForThread:) + public static func getFriendRequestUIState(for thread: TSThread) -> FriendRequestUIState { + // Friend requests have nothing to do with groups + guard let thread = thread as? TSContactThread else { return .none } + // If this is a note to self then we don't want to show the friend request + if thread.isNoteToSelf() { return .none } + var friendRequestStatuses: [LKFriendRequestStatus] = [] + storage.dbReadConnection.read { transaction in + let linkedDevices = LokiDatabaseUtilities.getLinkedDeviceHexEncodedPublicKeys(for: thread.contactIdentifier(), in: transaction) + friendRequestStatuses = linkedDevices.map { device in + return storage.getFriendRequestStatus(for: device, transaction: transaction) + } + } + + if friendRequestStatuses.contains(where: { $0 == .friends }) { return .friends } + if friendRequestStatuses.contains(where: { $0 == .requestReceived }) { return .received } + if friendRequestStatuses.contains(where: { $0 == .requestSent }) { return .sent } + + return .none + } + // MARK: - Sending @objc(acceptFriendRequestFromHexEncodedPublicKey:using:) public static func acceptFriendRequest(from hexEncodedPublicKey: String, using transaction: YapDatabaseReadWriteTransaction) { diff --git a/SignalServiceKit/src/Loki/Protocol/Friend Requests/FriendRequestProtocolTests.swift b/SignalServiceKit/src/Loki/Protocol/Friend Requests/FriendRequestProtocolTests.swift index 93959d405..6e8d97859 100644 --- a/SignalServiceKit/src/Loki/Protocol/Friend Requests/FriendRequestProtocolTests.swift +++ b/SignalServiceKit/src/Loki/Protocol/Friend Requests/FriendRequestProtocolTests.swift @@ -104,7 +104,7 @@ class FriendRequestProtocolTests : XCTestCase { func test_shouldInputBarBeEnabledReturnsTrueWhenStatusIsNotPending() { let validStatuses: [LKFriendRequestStatus] = [.none, .requestExpired, .friends] - let device = Curve25519.generateKeyPair().hexEncodedPublicKey + let device = generateHexEncodedPublicKey() let thread = createContactThread(for: device) for status in validStatuses { @@ -117,7 +117,7 @@ class FriendRequestProtocolTests : XCTestCase { func test_shouldInputBarBeEnabledReturnsFalseWhenStatusIsPending() { let pendingStatuses: [LKFriendRequestStatus] = [.requestSending, .requestSent, .requestReceived] - let device = Curve25519.generateKeyPair().hexEncodedPublicKey + let device = generateHexEncodedPublicKey() let thread = createContactThread(for: device) for status in pendingStatuses { @@ -202,6 +202,25 @@ class FriendRequestProtocolTests : XCTestCase { } } + func test_shouldInputBarEnabledShouldStillWorkIfLinkedDeviceThreadDoesNotExist() { + let master = generateHexEncodedPublicKey() + let slave = generateHexEncodedPublicKey() + + guard let masterDevice = getDevice(for: master) else { return XCTFail() } + guard let slaveDevice = getDevice(for: slave) else { return XCTFail() } + + let deviceLink = DeviceLink(between: masterDevice, and: slaveDevice) + storage.dbReadWriteConnection.readWrite { transaction in + self.storage.addDeviceLink(deviceLink, in: transaction) + self.storage.setFriendRequestStatus(.requestSent, for: master, transaction: transaction) + self.storage.setFriendRequestStatus(.friends, for: slave, transaction: transaction) + } + + let masterThread = createContactThread(for: master) + + XCTAssertTrue(FriendRequestProtocol.shouldInputBarBeEnabled(for: masterThread)) + } + // MARK: - shouldAttachmentButtonBeEnabled func test_shouldAttachmentButtonBeEnabledReturnsTrueOnGroupThread() { @@ -234,7 +253,7 @@ class FriendRequestProtocolTests : XCTestCase { } func test_shouldAttachmentButtonBeEnabledReturnsTrueWhenFriends() { - let device = Curve25519.generateKeyPair().hexEncodedPublicKey + let device = generateHexEncodedPublicKey() let thread = createContactThread(for: device) storage.dbReadWriteConnection.readWrite { transaction in @@ -245,7 +264,7 @@ class FriendRequestProtocolTests : XCTestCase { func test_shouldAttachmentButtonBeEnabledReturnsFalseWhenNotFriends() { let nonFriendStatuses: [LKFriendRequestStatus] = [.requestSending, .requestSent, .requestReceived, .none, .requestExpired] - let device = Curve25519.generateKeyPair().hexEncodedPublicKey + let device = generateHexEncodedPublicKey() let thread = createContactThread(for: device) for status in nonFriendStatuses { @@ -277,16 +296,162 @@ class FriendRequestProtocolTests : XCTestCase { XCTAssertTrue(FriendRequestProtocol.shouldAttachmentButtonBeEnabled(for: slaveThread)) } + func test_shouldAttachmentButtonBeEnabledShouldStillWorkIfLinkedDeviceThreadDoesNotExist() { + let master = generateHexEncodedPublicKey() + let slave = generateHexEncodedPublicKey() + + guard let masterDevice = getDevice(for: master) else { return XCTFail() } + guard let slaveDevice = getDevice(for: slave) else { return XCTFail() } + + let deviceLink = DeviceLink(between: masterDevice, and: slaveDevice) + storage.dbReadWriteConnection.readWrite { transaction in + self.storage.addDeviceLink(deviceLink, in: transaction) + self.storage.setFriendRequestStatus(.none, for: master, transaction: transaction) + self.storage.setFriendRequestStatus(.friends, for: slave, transaction: transaction) + } + + let masterThread = createContactThread(for: master) + + XCTAssertTrue(FriendRequestProtocol.shouldAttachmentButtonBeEnabled(for: masterThread)) + } + + // MARK: - getFriendRequestUIState + + func test_getFriendRequestUIStateShouldReturnNoneForGroupThreads() { + let allGroupTypes: [GroupType] = [.closedGroup, .openGroup, .rssFeed] + for groupType in allGroupTypes { + guard let groupThread = createGroupThread(groupType: groupType) else { return XCTFail() } + XCTAssertTrue(FriendRequestProtocol.getFriendRequestUIState(for: groupThread) == .none) + } + } + + func test_getFriendRequestUIStateShouldReturnNoneOnNoteToSelf() { + guard let master = OWSIdentityManager.shared().identityKeyPair()?.hexEncodedPublicKey else { return XCTFail() } + let slave = generateHexEncodedPublicKey() + + guard let masterDevice = getDevice(for: master) else { return XCTFail() } + guard let slaveDevice = getDevice(for: slave) else { return XCTFail() } + + let deviceLink = DeviceLink(between: masterDevice, and: slaveDevice) + storage.dbReadWriteConnection.readWrite { transaction in + self.storage.addDeviceLink(deviceLink, in: transaction) + self.storage.setFriendRequestStatus(.friends, for: master, transaction: transaction) + self.storage.setFriendRequestStatus(.friends, for: slave, transaction: transaction) + } + + let masterThread = createContactThread(for: master) + let slaveThread = createContactThread(for: slave) + + XCTAssertTrue(FriendRequestProtocol.getFriendRequestUIState(for: masterThread) == .none) + XCTAssertTrue(FriendRequestProtocol.getFriendRequestUIState(for: slaveThread) == .none ) + } + + func test_getFriendRequestUIStateShouldReturnTheCorrectStates() { + let bob = generateHexEncodedPublicKey() + let bobThread = createContactThread(for: bob) + + let expectedStates: [LKFriendRequestStatus : FriendRequestProtocol.FriendRequestUIState] = [ + .none: .none, + .requestExpired: .none, + .requestSending: .none, + .requestSent: .sent, + .requestReceived: .received, + .friends: .friends, + ] + + for (friendRequestStatus, uiState) in expectedStates { + storage.dbReadWriteConnection.readWrite { transaction in + self.storage.setFriendRequestStatus(friendRequestStatus, for: bob, transaction: transaction) + } + XCTAssertEqual(FriendRequestProtocol.getFriendRequestUIState(for: bobThread), uiState, "Expected FriendRequestUIState to be \(uiState)") + } + } + + func test_getFriendRequestUIStateShouldWorkWithMultiDevice() { + let master = generateHexEncodedPublicKey() + let slave = generateHexEncodedPublicKey() + + guard let masterDevice = getDevice(for: master) else { return XCTFail() } + guard let slaveDevice = getDevice(for: slave) else { return XCTFail() } + + let deviceLink = DeviceLink(between: masterDevice, and: slaveDevice) + storage.dbReadWriteConnection.readWrite { transaction in + self.storage.addDeviceLink(deviceLink, in: transaction) + self.storage.setFriendRequestStatus(.none, for: master, transaction: transaction) + } + + let masterThread = createContactThread(for: master) + let slaveThread = createContactThread(for: slave) + + let expectedStates: [LKFriendRequestStatus : FriendRequestProtocol.FriendRequestUIState] = [ + .none: .none, + .requestExpired: .none, + .requestSending: .none, + .requestSent: .sent, + .requestReceived: .received, + .friends: .friends, + ] + + for (friendRequestStatus, uiState) in expectedStates { + storage.dbReadWriteConnection.readWrite { transaction in + self.storage.setFriendRequestStatus(friendRequestStatus, for: slave, transaction: transaction) + } + + XCTAssertEqual(FriendRequestProtocol.getFriendRequestUIState(for: masterThread), uiState, "Expected FriendRequestUIState to be \(uiState.rawValue)") + XCTAssertEqual(FriendRequestProtocol.getFriendRequestUIState(for: slaveThread), uiState, "Expected FriendRequestUIState to be \(uiState.rawValue)") + } + } + + func test_getFriendRequestUIStateShouldPreferFriendsOverRequestReceived() { + // Case: We don't want to confuse the user by showing a friend request box when they're already friends + let master = generateHexEncodedPublicKey() + let slave = generateHexEncodedPublicKey() + + guard let masterDevice = getDevice(for: master) else { return XCTFail() } + guard let slaveDevice = getDevice(for: slave) else { return XCTFail() } + + let masterThread = createContactThread(for: master) + + let deviceLink = DeviceLink(between: masterDevice, and: slaveDevice) + storage.dbReadWriteConnection.readWrite { transaction in + self.storage.addDeviceLink(deviceLink, in: transaction) + self.storage.setFriendRequestStatus(.requestReceived, for: master, transaction: transaction) + self.storage.setFriendRequestStatus(.friends, for: slave, transaction: transaction) + } + + XCTAssertTrue(FriendRequestProtocol.getFriendRequestUIState(for: masterThread) == .friends) + } + + func test_getFriendRequestUIStateShouldPreferReceivedOverSent() { + // Case: We sent Bob a friend request and he sent one back to us through another device. + // If something went wrong then we should be able to fallback to manually accepting the friend request even if we sent one. + let master = generateHexEncodedPublicKey() + let slave = generateHexEncodedPublicKey() + + guard let masterDevice = getDevice(for: master) else { return XCTFail() } + guard let slaveDevice = getDevice(for: slave) else { return XCTFail() } + + let masterThread = createContactThread(for: master) + + let deviceLink = DeviceLink(between: masterDevice, and: slaveDevice) + storage.dbReadWriteConnection.readWrite { transaction in + self.storage.addDeviceLink(deviceLink, in: transaction) + self.storage.setFriendRequestStatus(.requestSent, for: master, transaction: transaction) + self.storage.setFriendRequestStatus(.requestReceived, for: slave, transaction: transaction) + } + + XCTAssertTrue(FriendRequestProtocol.getFriendRequestUIState(for: masterThread) == .received) + } + // MARK: - acceptFriendRequest // TODO: Add test to see if message was sent? func test_acceptFriendRequestShouldSetStatusToFriendsIfWeReceivedAFriendRequest() { // Case: Bob sent us a friend request, we should become friends with him on accepting - let bob = Curve25519.generateKeyPair().hexEncodedPublicKey + let bob = generateHexEncodedPublicKey() storage.dbReadWriteConnection.readWrite { transaction in self.storage.setFriendRequestStatus(.requestReceived, for: bob, transaction: transaction) - } storage.dbReadWriteConnection.readWrite { transaction in @@ -300,7 +465,7 @@ class FriendRequestProtocolTests : XCTestCase { // Since user accepted then we should send a friend request message let statuses: [LKFriendRequestStatus] = [.none, .requestExpired] for status in statuses { - let bob = Curve25519.generateKeyPair().hexEncodedPublicKey + let bob = generateHexEncodedPublicKey() storage.dbReadWriteConnection.readWrite { transaction in self.storage.setFriendRequestStatus(status, for: bob, transaction: transaction) } @@ -315,7 +480,7 @@ class FriendRequestProtocolTests : XCTestCase { func test_acceptFriendRequestShouldNotDoAnythingIfRequestHasBeenSent() { // Case: We sent Bob a friend request. // We can't accept because we don't have keys to communicate with Bob. - let bob = Curve25519.generateKeyPair().hexEncodedPublicKey + let bob = generateHexEncodedPublicKey() storage.dbReadWriteConnection.readWrite { transaction in self.storage.setFriendRequestStatus(.requestSent, for: bob, transaction: transaction) diff --git a/SignalServiceKit/src/Loki/Protocol/SessionProtocol.swift b/SignalServiceKit/src/Loki/Protocol/SessionProtocol.swift index a7550aa29..82b8a344f 100644 --- a/SignalServiceKit/src/Loki/Protocol/SessionProtocol.swift +++ b/SignalServiceKit/src/Loki/Protocol/SessionProtocol.swift @@ -67,8 +67,8 @@ public final class SessionProtocol : NSObject { // TODO: Check that the behaviors described above make sense - @objc(isMessageNoteToSelf:) - public static func isMessageNoteToSelf(_ thread: TSThread) -> Bool { + @objc(isThreadNoteToSelf:) + public static func isThreadNoteToSelf(_ thread: TSThread) -> Bool { guard let thread = thread as? TSContactThread else { return false } var isNoteToSelf = false storage.dbReadConnection.read { transaction in @@ -101,14 +101,14 @@ public final class SessionProtocol : NSObject { /// send them if certain conditions are met. @objc(shouldSendTypingIndicatorForThread:) public static func shouldSendTypingIndicator(for thread: TSThread) -> Bool { - return !thread.isGroupThread() && !isMessageNoteToSelf(thread) + return !thread.isGroupThread() && !isThreadNoteToSelf(thread) } // MARK: Receipts // Used from OWSReadReceiptManager @objc(shouldSendReadReceiptForThread:) public static func shouldSendReadReceipt(for thread: TSThread) -> Bool { - return !isMessageNoteToSelf(thread) && !thread.isGroupThread() + return !isThreadNoteToSelf(thread) && !thread.isGroupThread() } // TODO: Not sure how these two relate. EDIT: I think the one below is used to block delivery receipts. That means that