Don’t pass around TSGroupThread in group calls
This commit is contained in:
parent
19064ca096
commit
60dc4a8dc0
@ -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) {
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -31,7 +31,7 @@ public protocol CallMessageHandler {
|
||||
|
||||
func receivedGroupCallUpdateMessage(
|
||||
_ updateMessage: SSKProtoDataMessageGroupCallUpdate,
|
||||
for thread: TSGroupThread,
|
||||
forGroupId groupId: GroupIdentifier,
|
||||
serverReceivedTimestamp: UInt64
|
||||
) async
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -25,7 +25,7 @@ public class NoopCallMessageHandler: CallMessageHandler {
|
||||
|
||||
public func receivedGroupCallUpdateMessage(
|
||||
_ updateMessage: SSKProtoDataMessageGroupCallUpdate,
|
||||
for groupThread: TSGroupThread,
|
||||
forGroupId groupId: GroupIdentifier,
|
||||
serverReceivedTimestamp: UInt64
|
||||
) async {
|
||||
owsFailDebug("")
|
||||
|
||||
@ -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")
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user