From 60dc4a8dc0c06ddd884d0dcc248733056153fdac Mon Sep 17 00:00:00 2001 From: Max Radermacher Date: Tue, 7 Jan 2025 16:40:13 -0600 Subject: [PATCH] =?UTF-8?q?Don=E2=80=99t=20pass=20around=20TSGroupThread?= =?UTF-8?q?=20in=20group=20calls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Signal/Calls/CallKitCallManager.swift | 13 ++-- Signal/Calls/CallKitIdStore.swift | 12 ++- Signal/Calls/CallService.swift | 73 +++++++++++-------- Signal/Calls/CallStarter.swift | 42 ++++++----- Signal/Calls/CallTarget.swift | 3 +- Signal/Calls/CurrentCall.swift | 5 +- .../GroupCallAccessoryMessageDelegate.swift | 30 ++++++-- ...GroupCallRecordRingingCleanupManager.swift | 64 ++++++++-------- Signal/Calls/GroupThreadCall.swift | 34 +++++++-- Signal/Calls/SignalCall.swift | 2 +- Signal/Calls/UserInterface/CallHeader.swift | 18 +++-- .../UserInterface/CallKitCallUIAdaptee.swift | 13 +++- .../CallsListViewController.swift | 60 ++++++++++----- .../GroupCallViewController.swift | 18 +++-- .../UserInterface/NewCallViewController.swift | 9 ++- Signal/Calls/WebRTCCallMessageHandler.swift | 4 +- .../Components/CVComponentState.swift | 4 +- .../Components/CVComponentSystemMessage.swift | 17 +++-- .../ConversationViewController+Calls.swift | 11 +-- .../Loading/CVItemViewState.swift | 12 ++- .../Loading/CVLoadCoordinator.swift | 16 ++-- .../Loading/CVViewStateSnapshot.swift | 7 +- .../NotificationActionHandler.swift | 35 ++++++--- .../ConversationHeaderBuilder.swift | 23 ++++-- ...stViewController+ViewModelLoaderTest.swift | 2 +- SignalNSE/NSECallMessageHandler.swift | 4 +- .../Calls/CallMessageHandler.swift | 2 +- SignalServiceKit/Calls/GroupCallManager.swift | 48 +++++++----- .../Calls/GroupCallPeekClient.swift | 37 +++++----- .../Contacts/Threads/TSGroupThread.swift | 16 ++++ .../Contacts/Threads/ThreadStore.swift | 4 + .../Environment/NoopCallMessageHandler.swift | 2 +- SignalServiceKit/Groups/GroupsV2.swift | 4 +- SignalServiceKit/Groups/GroupsV2Impl.swift | 8 +- .../Messages/MessageReceiver.swift | 12 +-- .../MessageBackupIntegrationTests.swift | 4 +- SignalUI/Views/ConversationAvatarView.swift | 13 ++++ 37 files changed, 429 insertions(+), 252 deletions(-) diff --git a/Signal/Calls/CallKitCallManager.swift b/Signal/Calls/CallKitCallManager.swift index 7ca6cb3ba3..1d7a95b158 100644 --- a/Signal/Calls/CallKitCallManager.swift +++ b/Signal/Calls/CallKitCallManager.swift @@ -26,13 +26,13 @@ final class CallKitCallManager { static let kGroupThreadCallHandlePrefix = "SignalGroup:" static let kCallLinkCallHandlePrefix = "SignalCall:" - private static func decodeGroupId(fromIntentHandle handle: String) -> Data? { + private static func decodeGroupId(fromIntentHandle handle: String) -> GroupIdentifier? { let prefix = handle.prefix(kGroupThreadCallHandlePrefix.count) guard prefix == kGroupThreadCallHandlePrefix else { return nil } do { - return try Data.data(fromBase64Url: String(handle[prefix.endIndex...])) + return try GroupIdentifier(contents: [UInt8](Data.data(fromBase64Url: String(handle[prefix.endIndex...])))) } catch { // ignore the error return nil @@ -67,12 +67,12 @@ final class CallKitCallManager { case .groupThread(let groupThreadCall): if !showNamesOnCallScreen { let callKitId = CallKitCallManager.kAnonymousCallHandlePrefix + call.localId.uuidString - CallKitIdStore.setGroupThread(groupThreadCall.groupThread, forCallKitId: callKitId) + CallKitIdStore.setGroupId(groupThreadCall.groupId, forCallKitId: callKitId) type = .generic value = callKitId } else { type = .generic - value = Self.kGroupThreadCallHandlePrefix + groupThreadCall.groupThread.groupModel.groupId.asBase64Url + value = Self.kGroupThreadCallHandlePrefix + groupThreadCall.groupId.serialize().asData.asBase64Url } case .callLink(let callLinkCall): let callKitId: String @@ -91,7 +91,6 @@ final class CallKitCallManager { static func callTargetForHandleWithSneakyTransaction(_ handle: String) -> CallTarget? { owsAssertDebug(!handle.isEmpty) - let databaseStorage = SSKEnvironment.shared.databaseStorageRef let phoneNumberUtil = SSKEnvironment.shared.phoneNumberUtilRef let tsAccountManager = DependenciesBridge.shared.tsAccountManager @@ -100,9 +99,7 @@ final class CallKitCallManager { } if let groupId = decodeGroupId(fromIntentHandle: handle) { - return databaseStorage.read { tx in - return TSGroupThread.fetch(groupId: groupId, transaction: tx).map { .groupThread($0) } - } + return .groupThread(groupId) } if let serviceId = try? ServiceId.parseFrom(serviceIdString: handle) { diff --git a/Signal/Calls/CallKitIdStore.swift b/Signal/Calls/CallKitIdStore.swift index fefba6e6a7..5deeff0263 100644 --- a/Signal/Calls/CallKitIdStore.swift +++ b/Signal/Calls/CallKitIdStore.swift @@ -4,6 +4,7 @@ // import Foundation +import LibSignalClient import SignalRingRTC import SignalServiceKit import SignalUI @@ -14,7 +15,7 @@ class CallKitIdStore { private static let groupIdStore = KeyValueStore(collection: "TSStorageManagerCallKitIdToGroupId") private static let callLinkStore = KeyValueStore(collection: "CallKitIdToCallLink") - static func setGroupThread(_ thread: TSGroupThread, forCallKitId callKitId: String) { + static func setGroupId(_ groupId: GroupIdentifier, forCallKitId callKitId: String) { SSKEnvironment.shared.databaseStorageRef.write { tx in // Make sure it doesn't exist, but only in DEBUG builds. assert(!phoneNumberStore.hasValue(callKitId, transaction: tx.asV2Read)) @@ -22,7 +23,7 @@ class CallKitIdStore { assert(!groupIdStore.hasValue(callKitId, transaction: tx.asV2Read)) assert(!callLinkStore.hasValue(callKitId, transaction: tx.asV2Read)) - groupIdStore.setData(thread.groupModel.groupId, key: callKitId, transaction: tx.asV2Write) + groupIdStore.setData(groupId.serialize().asData, key: callKitId, transaction: tx.asV2Write) } } @@ -67,8 +68,11 @@ class CallKitIdStore { } // Next try group calls - if let groupId = groupIdStore.getData(callKitId, transaction: tx.asV2Read) { - return TSGroupThread.fetch(groupId: groupId, transaction: tx).map { .groupThread($0) } + if + let groupIdData = groupIdStore.getData(callKitId, transaction: tx.asV2Read), + let groupId = try? GroupIdentifier(contents: [UInt8](groupIdData)) + { + return .groupThread(groupId) } // Check the phone number store, for very old 1:1 calls. diff --git a/Signal/Calls/CallService.swift b/Signal/Calls/CallService.swift index accaf767e1..f2c772611a 100644 --- a/Signal/Calls/CallService.swift +++ b/Signal/Calls/CallService.swift @@ -267,7 +267,7 @@ final class CallService: CallServiceStateObserver, CallServiceStateDelegate { // Kick off a peek now that we've disconnected to get an updated participant state. Task { await self.groupCallManager.peekGroupCallAndUpdateThread( - call.groupThread, + forGroupId: call.groupId, peekTrigger: .localEvent() ) } @@ -451,16 +451,14 @@ final class CallService: CallServiceStateObserver, CallServiceStateDelegate { individualCallService.handleLocalHangupCall(call) case .groupThread(let groupThreadCall): if case .incomingRing(_, let ringId) = groupThreadCall.groupCallRingState { - let groupThread = groupThreadCall.groupThread - groupCallAccessoryMessageDelegate.localDeviceDeclinedGroupRing( ringId: ringId, - groupThread: groupThread + groupId: groupThreadCall.groupId ) do { try callManager.cancelGroupRing( - groupId: groupThread.groupId, + groupId: groupThreadCall.groupId.serialize().asData, ringId: ringId, reason: .declinedByUser ) @@ -523,14 +521,12 @@ final class CallService: CallServiceStateObserver, CallServiceStateDelegate { // MARK: - @MainActor - func buildAndConnectGroupCall(for thread: TSGroupThread, isVideoMuted: Bool) -> (SignalCall, GroupThreadCall)? { - owsAssertDebug(thread.groupModel.groupsVersion == .V2) - + func buildAndConnectGroupCall(for groupId: GroupIdentifier, isVideoMuted: Bool) -> (SignalCall, GroupThreadCall)? { return _buildAndConnectGroupCall(isOutgoingVideoMuted: isVideoMuted) { () -> (SignalCall, GroupThreadCall)? in let videoCaptureController = VideoCaptureController() let sfuUrl = DebugFlags.callingUseTestSFU.get() ? TSConstants.sfuTestURL : TSConstants.sfuURL let ringRtcCall = callManager.createGroupCall( - groupId: thread.groupModel.groupId, + groupId: groupId.serialize().asData, sfuUrl: sfuUrl, hkdfExtraInfo: Data(), audioLevelsIntervalMillis: nil, @@ -542,9 +538,12 @@ final class CallService: CallServiceStateObserver, CallServiceStateDelegate { let groupThreadCall = GroupThreadCall( delegate: self, ringRtcCall: ringRtcCall, - groupThread: thread, + groupId: groupId, videoCaptureController: videoCaptureController ) + guard let groupThreadCall else { + return nil + } return (SignalCall(groupThreadCall: groupThreadCall), groupThreadCall) } } @@ -706,8 +705,8 @@ final class CallService: CallServiceStateObserver, CallServiceStateDelegate { switch callTarget { case .individual(let contactThread): Task { await self.initiateIndividualCall(thread: contactThread, isVideo: isVideo) } - case .groupThread(let groupThread): - GroupCallViewController.presentLobby(thread: groupThread, videoMuted: !isVideo) + case .groupThread(let groupId): + GroupCallViewController.presentLobby(forGroupId: groupId, videoMuted: !isVideo) case .callLink(let callLink): GroupCallViewController.presentLobby(for: callLink) } @@ -849,7 +848,8 @@ final class CallService: CallServiceStateObserver, CallServiceStateDelegate { do { membershipInfo = try self.databaseStorage.read { tx in try self.groupCallManager.groupCallPeekClient.groupMemberInfo( - groupThread: groupThreadCall.groupThread, tx: tx.asV2Read + forGroupId: groupThreadCall.groupId, + tx: tx.asV2Read ) } } catch { @@ -924,18 +924,17 @@ extension CallService: GroupCallObserver { audioService.playOutboundRing() } - let groupThread = call.groupThread if ringRtcCall.localDeviceState.isJoined { if let eraId = ringRtcCall.peekInfo?.eraId { groupCallAccessoryMessageDelegate.localDeviceMaybeJoinedGroupCall( eraId: eraId, - groupThread: groupThread, + groupId: call.groupId, groupCallRingState: call.groupCallRingState ) } } else { groupCallAccessoryMessageDelegate.localDeviceMaybeLeftGroupCall( - groupThread: groupThread, + groupId: call.groupId, groupCall: ringRtcCall ) } @@ -956,7 +955,7 @@ extension CallService: GroupCallObserver { switch call.concreteType { case .groupThread(let call): - let groupThread = call.groupThread + let groupId = call.groupId if ringRtcCall.localDeviceState.isJoined, @@ -964,7 +963,7 @@ extension CallService: GroupCallObserver { { groupCallAccessoryMessageDelegate.localDeviceMaybeJoinedGroupCall( eraId: eraId, - groupThread: groupThread, + groupId: call.groupId, groupCallRingState: call.groupCallRingState ) } @@ -972,7 +971,7 @@ extension CallService: GroupCallObserver { databaseStorage.asyncWrite { tx in self.groupCallManager.updateGroupCallModelsForPeek( peekInfo: peekInfo, - groupThread: groupThread, + groupId: groupId, triggerEventTimestamp: MessageTimestampGenerator.sharedInstance.generateTimestamp(), tx: tx ) @@ -1023,11 +1022,18 @@ extension CallService: GroupThreadCallDelegate { Logger.info("") let groupCall = call.ringRtcCall - let groupThread = call.groupThread Task { [groupCallManager] in + let databaseStorage = SSKEnvironment.shared.databaseStorageRef + let groupThread = databaseStorage.read { tx in + return TSGroupThread.fetch(forGroupId: call.groupId, tx: tx) + } + guard let groupModel = groupThread?.groupModel as? TSGroupModelV2 else { + owsFailDebug("Missing v2 model for group call.") + return + } do { - let proof = try await groupCallManager.groupCallPeekClient.fetchGroupMembershipProof(groupThread: groupThread) + let proof = try await groupCallManager.groupCallPeekClient.fetchGroupMembershipProof(secretParams: try groupModel.secretParams()) await MainActor.run { groupCall.updateMembershipProof(proof: proof) } @@ -1080,8 +1086,8 @@ extension CallService: DatabaseChangeDelegate { switch callServiceState.currentCall?.mode { case nil, .individual, .callLink: break - case .groupThread(let groupThreadCall): - if databaseChanges.didUpdate(thread: groupThreadCall.groupThread) { + case .groupThread(let call): + if databaseChanges.threadUniqueIds.contains(call.threadUniqueId) { updateGroupMembersForCurrentCallIfNecessary() } } @@ -1544,18 +1550,23 @@ extension CallService: CallManagerDelegate { enum RingAction { case cancel - case ring(TSGroupThread) + case ring(GroupIdentifier) } let action: RingAction = databaseStorage.read { transaction in - guard let thread = TSGroupThread.fetch(groupId: groupId, transaction: transaction) else { + guard let groupId = try? GroupIdentifier(contents: [UInt8](groupId)) else { + owsFailDebug("discarding group ring \(ringId) from \(senderAci) for invalid group") + return .cancel + } + + guard let thread = TSGroupThread.fetch(forGroupId: groupId, tx: transaction) else { owsFailDebug("discarding group ring \(ringId) from \(senderAci) for unknown group") return .cancel } guard GroupsV2MessageProcessor.discardMode( forMessageFrom: senderAci, - groupId: groupId, + groupId: groupId.serialize().asData, tx: transaction ) == .doNotDiscard else { Logger.warn("discarding group ring \(ringId) from \(senderAci)") @@ -1575,7 +1586,7 @@ extension CallService: CallManagerDelegate { owsFailDebug("unable to check cancellation table: \(error)") } - return .ring(thread) + return .ring(groupId) } switch action { @@ -1585,15 +1596,15 @@ extension CallService: CallManagerDelegate { } catch { owsFailDebug("RingRTC failed to cancel group ring \(ringId): \(error)") } - case .ring(let thread): + case .ring(let groupId): let currentCall = self.callServiceState.currentCall - if case .groupThread(let call) = currentCall?.mode, call.groupThread.uniqueId == thread.uniqueId { + if case .groupThread(let call) = currentCall?.mode, call.groupId.serialize() == groupId.serialize() { // We're already ringing or connected, or at the very least already in the lobby. return } guard currentCall == nil else { do { - try callManager.cancelGroupRing(groupId: groupId, ringId: ringId, reason: .busy) + try callManager.cancelGroupRing(groupId: groupId.serialize().asData, ringId: ringId, reason: .busy) } catch { owsFailDebug("RingRTC failed to cancel group ring \(ringId): \(error)") } @@ -1604,7 +1615,7 @@ extension CallService: CallManagerDelegate { // This keeps us from popping the "give permission to use your camera" alert before the user answers. let videoMuted = AVCaptureDevice.authorizationStatus(for: .video) != .authorized guard let (call, groupThreadCall) = buildAndConnectGroupCall( - for: thread, + for: groupId, isVideoMuted: videoMuted ) else { return owsFailDebug("Failed to build group call") diff --git a/Signal/Calls/CallStarter.swift b/Signal/Calls/CallStarter.swift index 3a3f1d957a..aeb08d21ab 100644 --- a/Signal/Calls/CallStarter.swift +++ b/Signal/Calls/CallStarter.swift @@ -3,6 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // +import LibSignalClient import SignalUI import SignalServiceKit import SignalRingRTC @@ -12,7 +13,7 @@ import SignalRingRTC struct CallStarter { private enum Recipient { case contactThread(TSContactThread, withVideo: Bool) - case groupThread(TSGroupThread) + case groupThread(GroupIdentifier) case callLink(CallLinkRootKey) } @@ -48,8 +49,8 @@ struct CallStarter { self.context = context } - init(groupThread: TSGroupThread, context: Context) { - self.recipient = .groupThread(groupThread) + init(groupId: GroupIdentifier, context: Context) { + self.recipient = .groupThread(groupId) self.context = context } @@ -83,9 +84,9 @@ struct CallStarter { callTarget = .individual(thread) callThread = thread isVideoCall = withVideo - case .groupThread(let thread): - callTarget = .groupThread(thread) - callThread = thread + case .groupThread(let groupId): + callTarget = .groupThread(groupId) + callThread = context.databaseStorage.read { tx in TSGroupThread.fetch(forGroupId: groupId, tx: tx)! } isVideoCall = true case .callLink(let rootKey): callTarget = .callLink(CallLink(rootKey: rootKey)) @@ -108,25 +109,26 @@ struct CallStarter { return .callStarted } - switch callTarget { - case .individual(let thread): - guard thread.canCall else { + if let thread = callThread as? TSGroupThread, thread.isBlockedByAnnouncementOnly { + Self.showBlockedByAnnouncementOnlySheet(from: viewController) + return .callNotStarted + } + + if let thread = callThread { + let canCall = { + switch thread { + case let thread as TSContactThread: return thread.canCall + case let thread as TSGroupThread: return thread.canCall + default: return false + } + }() + guard canCall else { owsFailDebug("Shouldn't be able to startCall if canCall is false") return .callNotStarted } self.whitelistThread(thread) - case .groupThread(let thread): - guard !thread.isBlockedByAnnouncementOnly else { - Self.showBlockedByAnnouncementOnlySheet(from: viewController) - return .callNotStarted - } - guard thread.canCall else { - return .callNotStarted - } - self.whitelistThread(thread) - case .callLink: - break } + context.callService.initiateCall(to: callTarget, isVideo: isVideoCall) return .callStarted } diff --git a/Signal/Calls/CallTarget.swift b/Signal/Calls/CallTarget.swift index c3906a997f..cdaa045ea9 100644 --- a/Signal/Calls/CallTarget.swift +++ b/Signal/Calls/CallTarget.swift @@ -4,12 +4,13 @@ // import Foundation +import LibSignalClient import SignalServiceKit import SignalUI enum CallTarget { case individual(TSContactThread) - case groupThread(TSGroupThread) + case groupThread(GroupIdentifier) case callLink(CallLink) } diff --git a/Signal/Calls/CurrentCall.swift b/Signal/Calls/CurrentCall.swift index 3f77b18f4a..d1252ce1f5 100644 --- a/Signal/Calls/CurrentCall.swift +++ b/Signal/Calls/CurrentCall.swift @@ -4,6 +4,7 @@ // import Foundation +import LibSignalClient import SignalServiceKit struct CurrentCall { @@ -18,12 +19,12 @@ struct CurrentCall { extension CurrentCall: CurrentCallProvider { var hasCurrentCall: Bool { self.get() != nil } - var currentGroupCallThread: TSGroupThread? { + var currentGroupThreadCallGroupId: GroupIdentifier? { switch self.get()?.mode { case nil, .individual, .callLink: return nil case .groupThread(let call): - return call.groupThread + return call.groupId } } } diff --git a/Signal/Calls/GroupCallAccessoryMessageDelegate.swift b/Signal/Calls/GroupCallAccessoryMessageDelegate.swift index d7ef2fe876..80c5e7f423 100644 --- a/Signal/Calls/GroupCallAccessoryMessageDelegate.swift +++ b/Signal/Calls/GroupCallAccessoryMessageDelegate.swift @@ -4,6 +4,7 @@ // import Foundation +import LibSignalClient import SignalRingRTC import SignalServiceKit @@ -36,7 +37,7 @@ protocol GroupCallAccessoryMessageDelegate: AnyObject, CallServiceStateObserver /// state is `.joined` and we have an era ID for the call. func localDeviceMaybeJoinedGroupCall( eraId: String, - groupThread: TSGroupThread, + groupId: GroupIdentifier, groupCallRingState: GroupThreadCall.GroupCallRingState ) @@ -50,7 +51,7 @@ protocol GroupCallAccessoryMessageDelegate: AnyObject, CallServiceStateObserver /// - Important /// This method must be called on the main thread. func localDeviceMaybeLeftGroupCall( - groupThread: TSGroupThread, + groupId: GroupIdentifier, groupCall: SignalRingRTC.GroupCall ) @@ -67,7 +68,7 @@ protocol GroupCallAccessoryMessageDelegate: AnyObject, CallServiceStateObserver /// This method must be called on the main thread. func localDeviceDeclinedGroupRing( ringId: Int64, - groupThread: TSGroupThread + groupId: GroupIdentifier ) } @@ -107,7 +108,7 @@ class GroupCallAccessoryMessageHandler: GroupCallAccessoryMessageDelegate { return } localDeviceMaybeLeftGroupCall( - groupThread: oldGroupThreadCall.groupThread, + groupId: oldGroupThreadCall.groupId, groupCall: oldGroupThreadCall.ringRtcCall ) localDeviceGroupCallDidEnd() @@ -117,7 +118,7 @@ class GroupCallAccessoryMessageHandler: GroupCallAccessoryMessageDelegate { func localDeviceMaybeJoinedGroupCall( eraId: String, - groupThread: TSGroupThread, + groupId: GroupIdentifier, groupCallRingState: GroupThreadCall.GroupCallRingState ) { AssertIsOnMainThread() @@ -128,6 +129,11 @@ class GroupCallAccessoryMessageHandler: GroupCallAccessoryMessageDelegate { logger.info("Sending join messages for call.") databaseStorage.asyncWrite { tx in + guard let groupThread = TSGroupThread.fetch(forGroupId: groupId, tx: tx) else { + owsFailDebug("Missing thread for group call.") + return + } + let groupCallUpdateMessage = self.sendGroupCallUpdateMessage( groupThread: groupThread, eraId: eraId, @@ -150,7 +156,7 @@ class GroupCallAccessoryMessageHandler: GroupCallAccessoryMessageDelegate { } func localDeviceMaybeLeftGroupCall( - groupThread: TSGroupThread, + groupId: GroupIdentifier, groupCall: SignalRingRTC.GroupCall ) { AssertIsOnMainThread() @@ -161,6 +167,11 @@ class GroupCallAccessoryMessageHandler: GroupCallAccessoryMessageDelegate { logger.info("Sending leave message for call.") databaseStorage.asyncWrite { tx in + guard let groupThread = TSGroupThread.fetch(forGroupId: groupId, tx: tx) else { + owsFailDebug("Missing thread for group call.") + return + } + _ = self.sendGroupCallUpdateMessage( groupThread: groupThread, eraId: groupCall.peekInfo?.eraId, @@ -177,11 +188,16 @@ class GroupCallAccessoryMessageHandler: GroupCallAccessoryMessageDelegate { func localDeviceDeclinedGroupRing( ringId: Int64, - groupThread: TSGroupThread + groupId: GroupIdentifier ) { AssertIsOnMainThread() databaseStorage.asyncWrite { tx in + guard let groupThread = TSGroupThread.fetch(forGroupId: groupId, tx: tx) else { + owsFailDebug("Missing thread for group call.") + return + } + self.groupCallRecordManager.createOrUpdateCallRecordForDeclinedRing( ringId: ringId, groupThread: groupThread, diff --git a/Signal/Calls/GroupCallRecordRingingCleanupManager.swift b/Signal/Calls/GroupCallRecordRingingCleanupManager.swift index 19daadb3bf..fa9def28f2 100644 --- a/Signal/Calls/GroupCallRecordRingingCleanupManager.swift +++ b/Signal/Calls/GroupCallRecordRingingCleanupManager.swift @@ -3,6 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // +import LibSignalClient import SignalRingRTC import SignalServiceKit @@ -102,25 +103,28 @@ class GroupCallRecordRingingCleanupManager { /// A little chunky – group by the group thread row ID, then map those /// groupings to load the group thread for each row ID. - let callRecordsByGroupThread: [(TSGroupThread, [CallRecord])] = Dictionary( + let callRecordsByGroupId: [(GroupIdentifier, [CallRecord])] = Dictionary( grouping: callRecordsToPeek, by: { $0.conversationId } - ).compactMap { (conversationId, callRecords) -> (TSGroupThread, [CallRecord])? in + ).compactMap { (conversationId, callRecords) -> (GroupIdentifier, [CallRecord])? in switch conversationId { case .thread(let threadRowId): - guard let groupThread = threadStore.fetchThread( - rowId: threadRowId, tx: tx - ) as? TSGroupThread else { return nil } - return (groupThread, callRecords) + guard + let groupThread = threadStore.fetchThread(rowId: threadRowId, tx: tx) as? TSGroupThread, + let groupId = try? groupThread.groupIdentifier + else { + return nil + } + return (groupId, callRecords) case .callLink(_): return nil } } - for (groupThread, callRecords) in callRecordsByGroupThread { + for (groupId, callRecords) in callRecordsByGroupId { Task { try await peekGroupAndNotifyIfNecessary( - groupThread: groupThread, + groupId: groupId, callRecords: callRecords ) } @@ -132,34 +136,32 @@ class GroupCallRecordRingingCleanupManager { /// matches one of the records for the group (i.e., the call that created /// the ringing record is still ongoing), posts a notification. private func peekGroupAndNotifyIfNecessary( - groupThread: TSGroupThread, + groupId: GroupIdentifier, callRecords: [CallRecord] ) async throws { - let peekInfo = try await self.groupCallPeekClient.fetchPeekInfo( - groupThread: groupThread - ) - - let interactionRowIdsMatchingCurrentCall = callRecords.compactMap { callRecord -> Int64? in - switch callRecord.interactionReference { - case .thread(let threadRowId, let interactionRowId): - owsPrecondition(threadRowId == groupThread.sqliteRowId!) - guard callRecord.callId == peekInfo.eraId.map({ callIdFromEra($0) }) else { - return nil - } - return interactionRowId - case .none: - owsFail("Must pass callRecords for groupThread.") - } - } - - owsAssertDebug(interactionRowIdsMatchingCurrentCall.count <= 1) + let peekInfo = try await self.groupCallPeekClient.fetchPeekInfo(groupId: groupId) + let callId = peekInfo.eraId.map({ callIdFromEra($0) }) await self.db.awaitableWrite { tx in // Reload the group thread, since it may have changed. - guard let groupThread = self.threadStore.fetchGroupThread( - uniqueId: groupThread.uniqueId, - tx: tx - ) else { owsFail("Where did the thread go?") } + guard let groupThread = self.threadStore.fetchGroupThread(groupId: groupId, tx: tx) else { + owsFail("Where did the thread go?") + } + + let interactionRowIdsMatchingCurrentCall = callRecords.compactMap { callRecord -> Int64? in + switch callRecord.interactionReference { + case .thread(let threadRowId, let interactionRowId): + owsPrecondition(threadRowId == groupThread.sqliteRowId!) + guard callRecord.callId == callId else { + return nil + } + return interactionRowId + case .none: + owsFail("Must pass callRecords for groupThread.") + } + } + + owsAssertDebug(interactionRowIdsMatchingCurrentCall.count <= 1) for interactionRowId in interactionRowIdsMatchingCurrentCall { let interaction = self.interactionStore.fetchInteraction(rowId: interactionRowId, tx: tx) diff --git a/Signal/Calls/GroupThreadCall.swift b/Signal/Calls/GroupThreadCall.swift index d441128921..d494184ba8 100644 --- a/Signal/Calls/GroupThreadCall.swift +++ b/Signal/Calls/GroupThreadCall.swift @@ -4,6 +4,7 @@ // import Foundation +import LibSignalClient import SignalRingRTC import SignalServiceKit import SignalUI @@ -16,20 +17,32 @@ protocol GroupThreadCallDelegate: AnyObject { final class GroupThreadCall: Signal.GroupCall { private weak var delegate: (any GroupThreadCallDelegate)? - let groupThread: TSGroupThread + let groupId: GroupIdentifier + let threadUniqueId: String var membershipDidChangeObserver: (any NSObjectProtocol)! - init( + init?( delegate: any GroupThreadCallDelegate, ringRtcCall: SignalRingRTC.GroupCall, - groupThread: TSGroupThread, + groupId: GroupIdentifier, videoCaptureController: VideoCaptureController ) { self.delegate = delegate - self.groupThread = groupThread + self.groupId = groupId + + let databaseStorage = SSKEnvironment.shared.databaseStorageRef + let groupThread = databaseStorage.read { tx in + return TSGroupThread.fetch(forGroupId: groupId, tx: tx) + } + guard let groupThread else { + owsFailDebug("Missing thread for active call.") + return nil + } + + self.threadUniqueId = groupThread.uniqueId super.init( - audioDescription: "[SignalCall] with group \(groupThread.groupModel.groupId)", + audioDescription: "[SignalCall] with group \(groupId.serialize().asData)", ringRtcCall: ringRtcCall, videoCaptureController: videoCaptureController ) @@ -100,10 +113,17 @@ final class GroupThreadCall: Signal.GroupCall { private func groupMembershipDidChange(_ notification: Notification) { // NotificationCenter dispatches by object identity rather than equality, // so we filter based on the thread ID here. - guard groupThread.uniqueId == notification.object as? String else { + guard threadUniqueId == notification.object as? String else { + return + } + let databaseStorage = SSKEnvironment.shared.databaseStorageRef + let groupThread = databaseStorage.read { tx in + return TSGroupThread.fetch(forGroupId: groupId, tx: tx) + } + guard let groupThread else { + owsFailDebug("Missing group thread for active call.") return } - SSKEnvironment.shared.databaseStorageRef.read(block: groupThread.anyReload(transaction:)) let groupModel = groupThread.groupModel let isGroupTooLarge = groupModel.groupMembers.count > RemoteConfig.current.maxGroupCallRingSize ringRestrictions.update(.groupTooLarge, present: isGroupTooLarge) diff --git a/Signal/Calls/SignalCall.swift b/Signal/Calls/SignalCall.swift index 1edd9101f3..4202f85d04 100644 --- a/Signal/Calls/SignalCall.swift +++ b/Signal/Calls/SignalCall.swift @@ -134,7 +134,7 @@ enum CallMode { switch (self, callTarget) { case (.individual(let call), .individual(let thread)) where call.thread.uniqueId == thread.uniqueId: return true - case (.groupThread(let call), .groupThread(let thread)) where call.groupThread.uniqueId == thread.uniqueId: + case (.groupThread(let call), .groupThread(let groupId)) where call.groupId.serialize() == groupId.serialize(): return true case (.callLink(let call), .callLink(let callLink)) where call.callLink.rootKey.bytes == callLink.rootKey.bytes: return true diff --git a/Signal/Calls/UserInterface/CallHeader.swift b/Signal/Calls/UserInterface/CallHeader.swift index 7b889ec8ef..402bf06a7b 100644 --- a/Signal/Calls/UserInterface/CallHeader.swift +++ b/Signal/Calls/UserInterface/CallHeader.swift @@ -93,14 +93,14 @@ class CallHeader: UIView { // Avatar switch groupCall.concreteType { - case .groupThread(let groupThreadCall): + case .groupThread(let call): let avatarView = ConversationAvatarView( sizeClass: .customDiameter(96), localUserDisplayMode: .asLocalUser, badged: false ) avatarView.updateWithSneakyTransactionIfNecessary { - $0.dataSource = .thread(groupThreadCall.groupThread) + $0.setGroupIdWithSneakyTransaction(groupId: call.groupId.serialize().asData) } let avatarPaddingView = UIView() avatarPaddingView.addSubview(avatarView) @@ -202,9 +202,12 @@ class CallHeader: UIView { } private func fetchGroupSizeAndMemberNamesWithSneakyTransaction(groupThreadCall: GroupThreadCall) -> (Int, [String]) { - let groupThread = groupThreadCall.groupThread return SSKEnvironment.shared.databaseStorageRef.read { transaction in // FIXME: Register for notifications so we can update if someone leaves the group while the screen is up? + guard let groupThread = TSGroupThread.fetch(forGroupId: groupThreadCall.groupId, tx: transaction) else { + owsFailDebug("Couldn't fetch thread for active call.") + return (0, [] as [String]) + } let memberNames = groupThread.sortedMemberNames( includingBlocked: false, useShortNameIfAvailable: true, @@ -460,8 +463,13 @@ class CallHeader: UIView { switch groupCall.concreteType { case .groupThread(let groupThreadCall): // FIXME: This should auto-update if the group name changes. - return SSKEnvironment.shared.databaseStorageRef.read { transaction in - SSKEnvironment.shared.contactManagerRef.displayName(for: groupThreadCall.groupThread, transaction: transaction) + let databaseStorage = SSKEnvironment.shared.databaseStorageRef + return databaseStorage.read { tx in + let contactManager = SSKEnvironment.shared.contactManagerRef + guard let groupThread = TSGroupThread.fetch(forGroupId: groupThreadCall.groupId, tx: tx) else { + return TSGroupThread.defaultGroupName + } + return contactManager.displayName(for: groupThread, transaction: tx) } case .callLink(let call): return call.callLinkState.localizedName diff --git a/Signal/Calls/UserInterface/CallKitCallUIAdaptee.swift b/Signal/Calls/UserInterface/CallKitCallUIAdaptee.swift index 8dd2a8ac9b..1d654137a0 100644 --- a/Signal/Calls/UserInterface/CallKitCallUIAdaptee.swift +++ b/Signal/Calls/UserInterface/CallKitCallUIAdaptee.swift @@ -113,7 +113,18 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, @preconcurrency CXPro ) case .groupThread(let call): if showNamesOnCallScreen { - return SSKEnvironment.shared.databaseStorageRef.read { tx in SSKEnvironment.shared.contactManagerRef.displayName(for: call.groupThread, transaction: tx) } + let groupName = SSKEnvironment.shared.databaseStorageRef.read { tx -> String? in + let groupThread = TSGroupThread.fetch(forGroupId: call.groupId, tx: tx) + guard let groupThread else { + owsFailDebug("Missing group thread for active call.") + return nil + } + let contactManager = SSKEnvironment.shared.contactManagerRef + return contactManager.displayName(for: groupThread, transaction: tx) + } + if let groupName { + return groupName + } } return OWSLocalizedString( "CALLKIT_ANONYMOUS_GROUP_NAME", diff --git a/Signal/Calls/UserInterface/CallsListViewController.swift b/Signal/Calls/UserInterface/CallsListViewController.swift index 14e8740ded..db99af2e66 100644 --- a/Signal/Calls/UserInterface/CallsListViewController.swift +++ b/Signal/Calls/UserInterface/CallsListViewController.swift @@ -3,6 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // +import LibSignalClient import SignalUI import SignalRingRTC import SignalServiceKit @@ -652,7 +653,12 @@ class CallsListViewController: OWSViewController, HomeTabViewController, CallSer case .individual(let call): conversationId = .thread(threadRowId: call.thread.sqliteRowId!) case .groupThread(let call): - conversationId = .thread(threadRowId: call.groupThread.sqliteRowId!) + let rowId = deps.db.read { tx in deps.threadStore.fetchGroupThread(groupId: call.groupId, tx: tx)?.sqliteRowId } + guard let rowId else { + owsFailDebug("Can't reload call with non-existent group thread.") + return + } + conversationId = .thread(threadRowId: rowId) case .callLink(let call): // Query the database separately when starting & ending calls because the // row will usually be inserted during the call (ie `rowId` may be nil when @@ -1069,7 +1075,7 @@ class CallsListViewController: OWSViewController, HomeTabViewController, CallSer case let groupThread as TSGroupThread: title = groupThread.groupModel.groupNameOrDefault medium = .video - recipientType = .group(groupThread: groupThread) + recipientType = .groupThread(groupId: groupThread.groupId) default: owsFail("Call thread was neither contact nor group! This should be impossible.") } @@ -1113,12 +1119,12 @@ class CallsListViewController: OWSViewController, HomeTabViewController, CallSer private var callLinkPeekDates = [Data: MonotonicDate]() private enum Peekable { - case groupThread(groupThread: TSGroupThread) + case groupThread(groupId: GroupIdentifier) case callLink(rootKey: CallLinkRootKey) var identifier: Data { switch self { - case .groupThread(let thread): thread.groupId + case .groupThread(let groupId): groupId.serialize().asData case .callLink(let rootKey): rootKey.deriveRoomId() } } @@ -1146,9 +1152,9 @@ class CallsListViewController: OWSViewController, HomeTabViewController, CallSer self.peekQueue.removeFirst(peekBatch.count) for peekable in peekBatch { switch peekable { - case .groupThread(let thread): + case .groupThread(let groupId): Task { [deps] in - await deps.groupCallManager.peekGroupCallAndUpdateThread(thread, peekTrigger: .localEvent()) + await deps.groupCallManager.peekGroupCallAndUpdateThread(forGroupId: groupId, peekTrigger: .localEvent()) } case .callLink(let rootKey): self.callLinkPeekDates[rootKey.deriveRoomId()] = MonotonicDate() @@ -1234,8 +1240,12 @@ class CallsListViewController: OWSViewController, HomeTabViewController, CallSer switch viewModel.recipientType { case .individual: break - case .group(groupThread: let groupThread): - addToPeekQueue(.groupThread(groupThread: groupThread)) + case .groupThread(groupId: let groupId): + guard let groupId = try? GroupIdentifier(contents: [UInt8](groupId)) else { + owsFailDebug("Can't peek group call with invalid group id.") + break + } + addToPeekQueue(.groupThread(groupId: groupId)) case .callLink(let rootKey): addToPeekQueue(.callLink(rootKey: rootKey)) } @@ -1247,7 +1257,7 @@ class CallsListViewController: OWSViewController, HomeTabViewController, CallSer continue } switch viewModel.recipientType { - case .individual, .group: + case .individual, .groupThread: break case .callLink(let rootKey): // Skip any where the link is more than 10 days old. @@ -1365,7 +1375,7 @@ class CallsListViewController: OWSViewController, HomeTabViewController, CallSer enum RecipientType { case individual(type: IndividualCallType, contactThread: TSContactThread) - case group(groupThread: TSGroupThread) + case groupThread(groupId: Data) case callLink(CallLinkRootKey) enum IndividualCallType { @@ -1718,7 +1728,7 @@ extension CallsListViewController: UITableViewDelegate { image: "arrow-square-upright-fill", title: Strings.goToChatActionTitle ) { [weak self] in - self?.goToChat(for: chatThread) + self?.goToChat(for: chatThread()!) } return .init(actions: [goToChatAction]) @@ -1829,7 +1839,7 @@ extension CallsListViewController: UITableViewDelegate { self?.startCall(from: viewModel, withVideo: false) } actions.append(audioCallAction) - case .group, .callLink: + case .groupThread, .callLink: break } @@ -1849,7 +1859,7 @@ extension CallsListViewController: UITableViewDelegate { image: Theme.iconImage(.contextMenuOpenInChat), attributes: [] ) { [weak self] _ in - self?.goToChat(for: chatThread) + self?.goToChat(for: chatThread()!) } actions.append(goToChatAction) } @@ -1910,10 +1920,11 @@ extension CallsListViewController: CallCellDelegate, NewCallViewControllerDelega withVideo: withVideo ?? (type == .video), context: self.callStarterContext ).startCall(from: self) - case let .group(groupThread): + case let .groupThread(groupId): owsPrecondition(withVideo != false, "Can't start voice call.") + let groupId = try! GroupIdentifier(contents: [UInt8](groupId)) CallStarter( - groupThread: groupThread, + groupId: groupId, context: self.callStarterContext ).startCall(from: self) case .callLink(let rootKey): @@ -2082,7 +2093,10 @@ extension CallsListViewController: CallCellDelegate, NewCallViewControllerDelega AssertIsOnMainThread() switch viewModel.recipientType { - case .individual(type: _, let thread as TSThread), .group(let thread as TSThread): + case .individual(type: _, let thread): + showCallInfo(forThread: thread, callRecords: viewModel.callRecords) + case .groupThread(let groupId): + let thread = deps.db.read { tx in deps.threadStore.fetchGroupThread(groupId: groupId, tx: tx)! } showCallInfo(forThread: thread, callRecords: viewModel.callRecords) case .callLink(let rootKey): showCallInfo(forRootKey: rootKey, callRecords: viewModel.callRecords) @@ -2134,10 +2148,14 @@ extension CallsListViewController: CallCellDelegate, NewCallViewControllerDelega // MARK: NewCallViewControllerDelegate - private func goToChatThread(from viewModel: CallViewModel) -> TSThread? { + private func goToChatThread(from viewModel: CallViewModel) -> (() -> TSThread?)? { switch viewModel.recipientType { - case .individual(type: _, let thread as TSThread), .group(let thread as TSThread): - return thread + case .individual(type: _, let thread): + return { thread } + case .groupThread(let groupId): + return { [deps] in + return deps.db.read { tx in deps.threadStore.fetchGroupThread(groupId: groupId, tx: tx) } + } case .callLink(_): return nil } @@ -2396,8 +2414,10 @@ private extension CallsListViewController { avatarView.updateWithSneakyTransactionIfNecessary { configuration in switch viewModel.recipientType { - case .individual(type: _, let thread as TSThread), .group(let thread as TSThread): + case .individual(type: _, let thread): configuration.dataSource = .thread(thread) + case .groupThread(let groupId): + configuration.setGroupIdWithSneakyTransaction(groupId: groupId) case .callLink(let rootKey): configuration.dataSource = .asset(avatar: CommonCallLinksUI.callLinkIcon(rootKey: rootKey), badge: nil) } diff --git a/Signal/Calls/UserInterface/GroupCallViewController.swift b/Signal/Calls/UserInterface/GroupCallViewController.swift index eeb69e5518..0c457fa959 100644 --- a/Signal/Calls/UserInterface/GroupCallViewController.swift +++ b/Signal/Calls/UserInterface/GroupCallViewController.swift @@ -354,11 +354,11 @@ final class GroupCallViewController: UIViewController { } } - static func presentLobby(thread: TSGroupThread, videoMuted: Bool = false) { + static func presentLobby(forGroupId groupId: GroupIdentifier, videoMuted: Bool = false) { self._presentLobby { viewController in let result = await self._prepareLobby(from: viewController, shouldAskForCameraPermission: !videoMuted) { let callService = AppEnvironment.shared.callService! - return callService.buildAndConnectGroupCall(for: thread, isVideoMuted: videoMuted) + return callService.buildAndConnectGroupCall(for: groupId, isVideoMuted: videoMuted) } await SSKEnvironment.shared.databaseStorageRef.awaitableWrite { tx in // Dismiss the group call tooltip @@ -1558,7 +1558,8 @@ extension GroupCallViewController: CallViewControllerWindowReference { ringRtcCall.localDeviceState.joinState == .notJoined { // If we haven't joined the call yet, we want to alert for all members of the group - addressesToCheck = groupThreadCall.groupThread.recipientAddresses(with: transaction) + let groupThread = TSGroupThread.fetch(forGroupId: groupThreadCall.groupId, tx: transaction) + addressesToCheck = groupThread!.recipientAddresses(with: transaction) } else { // If we are in the call, we only care about safety numbers for the active call participants addressesToCheck = ringRtcCall.remoteDeviceStates.map { $0.value.address } @@ -1604,9 +1605,16 @@ extension GroupCallViewController: CallViewControllerWindowReference { let atLeastOneUnresolvedPresentAtJoin = unresolvedAddresses.contains { membersAtJoin?.contains($0) ?? false } switch groupCall.concreteType { case .groupThread(let call): + let databaseStorage = SSKEnvironment.shared.databaseStorageRef + let groupThread = databaseStorage.read { tx in + return TSGroupThread.fetch(forGroupId: call.groupId, tx: tx) + } + guard let groupThread else { + owsFail("Missing thread for active call.") + } SSKEnvironment.shared.notificationPresenterRef.notifyForGroupCallSafetyNumberChange( - callTitle: call.groupThread.groupNameOrDefault, - threadUniqueId: call.groupThread.uniqueId, + callTitle: groupThread.groupNameOrDefault, + threadUniqueId: groupThread.uniqueId, roomId: nil, presentAtJoin: atLeastOneUnresolvedPresentAtJoin ) diff --git a/Signal/Calls/UserInterface/NewCallViewController.swift b/Signal/Calls/UserInterface/NewCallViewController.swift index ad2941a2c8..8b27f6977d 100644 --- a/Signal/Calls/UserInterface/NewCallViewController.swift +++ b/Signal/Calls/UserInterface/NewCallViewController.swift @@ -3,6 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // +import LibSignalClient import SignalUI import SignalServiceKit @@ -52,9 +53,9 @@ class NewCallViewController: RecipientPickerContainerViewController { } - private func startGroupCall(thread: TSGroupThread) { + private func startGroupCall(groupId: GroupIdentifier) { self.startCall(callStarter: CallStarter( - groupThread: thread, + groupId: groupId, context: self.callStarterContext )) } @@ -120,7 +121,7 @@ extension NewCallViewController: RecipientContextMenuHelperDelegate { [ goToChatAction(thread: groupThread), startVideoCallAction { [weak self] _ in - self?.startGroupCall(thread: groupThread) + self?.startGroupCall(groupId: try! groupThread.groupIdentifier) } ] } @@ -139,7 +140,7 @@ extension NewCallViewController: RecipientPickerDelegate, UsernameLinkScanDelega let thread = TSContactThread.getOrCreateThread(contactAddress: address) startIndividualCall(thread: thread, withVideo: false) case let .group(groupThread): - startGroupCall(thread: groupThread) + startGroupCall(groupId: try! groupThread.groupIdentifier) } } diff --git a/Signal/Calls/WebRTCCallMessageHandler.swift b/Signal/Calls/WebRTCCallMessageHandler.swift index 00120b73bf..8adde79b00 100644 --- a/Signal/Calls/WebRTCCallMessageHandler.swift +++ b/Signal/Calls/WebRTCCallMessageHandler.swift @@ -120,11 +120,11 @@ class WebRTCCallMessageHandler: CallMessageHandler { func receivedGroupCallUpdateMessage( _ updateMessage: SSKProtoDataMessageGroupCallUpdate, - for groupThread: TSGroupThread, + forGroupId groupId: GroupIdentifier, serverReceivedTimestamp: UInt64 ) async { await groupCallManager.peekGroupCallAndUpdateThread( - groupThread, + forGroupId: groupId, peekTrigger: .receivedGroupUpdateMessage( eraId: updateMessage.eraID, messageTimestamp: serverReceivedTimestamp diff --git a/Signal/ConversationView/Components/CVComponentState.swift b/Signal/ConversationView/Components/CVComponentState.swift index c89ac35593..18aa7fe4f9 100644 --- a/Signal/ConversationView/Components/CVComponentState.swift +++ b/Signal/ConversationView/Components/CVComponentState.swift @@ -894,10 +894,10 @@ fileprivate extension CVComponentState.Builder { avatarDataSource: avatarDataSource) return build() case .info, .error, .call: - let currentGroupCallThreadUniqueId = viewStateSnapshot.currentGroupCallThreadUniqueId + let currentGroupThreadCallGroupId = viewStateSnapshot.currentGroupThreadCallGroupId self.systemMessage = CVComponentSystemMessage.buildComponentState(interaction: interaction, threadViewModel: threadViewModel, - currentGroupCallThreadUniqueId: currentGroupCallThreadUniqueId, + currentGroupThreadCallGroupId: currentGroupThreadCallGroupId, transaction: transaction) return build() case .unreadIndicator: diff --git a/Signal/ConversationView/Components/CVComponentSystemMessage.swift b/Signal/ConversationView/Components/CVComponentSystemMessage.swift index 256c68ceee..0841b26cbc 100644 --- a/Signal/ConversationView/Components/CVComponentSystemMessage.swift +++ b/Signal/ConversationView/Components/CVComponentSystemMessage.swift @@ -594,14 +594,14 @@ extension CVComponentSystemMessage { static func buildComponentState(interaction: TSInteraction, threadViewModel: ThreadViewModel, - currentGroupCallThreadUniqueId: String?, + currentGroupThreadCallGroupId: GroupIdentifier?, transaction: SDSAnyReadTransaction) -> CVComponentState.SystemMessage { let title = Self.title(forInteraction: interaction, transaction: transaction) let maybeOverrideTitleColor = Self.overrideTextColor(forInteraction: interaction) let action = Self.action(forInteraction: interaction, threadViewModel: threadViewModel, - currentGroupCallThreadUniqueId: currentGroupCallThreadUniqueId, + currentGroupThreadCallGroupId: currentGroupThreadCallGroupId, transaction: transaction) return buildComponentState(title: title, action: action, titleColor: maybeOverrideTitleColor) @@ -1037,7 +1037,7 @@ extension CVComponentSystemMessage { static func action( forInteraction interaction: TSInteraction, threadViewModel: ThreadViewModel, - currentGroupCallThreadUniqueId: String?, + currentGroupThreadCallGroupId: GroupIdentifier?, transaction: SDSAnyReadTransaction ) -> Action? { if let errorMessage = interaction as? TSErrorMessage { @@ -1050,7 +1050,7 @@ extension CVComponentSystemMessage { return action( forGroupCall: groupCall, threadViewModel: threadViewModel, - currentGroupCallThreadUniqueId: currentGroupCallThreadUniqueId + currentGroupThreadCallGroupId: currentGroupThreadCallGroupId ) } else { owsFailDebug("Invalid interaction.") @@ -1371,9 +1371,12 @@ extension CVComponentSystemMessage { private static func action( forGroupCall groupCallMessage: OWSGroupCallMessage, threadViewModel: ThreadViewModel, - currentGroupCallThreadUniqueId: String? + currentGroupThreadCallGroupId: GroupIdentifier? ) -> Action? { - let thread = threadViewModel.threadRecord + guard let groupThread = threadViewModel.threadRecord as? TSGroupThread else { + return nil + } + // Assume the current thread supports calling if we have no delegate. This ensures we always // overestimate cell measurement in cases where the current thread doesn't support calling. let isCallingSupported = ConversationViewController.canCall(threadViewModel: threadViewModel) @@ -1384,7 +1387,7 @@ extension CVComponentSystemMessage { } // TODO: We need to touch thread whenever current call changes. - let isCurrentCallForThread = currentGroupCallThreadUniqueId == thread.uniqueId + let isCurrentCallForThread = currentGroupThreadCallGroupId?.serialize().asData == groupThread.groupId let returnTitle = OWSLocalizedString("CALL_RETURN_BUTTON", comment: "Button to return to the current call") let title = isCurrentCallForThread ? returnTitle : CallStrings.joinGroupCall diff --git a/Signal/ConversationView/ConversationViewController+Calls.swift b/Signal/ConversationView/ConversationViewController+Calls.swift index 44fe18abba..1be0b6e52f 100644 --- a/Signal/ConversationView/ConversationViewController+Calls.swift +++ b/Signal/ConversationView/ConversationViewController+Calls.swift @@ -3,6 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // +import LibSignalClient import SignalServiceKit import SignalUI @@ -15,7 +16,7 @@ public extension ConversationViewController { case .individual(let call): return call.thread.uniqueId == thread.uniqueId case .groupThread(let call): - return call.groupThread.uniqueId == thread.uniqueId + return call.groupId.serialize().asData == (thread as? TSGroupThread)?.groupId case .callLink: return false } @@ -39,13 +40,13 @@ public extension ConversationViewController { @objc func showGroupLobbyOrActiveCall() { - guard let groupThread = thread as? TSGroupThread else { + guard let groupId = try? (thread as? TSGroupThread)?.groupIdentifier else { owsFailDebug("Tried to present group call for non-group thread.") return } let startCallResult = CallStarter( - groupThread: groupThread, + groupId: groupId, context: self.callStarterContext ).startCall(from: self) @@ -88,10 +89,10 @@ public extension ConversationViewController { } func refreshCallState() { - if let groupThread = thread as? TSGroupThread { + if let groupId = try? (thread as? TSGroupThread)?.groupIdentifier { Task { await SSKEnvironment.shared.groupCallManagerRef.peekGroupCallAndUpdateThread( - groupThread, + forGroupId: groupId, peekTrigger: .localEvent() ) } diff --git a/Signal/ConversationView/Loading/CVItemViewState.swift b/Signal/ConversationView/Loading/CVItemViewState.swift index 9c274f7350..2e07c7c673 100644 --- a/Signal/ConversationView/Loading/CVItemViewState.swift +++ b/Signal/ConversationView/Loading/CVItemViewState.swift @@ -622,10 +622,20 @@ struct CVItemModelBuilder: CVItemBuilding { } // Hide "call" buttons if there is an active call in another thread. + func isCurrentGroupCallForCurrentThread() -> Bool { + guard + let currentGroupThreadCallGroupId = viewStateSnapshot.currentGroupThreadCallGroupId, + let groupThread = thread as? TSGroupThread + else { + return false + } + return currentGroupThreadCallGroupId.serialize().asData == groupThread.groupId + } + if item.interactionType == .call, viewStateSnapshot.hasActiveCall, - viewStateSnapshot.currentGroupCallThreadUniqueId != thread.uniqueId + !isCurrentGroupCallForCurrentThread() { item.itemViewState.shouldCollapseSystemMessageAction = true } diff --git a/Signal/ConversationView/Loading/CVLoadCoordinator.swift b/Signal/ConversationView/Loading/CVLoadCoordinator.swift index 0b71f6c7f1..1561cf8b03 100644 --- a/Signal/ConversationView/Loading/CVLoadCoordinator.swift +++ b/Signal/ConversationView/Loading/CVLoadCoordinator.swift @@ -810,19 +810,13 @@ extension CVLoadCoordinator: UIScrollViewDelegate { // MARK: - extension CVLoadCoordinator: CallServiceStateObserver { - private func doesGroupThreadCall(_ signalCall: SignalCall?, matchThread thread: TSThread) -> Bool { - switch signalCall?.mode { - case .groupThread(let call): - return call.groupThread.uniqueId == thread.uniqueId - case nil, .individual, .callLink: - return false - } - } - func didUpdateCall(from oldValue: SignalCall?, to newValue: SignalCall?) { + guard let groupId = try? (thread as? TSGroupThread)?.groupIdentifier else { + return + } let matchesThread: Bool = ( - doesGroupThreadCall(oldValue, matchThread: thread) - || doesGroupThreadCall(newValue, matchThread: thread) + oldValue?.mode.matches(.groupThread(groupId)) == true + || newValue?.mode.matches(.groupThread(groupId)) == true ) guard matchesThread else { return diff --git a/Signal/ConversationView/Loading/CVViewStateSnapshot.swift b/Signal/ConversationView/Loading/CVViewStateSnapshot.swift index e0727b643f..cf23ca5ccf 100644 --- a/Signal/ConversationView/Loading/CVViewStateSnapshot.swift +++ b/Signal/ConversationView/Loading/CVViewStateSnapshot.swift @@ -3,6 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // +import LibSignalClient import SignalServiceKit import SignalUI @@ -39,7 +40,7 @@ struct CVViewStateSnapshot { let oldestUnreadMessageSortId: UInt64? let hasActiveCall: Bool - let currentGroupCallThreadUniqueId: String? + let currentGroupThreadCallGroupId: GroupIdentifier? private static var currentCallProvider: any CurrentCallProvider { DependenciesBridge.shared.currentCallProvider } @@ -60,7 +61,7 @@ struct CVViewStateSnapshot { searchText: viewState.lastSearchedText, oldestUnreadMessageSortId: oldestUnreadMessageSortId, hasActiveCall: currentCallProvider.hasCurrentCall, - currentGroupCallThreadUniqueId: currentCallProvider.currentGroupCallThread?.uniqueId + currentGroupThreadCallGroupId: currentCallProvider.currentGroupThreadCallGroupId ) } @@ -79,7 +80,7 @@ struct CVViewStateSnapshot { searchText: nil, oldestUnreadMessageSortId: nil, hasActiveCall: false, - currentGroupCallThreadUniqueId: nil + currentGroupThreadCallGroupId: nil ) } } diff --git a/Signal/Notifications/NotificationActionHandler.swift b/Signal/Notifications/NotificationActionHandler.swift index aa76e8d551..2e40dd1292 100644 --- a/Signal/Notifications/NotificationActionHandler.swift +++ b/Signal/Notifications/NotificationActionHandler.swift @@ -3,6 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // +import LibSignalClient import SignalServiceKit import SignalUI @@ -280,11 +281,25 @@ public class NotificationActionHandler { let threadUniqueId = userInfo[AppNotificationUserInfoKey.threadId] as? String let callLinkRoomId = userInfo[AppNotificationUserInfoKey.roomId] as? String - let callTarget = { () -> CallTarget? in + enum LobbyTarget { + case groupThread(groupId: GroupIdentifier, uniqueId: String) + case callLink(CallLink) + + var callTarget: CallTarget { + switch self { + case .groupThread(let groupId, uniqueId: _): + return .groupThread(groupId) + case .callLink(let callLink): + return .callLink(callLink) + } + } + } + + let lobbyTarget = { () -> LobbyTarget? in if let threadUniqueId { return SSKEnvironment.shared.databaseStorageRef.read { tx in - if let thread = TSThread.anyFetch(uniqueId: threadUniqueId, transaction: tx) as? TSGroupThread { - return .groupThread(thread) + if let groupId = try? (TSThread.anyFetch(uniqueId: threadUniqueId, transaction: tx) as? TSGroupThread)?.groupIdentifier { + return .groupThread(groupId: groupId, uniqueId: threadUniqueId) } return nil } @@ -303,28 +318,26 @@ public class NotificationActionHandler { } return nil }() - guard let callTarget else { + guard let lobbyTarget else { owsFailDebug("Couldn't resolve destination for call lobby.") return } let currentCall = Self.callService.callServiceState.currentCall - if currentCall?.mode.matches(callTarget) == true { + if currentCall?.mode.matches(lobbyTarget.callTarget) == true { AppEnvironment.shared.windowManagerRef.returnToCallView() return } if currentCall == nil { - callService.initiateCall(to: callTarget, isVideo: true) + callService.initiateCall(to: lobbyTarget.callTarget, isVideo: true) return } - switch callTarget { - case .individual: - owsFail("Not supported.") - case .groupThread(let thread as TSThread): + switch lobbyTarget { + case .groupThread(groupId: _, let uniqueId): // If currentCall is non-nil, we can't join a call anyway, so fall back to showing the thread. - self.showThread(uniqueId: thread.uniqueId) + self.showThread(uniqueId: uniqueId) case .callLink: // Nothing to show for a call link. break diff --git a/Signal/src/ViewControllers/ThreadSettings/ConversationHeaderBuilder.swift b/Signal/src/ViewControllers/ThreadSettings/ConversationHeaderBuilder.swift index 7141561f62..b90afe658e 100644 --- a/Signal/src/ViewControllers/ThreadSettings/ConversationHeaderBuilder.swift +++ b/Signal/src/ViewControllers/ThreadSettings/ConversationHeaderBuilder.swift @@ -233,7 +233,7 @@ struct ConversationHeaderBuilder { switch currentCall?.mode { case nil: return false case .individual(let call): return call.thread.uniqueId == delegate.thread.uniqueId - case .groupThread(let call): return call.groupThread.uniqueId == delegate.thread.uniqueId + case .groupThread(let call): return call.groupId.serialize().asData == (delegate.thread as? TSGroupThread)?.groupId case .callLink: return false } }() @@ -602,13 +602,22 @@ extension ConversationHeaderDelegate { return } let callTarget: CallTarget - switch thread { - case let contactThread as TSContactThread: + if let contactThread = thread as? TSContactThread { callTarget = .individual(contactThread) - case let groupThread as TSGroupThread where withVideo: - callTarget = .groupThread(groupThread) - default: - owsFailDebug("Tried to start an audio only group call") + } else if let groupThread = thread as? TSGroupThread { + if withVideo { + if let groupId = try? groupThread.groupIdentifier { + callTarget = .groupThread(groupId) + } else { + owsFailDebug("Tried to start a group call with an invalid groupId") + return + } + } else { + owsFailDebug("Tried to start an audio only group call") + return + } + } else { + owsFailDebug("Tried to start an invalid call") return } diff --git a/Signal/test/CallsTab/CallsListViewController+ViewModelLoaderTest.swift b/Signal/test/CallsTab/CallsListViewController+ViewModelLoaderTest.swift index 958a0f1ff9..3445286f50 100644 --- a/Signal/test/CallsTab/CallsListViewController+ViewModelLoaderTest.swift +++ b/Signal/test/CallsTab/CallsListViewController+ViewModelLoaderTest.swift @@ -32,7 +32,7 @@ final class CallsListViewControllerViewModelLoaderTest: XCTestCase { contactPhoneNumber: nil )) case .group: - return .group(groupThread: TSGroupThread.forUnitTest()) + return .groupThread(groupId: Data(count: 32)) case .callLink: fatalError() } diff --git a/SignalNSE/NSECallMessageHandler.swift b/SignalNSE/NSECallMessageHandler.swift index 2c033d6bb5..da2bac1521 100644 --- a/SignalNSE/NSECallMessageHandler.swift +++ b/SignalNSE/NSECallMessageHandler.swift @@ -193,11 +193,11 @@ class NSECallMessageHandler: CallMessageHandler { func receivedGroupCallUpdateMessage( _ updateMessage: SSKProtoDataMessageGroupCallUpdate, - for groupThread: TSGroupThread, + forGroupId groupId: GroupIdentifier, serverReceivedTimestamp: UInt64 ) async { await groupCallManager.peekGroupCallAndUpdateThread( - groupThread, + forGroupId: groupId, peekTrigger: .receivedGroupUpdateMessage( eraId: updateMessage.eraID, messageTimestamp: serverReceivedTimestamp diff --git a/SignalServiceKit/Calls/CallMessageHandler.swift b/SignalServiceKit/Calls/CallMessageHandler.swift index a7d566fc3a..55353b58c2 100644 --- a/SignalServiceKit/Calls/CallMessageHandler.swift +++ b/SignalServiceKit/Calls/CallMessageHandler.swift @@ -31,7 +31,7 @@ public protocol CallMessageHandler { func receivedGroupCallUpdateMessage( _ updateMessage: SSKProtoDataMessageGroupCallUpdate, - for thread: TSGroupThread, + forGroupId groupId: GroupIdentifier, serverReceivedTimestamp: UInt64 ) async } diff --git a/SignalServiceKit/Calls/GroupCallManager.swift b/SignalServiceKit/Calls/GroupCallManager.swift index 9fef48eb60..5a998ff748 100644 --- a/SignalServiceKit/Calls/GroupCallManager.swift +++ b/SignalServiceKit/Calls/GroupCallManager.swift @@ -3,18 +3,18 @@ // SPDX-License-Identifier: AGPL-3.0-only // -import LibSignalClient +public import LibSignalClient public import SignalRingRTC public protocol CurrentCallProvider { var hasCurrentCall: Bool { get } - var currentGroupCallThread: TSGroupThread? { get } + var currentGroupThreadCallGroupId: GroupIdentifier? { get } } public class CurrentCallNoOpProvider: CurrentCallProvider { public init() {} public var hasCurrentCall: Bool { false } - public var currentGroupCallThread: TSGroupThread? { nil } + public var currentGroupThreadCallGroupId: GroupIdentifier? { nil } } /// Fetches & updates group call state. @@ -69,20 +69,21 @@ public class GroupCallManager { } public func peekGroupCallAndUpdateThread( - _ thread: TSGroupThread, + forGroupId groupId: GroupIdentifier, peekTrigger: PeekTrigger ) async { - logger.info("Peek requested for thread \(thread.uniqueId) with trigger: \(peekTrigger)") + logger.info("Peek requested for group \(groupId) with trigger: \(peekTrigger)") // If the currentCall is for the provided thread, we don't need to // perform an explicit peek. Connected calls will receive automatic // updates from RingRTC. - guard currentCallProvider.currentGroupCallThread != thread else { + if currentCallProvider.currentGroupThreadCallGroupId?.serialize() == groupId.serialize() { logger.info("Ignoring peek request for the current call.") return } - guard thread.isLocalUserFullMember else { + let groupThread = databaseStorage.read { tx in TSGroupThread.fetch(forGroupId: groupId, tx: tx) } + guard let groupThread, groupThread.isLocalUserFullMember else { logger.warn("Ignoring peek request for non-member thread!") return } @@ -102,11 +103,11 @@ public class GroupCallManager { await self.upsertPlaceholderGroupCallModelsIfNecessary( eraId: eraId, triggerEventTimestamp: messageTimestamp, - groupThread: thread + groupId: groupId ) } - let info = try await self.groupCallPeekClient.fetchPeekInfo(groupThread: thread) + let info = try await self.groupCallPeekClient.fetchPeekInfo(groupId: groupId) let shouldUpdateCallModels: Bool = { guard let infoEraId = info.eraId else { @@ -130,28 +131,28 @@ public class GroupCallManager { }() if shouldUpdateCallModels { - self.logger.info("Applying group call PeekInfo for thread: \(thread.uniqueId), callId: \(info.callId?.description ?? "(null)")") + self.logger.info("Applying group call PeekInfo for groupId: \(groupId), callId: \(info.callId?.description ?? "(null)")") await self.databaseStorage.awaitableWrite { tx in self.updateGroupCallModelsForPeek( peekInfo: info, - groupThread: thread, + groupId: groupId, triggerEventTimestamp: peekTrigger.timestamp, tx: tx ) } } else { - self.logger.info("Ignoring group call PeekInfo for thread: \(thread.uniqueId), stale callId: \(info.callId?.description ?? "(null)")") + self.logger.info("Ignoring group call PeekInfo for groupId: \(groupId), stale callId: \(info.callId?.description ?? "(null)")") } } catch { if error.isNetworkFailureOrTimeout { - self.logger.warn("Failed to fetch PeekInfo for \(thread.uniqueId): \(error)") + self.logger.warn("Failed to fetch PeekInfo for \(groupId): \(error)") } else if !TSConstants.isUsingProductionService { // Staging uses the production credentials, so trying to send a request // with the staging credentials is expected to fail. - self.logger.warn("Expected failure to fetch PeekInfo for \(thread.uniqueId): \(error)") + self.logger.warn("Expected failure to fetch PeekInfo for \(groupId): \(error)") } else { - owsFailDebug("Failed to fetch PeekInfo for \(thread.uniqueId): \(error)") + owsFailDebug("Failed to fetch PeekInfo for \(groupId): \(error)") } } } @@ -160,12 +161,17 @@ public class GroupCallManager { /// peek info. public func updateGroupCallModelsForPeek( peekInfo: PeekInfo, - groupThread: TSGroupThread, + groupId: GroupIdentifier, triggerEventTimestamp: UInt64, tx: SDSAnyWriteTransaction ) { let currentCallId: CallId? = peekInfo.callId + guard let groupThread = TSGroupThread.fetch(forGroupId: groupId, tx: tx) else { + owsFailDebug("Can't update call with missing thread.") + return + } + // Clean up any unended group calls that don't match the currently // in-progress call. let interactionForCurrentCall = self.cleanUpUnendedCallMessagesAsNecessary( @@ -226,7 +232,7 @@ public class GroupCallManager { case .found(let interactionToUpdate): let wasOldMessageEmpty = interactionToUpdate.joinedMemberAcis.isEmpty && !interactionToUpdate.hasEnded - logger.info("Updating group call interaction for thread \(groupThread.uniqueId), callId \(currentCallId). Joined member count: \(joinedMemberAcis.count)") + logger.info("Updating group call interaction for thread \(groupId), callId \(currentCallId). Joined member count: \(joinedMemberAcis.count)") self.interactionStore.updateGroupCallInteractionAcis( groupCallInteraction: interactionToUpdate, @@ -401,9 +407,13 @@ public class GroupCallManager { private func upsertPlaceholderGroupCallModelsIfNecessary( eraId: String, triggerEventTimestamp: UInt64, - groupThread: TSGroupThread + groupId: GroupIdentifier ) async { await databaseStorage.awaitableWrite { tx in + guard let groupThread = TSGroupThread.fetch(forGroupId: groupId, tx: tx) else { + owsFailDebug("Can't find TSGroupThread that must exist.") + return + } guard !GroupCallInteractionFinder().existsGroupCallMessageForEraId( eraId, thread: groupThread, transaction: tx ) else { @@ -463,7 +473,7 @@ public class GroupCallManager { AssertNotOnMainThread() // The message can't be for the current call - guard currentCallProvider.currentGroupCallThread != groupThread else { + if currentCallProvider.currentGroupThreadCallGroupId?.serialize().asData == groupThread.groupId { return } diff --git a/SignalServiceKit/Calls/GroupCallPeekClient.swift b/SignalServiceKit/Calls/GroupCallPeekClient.swift index 24c619e3e7..b92e6f2b70 100644 --- a/SignalServiceKit/Calls/GroupCallPeekClient.swift +++ b/SignalServiceKit/Calls/GroupCallPeekClient.swift @@ -3,7 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // -import LibSignalClient +public import LibSignalClient public import SignalRingRTC public class GroupCallPeekLogger: PrefixedLogger { @@ -41,11 +41,19 @@ public class GroupCallPeekClient { /// Fetch the current group call peek info for the given thread. @MainActor - public func fetchPeekInfo(groupThread: TSGroupThread) async throws -> PeekInfo { - let membershipProof = try await self.fetchGroupMembershipProof(groupThread: groupThread) + public func fetchPeekInfo(groupId: GroupIdentifier) async throws -> PeekInfo { + let secretParams = try self.db.read { tx in + let groupThread = TSGroupThread.fetch(forGroupId: groupId, tx: SDSDB.shimOnlyBridge(tx)) + return try (groupThread?.groupModel as? TSGroupModelV2)?.secretParams() + } + guard let secretParams else { + throw OWSGenericError("Can't peek without secret params.") + } + + let membershipProof = try await self.fetchGroupMembershipProof(secretParams: secretParams) let membership = try self.db.read { tx in - try self.groupMemberInfo(groupThread: groupThread, tx: tx) + try self.groupMemberInfo(forGroupId: groupId, tx: tx) } let peekRequest = PeekRequest( @@ -64,12 +72,8 @@ public class GroupCallPeekClient { /// Fetches a data blob that serves as proof of membership in the group. /// Used by RingRTC to verify access to group call information. - public func fetchGroupMembershipProof(groupThread: TSGroupThread) async throws -> Data { - guard let groupModel = groupThread.groupModel as? TSGroupModelV2 else { - throw OWSAssertionError("Expected V2 group model!") - } - - let credential = try await self.groupsV2.fetchGroupExternalCredentials(groupModel: groupModel) + public func fetchGroupMembershipProof(secretParams: GroupSecretParams) async throws -> Data { + let credential = try await self.groupsV2.fetchGroupExternalCredentials(secretParams: secretParams) guard let tokenData = credential.token?.data(using: .utf8) else { throw OWSAssertionError("Invalid credential") @@ -78,20 +82,15 @@ public class GroupCallPeekClient { return tokenData } - public func groupMemberInfo( - groupThread: TSGroupThread, - tx: DBReadTransaction - ) throws -> [GroupMemberInfo] { - // Make sure we're working with the latest group state. - groupThread.anyReload(transaction: SDSDB.shimOnlyBridge(tx)) - - guard let groupModel = groupThread.groupModel as? TSGroupModelV2 else { + public func groupMemberInfo(forGroupId groupId: GroupIdentifier, tx: DBReadTransaction) throws -> [GroupMemberInfo] { + let groupThread = TSGroupThread.fetch(forGroupId: groupId, tx: SDSDB.shimOnlyBridge(tx)) + guard let groupModel = groupThread?.groupModel as? TSGroupModelV2 else { throw OWSAssertionError("Expected V2 group model!") } let groupV2Params = try groupModel.groupV2Params() - return groupThread.groupMembership.fullMembers.compactMap { + return groupModel.groupMembership.fullMembers.compactMap { guard let aci = $0.serviceId as? Aci else { owsFailDebug("Skipping group member, missing uuid") return nil diff --git a/SignalServiceKit/Contacts/Threads/TSGroupThread.swift b/SignalServiceKit/Contacts/Threads/TSGroupThread.swift index 95e3155601..3dcbe45bf4 100644 --- a/SignalServiceKit/Contacts/Threads/TSGroupThread.swift +++ b/SignalServiceKit/Contacts/Threads/TSGroupThread.swift @@ -3,6 +3,22 @@ // SPDX-License-Identifier: AGPL-3.0-only // +public import LibSignalClient + +// MARK: - + +extension TSGroupThread { + public var groupIdentifier: GroupIdentifier { + get throws { + return try GroupIdentifier(contents: [UInt8](self.groupId)) + } + } + + public static func fetch(forGroupId groupId: GroupIdentifier, tx: SDSAnyReadTransaction) -> TSGroupThread? { + return fetch(groupId: groupId.serialize().asData, transaction: tx) + } +} + // MARK: - public extension TSGroupThread { diff --git a/SignalServiceKit/Contacts/Threads/ThreadStore.swift b/SignalServiceKit/Contacts/Threads/ThreadStore.swift index cb7a4b59aa..c37b7222b3 100644 --- a/SignalServiceKit/Contacts/Threads/ThreadStore.swift +++ b/SignalServiceKit/Contacts/Threads/ThreadStore.swift @@ -87,6 +87,10 @@ public protocol ThreadStore { } extension ThreadStore { + public func fetchGroupThread(groupId: GroupIdentifier, tx: any DBReadTransaction) -> TSGroupThread? { + return fetchGroupThread(groupId: groupId.serialize().asData, tx: tx) + } + public func fetchGroupThread(uniqueId: String, tx: DBReadTransaction) -> TSGroupThread? { guard let thread = fetchThread(uniqueId: uniqueId, tx: tx) else { return nil diff --git a/SignalServiceKit/Environment/NoopCallMessageHandler.swift b/SignalServiceKit/Environment/NoopCallMessageHandler.swift index 4919e322c4..04fc656cd4 100644 --- a/SignalServiceKit/Environment/NoopCallMessageHandler.swift +++ b/SignalServiceKit/Environment/NoopCallMessageHandler.swift @@ -25,7 +25,7 @@ public class NoopCallMessageHandler: CallMessageHandler { public func receivedGroupCallUpdateMessage( _ updateMessage: SSKProtoDataMessageGroupCallUpdate, - for groupThread: TSGroupThread, + forGroupId groupId: GroupIdentifier, serverReceivedTimestamp: UInt64 ) async { owsFailDebug("") diff --git a/SignalServiceKit/Groups/GroupsV2.swift b/SignalServiceKit/Groups/GroupsV2.swift index 6b943a3861..f9861dc572 100644 --- a/SignalServiceKit/Groups/GroupsV2.swift +++ b/SignalServiceKit/Groups/GroupsV2.swift @@ -157,7 +157,7 @@ public protocol GroupsV2 { removeLocalUserBlock: @escaping (SDSAnyWriteTransaction) -> Void ) async throws - func fetchGroupExternalCredentials(groupModel: TSGroupModelV2) async throws -> GroupsProtoGroupExternalCredential + func fetchGroupExternalCredentials(secretParams: GroupSecretParams) async throws -> GroupsProtoGroupExternalCredential func groupRecordPendingStorageServiceRestore( masterKeyData: Data, @@ -716,7 +716,7 @@ public class MockGroupsV2: GroupsV2 { owsFail("Not implemented.") } - public func fetchGroupExternalCredentials(groupModel: TSGroupModelV2) async throws -> GroupsProtoGroupExternalCredential { + public func fetchGroupExternalCredentials(secretParams: GroupSecretParams) async throws -> GroupsProtoGroupExternalCredential { owsFail("Not implemented") } diff --git a/SignalServiceKit/Groups/GroupsV2Impl.swift b/SignalServiceKit/Groups/GroupsV2Impl.swift index 46c1575cfb..a2073c75cd 100644 --- a/SignalServiceKit/Groups/GroupsV2Impl.swift +++ b/SignalServiceKit/Groups/GroupsV2Impl.swift @@ -2037,17 +2037,19 @@ public class GroupsV2Impl: GroupsV2 { } } - public func fetchGroupExternalCredentials(groupModel: TSGroupModelV2) async throws -> GroupsProtoGroupExternalCredential { + public func fetchGroupExternalCredentials(secretParams: GroupSecretParams) async throws -> GroupsProtoGroupExternalCredential { + let groupParams = try GroupV2Params(groupSecretParams: secretParams) + let requestBuilder: RequestBuilder = { authCredential in try StorageService.buildFetchGroupExternalCredentials( - groupV2Params: try groupModel.groupV2Params(), + groupV2Params: groupParams, authCredential: authCredential ) } let response = try await performServiceRequest( requestBuilder: requestBuilder, - groupId: groupModel.groupId, + groupId: try secretParams.getPublicParams().getGroupIdentifier().serialize().asData, behavior400: .fail, behavior403: .fetchGroupUpdates, behavior404: .fail diff --git a/SignalServiceKit/Messages/MessageReceiver.swift b/SignalServiceKit/Messages/MessageReceiver.swift index 3ac3d292bb..a524373987 100644 --- a/SignalServiceKit/Messages/MessageReceiver.swift +++ b/SignalServiceKit/Messages/MessageReceiver.swift @@ -469,18 +469,18 @@ public final class MessageReceiver { ) } } else if let groupCallUpdate = dataMessage.groupCallUpdate { - if let groupId, let groupThread = TSGroupThread.fetch(groupId: groupId, transaction: tx) { + if let groupId = try? GroupIdentifier(contents: [UInt8](groupId ?? Data())) { let pendingTask = MessageReceiver.buildPendingTask(label: "GroupCallUpdate") Task { [callMessageHandler] in defer { pendingTask.complete() } await callMessageHandler.receivedGroupCallUpdateMessage( groupCallUpdate, - for: groupThread, + forGroupId: groupId, serverReceivedTimestamp: decryptedEnvelope.timestamp ) } } else { - Logger.warn("Received GroupCallUpdate for unknown groupId") + Logger.warn("Received GroupCallUpdate for invalid groupId") } } else { guard let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiers(tx: tx.asV2Read) else { @@ -950,8 +950,8 @@ public final class MessageReceiver { } if let groupCallUpdate = dataMessage.groupCallUpdate { - guard let groupThread = thread as? TSGroupThread else { - Logger.warn("Ignoring group call update for non-group thread") + guard let groupId = try? (thread as? TSGroupThread)?.groupIdentifier else { + Logger.warn("Ignoring group call update invalid group thread.") return nil } let pendingTask = Self.buildPendingTask(label: "GroupCallUpdate") @@ -959,7 +959,7 @@ public final class MessageReceiver { defer { pendingTask.complete() } await callMessageHandler.receivedGroupCallUpdateMessage( groupCallUpdate, - for: groupThread, + forGroupId: groupId, serverReceivedTimestamp: envelope.timestamp ) } diff --git a/SignalServiceKit/tests/MessageBackup/MessageBackupIntegrationTests.swift b/SignalServiceKit/tests/MessageBackup/MessageBackupIntegrationTests.swift index 84bf24dc47..99d80d6b00 100644 --- a/SignalServiceKit/tests/MessageBackup/MessageBackupIntegrationTests.swift +++ b/SignalServiceKit/tests/MessageBackup/MessageBackupIntegrationTests.swift @@ -535,12 +535,12 @@ private enum CrashyMocks { final class MockCallMessageHandler: CallMessageHandler { func receivedEnvelope(_ envelope: SSKProtoEnvelope, callEnvelope: CallEnvelopeType, from caller: (aci: Aci, deviceId: UInt32), toLocalIdentity localIdentity: OWSIdentity, plaintextData: Data, wasReceivedByUD: Bool, sentAtTimestamp: UInt64, serverReceivedTimestamp: UInt64, serverDeliveryTimestamp: UInt64, tx: SDSAnyWriteTransaction) { failTest(Self.self) } - func receivedGroupCallUpdateMessage(_ updateMessage: SSKProtoDataMessageGroupCallUpdate, for thread: TSGroupThread, serverReceivedTimestamp: UInt64) async { failTest(Self.self) } + func receivedGroupCallUpdateMessage(_ updateMessage: SSKProtoDataMessageGroupCallUpdate, forGroupId groupId: GroupIdentifier, serverReceivedTimestamp: UInt64) async { failTest(Self.self) } } final class MockCurrentCallThreadProvider: CurrentCallProvider { var hasCurrentCall: Bool { failTest(Self.self) } - var currentGroupCallThread: TSGroupThread? { failTest(Self.self) } + var currentGroupThreadCallGroupId: GroupIdentifier? { failTest(Self.self) } } final class MockNotificationPresenter: NotificationPresenter { diff --git a/SignalUI/Views/ConversationAvatarView.swift b/SignalUI/Views/ConversationAvatarView.swift index a24dfc0d1b..45553c9ddd 100644 --- a/SignalUI/Views/ConversationAvatarView.swift +++ b/SignalUI/Views/ConversationAvatarView.swift @@ -140,6 +140,19 @@ public class ConversationAvatarView: UIView, CVView, PrimaryImageView { /// The data provider used to fetch an avatar and badge public var dataSource: ConversationAvatarDataSource? + + public mutating func setGroupIdWithSneakyTransaction(groupId: Data) { + if dataSource?.groupId == groupId { + return + } + let databaseStorage = SSKEnvironment.shared.databaseStorageRef + self.dataSource = databaseStorage.read { tx in + return TSGroupThread.fetch(groupId: groupId, transaction: tx) + }.map { + return .thread($0) + } + } + /// Adjusts how the local user profile avatar is generated (Note to Self or Avatar?) public var localUserDisplayMode: LocalUserDisplayMode /// Places the user's badge (if they have one) over the avatar. Only supported for predefined size classes