Don’t pass around TSGroupThread in group calls

This commit is contained in:
Max Radermacher 2025-01-07 16:40:13 -06:00 committed by GitHub
parent 19064ca096
commit 60dc4a8dc0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 429 additions and 252 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -31,7 +31,7 @@ public protocol CallMessageHandler {
func receivedGroupCallUpdateMessage(
_ updateMessage: SSKProtoDataMessageGroupCallUpdate,
for thread: TSGroupThread,
forGroupId groupId: GroupIdentifier,
serverReceivedTimestamp: UInt64
) async
}

View File

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

View File

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

View File

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

View File

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

View File

@ -25,7 +25,7 @@ public class NoopCallMessageHandler: CallMessageHandler {
public func receivedGroupCallUpdateMessage(
_ updateMessage: SSKProtoDataMessageGroupCallUpdate,
for groupThread: TSGroupThread,
forGroupId groupId: GroupIdentifier,
serverReceivedTimestamp: UInt64
) async {
owsFailDebug("")

View File

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

View File

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

View File

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

View File

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

View File

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