Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/Calls/UserInterface/CallDrawerSheetDataSource.swift
1 views
//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation
import LibSignalClient
import SignalRingRTC
import SignalServiceKit
import SignalUI
import UIKit

// MARK: - Call Drawer Protocols

protocol CallDrawerSheetDataSourceObserver: AnyObject {
    func callSheetMembershipDidChange(_ dataSource: CallDrawerSheetDataSource)
    func callSheetRaisedHandsDidChange(_ dataSource: CallDrawerSheetDataSource)
}

@MainActor
protocol CallDrawerSheetDataSource {
    typealias JoinedMember = CallDrawerSheet.JoinedMember

    func unsortedMembers(tx: DBReadTransaction) -> [JoinedMember]
    func raisedHandMemberIds() -> [JoinedMember.ID]

    func addObserver(_ observer: any CallDrawerSheetDataSourceObserver, syncStateImmediately: Bool)
    func removeObserver(_ observer: any CallDrawerSheetDataSourceObserver)
}

// MARK: - Group Call data source

final class GroupCallSheetDataSource<Call: GroupCall>: CallDrawerSheetDataSource, GroupCallObserver {
    private let ringRtcCall: SignalRingRTC.GroupCall
    private let groupCall: Call

    @MainActor
    init(groupCall: Call) {
        self.ringRtcCall = groupCall.ringRtcCall
        self.groupCall = groupCall

        groupCall.addObserver(self)
    }

    private var observers: WeakArray<any CallDrawerSheetDataSourceObserver> = []
    func addObserver(_ observer: any CallDrawerSheetDataSourceObserver, syncStateImmediately: Bool = false) {
        AssertIsOnMainThread()
        observers.append(observer)
        if syncStateImmediately {
            // Synchronize observer with current call state
            observer.callSheetMembershipDidChange(self)
            observer.callSheetRaisedHandsDidChange(self)
        }
    }

    func removeObserver(_ observer: any CallDrawerSheetDataSourceObserver) {
        observers.removeAll(where: { $0 === observer })
    }

    func raisedHandMemberIds() -> [JoinedMember.ID] {
        return groupCall.raisedHands.map { .demuxID($0) }
    }

    func unsortedMembers(tx: DBReadTransaction) -> [JoinedMember] {
        let avatarBuilder = SSKEnvironment.shared.avatarBuilderRef
        let contactManager = SSKEnvironment.shared.contactManagerRef
        let tsAccountManager = DependenciesBridge.shared.tsAccountManager

        guard let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx) else {
            return []
        }

        func avatarImage(aci: Aci) -> UIImage? {
            return avatarBuilder.avatarImage(
                forAddress: SignalServiceAddress(aci),
                diameterPoints: 36,
                localUserDisplayMode: .asLocalUser,
                transaction: tx,
            )
        }

        func names(aci: Aci) -> (
            resolved: String,
            comparable: DisplayName.ComparableValue,
            isUnknown: Bool,
        ) {
            let resolvedName: String
            let comparableName: DisplayName.ComparableValue
            let isUnknown: Bool
            if aci == localIdentifiers.aci {
                resolvedName = OWSLocalizedString(
                    "GROUP_CALL_YOU_ON_ANOTHER_DEVICE",
                    comment: "Text describing the local user in the group call members sheet when connected from another device.",
                )
                comparableName = .nameValue(resolvedName)
                isUnknown = false
            } else {
                let displayName = contactManager.displayName(for: SignalServiceAddress(aci), tx: tx)
                resolvedName = displayName.resolvedValue(config: config.displayNameConfig)
                comparableName = displayName.comparableValue(config: config)
                isUnknown = switch displayName {
                case .nickname, .systemContactName, .profileName, .phoneNumber, .username:
                    false
                case .unknown, .deletedAccount:
                    true
                }
            }

            return (resolvedName, comparableName, isUnknown)
        }

        var members = [JoinedMember]()
        let config: DisplayName.ComparableValue.Config = .current()
        if self.ringRtcCall.localDeviceState.joinState == .joined {
            members += self.ringRtcCall.remoteDeviceStates.values.map { member in
                let (resolvedName, comparableName, isUnknown) = names(aci: member.aci)
                return JoinedMember(
                    id: .demuxID(member.demuxId),
                    aci: member.aci,
                    displayName: resolvedName,
                    comparableName: comparableName,
                    avatarImage: avatarImage(aci: member.aci),
                    demuxID: member.demuxId,
                    isLocalUser: false,
                    isUnknown: isUnknown,
                    isAudioMuted: member.audioMuted,
                    isVideoMuted: member.videoMuted,
                    isPresenting: member.presenting,
                )
            }

            let displayName = CommonStrings.you
            let comparableName: DisplayName.ComparableValue = .nameValue(displayName)
            let id: JoinedMember.ID
            let demuxId: UInt32?
            if let localDemuxId = ringRtcCall.localDeviceState.demuxId {
                id = .demuxID(localDemuxId)
                demuxId = localDemuxId
            } else {
                id = .aci(localIdentifiers.aci)
                demuxId = nil
            }
            members.append(JoinedMember(
                id: id,
                aci: localIdentifiers.aci,
                displayName: displayName,
                comparableName: comparableName,
                avatarImage: avatarImage(aci: localIdentifiers.aci),
                demuxID: demuxId,
                isLocalUser: true,
                isUnknown: false,
                isAudioMuted: self.ringRtcCall.isOutgoingAudioMuted,
                isVideoMuted: self.ringRtcCall.isOutgoingVideoMuted,
                isPresenting: false,
            ))
        } else {
            // If we're not yet in the call, `remoteDeviceStates` will not exist.
            // We can get the list of joined members still, provided we are connected.
            members += self.ringRtcCall.peekInfo?.joinedMembers.map { aciUuid in
                let aci = Aci(fromUUID: aciUuid)
                let (resolvedName, comparableName, isUnknown) = names(aci: aci)
                return JoinedMember(
                    id: .aci(aci),
                    aci: aci,
                    displayName: resolvedName,
                    comparableName: comparableName,
                    avatarImage: avatarImage(aci: aci),
                    demuxID: nil,
                    isLocalUser: false,
                    isUnknown: isUnknown,
                    isAudioMuted: nil,
                    isVideoMuted: nil,
                    isPresenting: nil,
                )
            } ?? []
        }
        return members
    }

    // MARK: GroupCallObserver

    func groupCallLocalDeviceStateChanged(_ call: GroupCall) {
        AssertIsOnMainThread()
        observers.elements.forEach { $0.callSheetMembershipDidChange(self) }
    }

    func groupCallRemoteDeviceStatesChanged(_ call: GroupCall) {
        AssertIsOnMainThread()
        observers.elements.forEach { $0.callSheetMembershipDidChange(self) }
    }

    func groupCallPeekChanged(_ call: GroupCall) {
        AssertIsOnMainThread()
        observers.elements.forEach { $0.callSheetMembershipDidChange(self) }
    }

    func groupCallEnded(_ call: GroupCall, reason: CallEndReason) {
        AssertIsOnMainThread()
        observers.elements.forEach { $0.callSheetMembershipDidChange(self) }
    }

    func groupCallReceivedRaisedHands(_ call: GroupCall, raisedHands: [DemuxId]) {
        AssertIsOnMainThread()
        observers.elements.forEach { $0.callSheetRaisedHandsDidChange(self) }
    }
}

// MARK: Call Links

typealias CallLinkSheetDataSource = GroupCallSheetDataSource<CallLinkCall>

extension CallLinkSheetDataSource {
    func url() -> URL {
        return self.groupCall.callLink.url()
    }

    var callLink: CallLink {
        self.groupCall.callLink
    }

    var isAdmin: Bool {
        self.groupCall.isAdmin
    }

    var adminPasskey: Data? {
        self.groupCall.adminPasskey
    }

    var callLinkState: SignalServiceKit.CallLinkState {
        self.groupCall.callLinkState
    }

    func removeMember(demuxId: DemuxId) {
        ringRtcCall.removeClient(demuxId: demuxId)
    }

    func blockMember(demuxId: DemuxId) {
        ringRtcCall.blockClient(demuxId: demuxId)
    }
}

// MARK: - Individual call data source

class IndividualCallSheetDataSource: CallDrawerSheetDataSource {

    private var call: SignalCall
    private var individualCall: IndividualCall
    private var thread: TSContactThread

    init(
        thread: TSContactThread,
        call: SignalCall,
        individualCall: IndividualCall,
    ) {
        self.call = call
        self.thread = thread
        self.individualCall = individualCall
        individualCall.addObserverAndSyncState(self)
    }

    func unsortedMembers(tx: DBReadTransaction) -> [JoinedMember] {
        let avatarBuilder = SSKEnvironment.shared.avatarBuilderRef
        let contactManager = SSKEnvironment.shared.contactManagerRef
        let tsAccountManager = DependenciesBridge.shared.tsAccountManager

        func avatarImage(aci: Aci) -> UIImage? {
            return avatarBuilder.avatarImage(
                forAddress: SignalServiceAddress(aci),
                diameterPoints: 36,
                localUserDisplayMode: .asLocalUser,
                transaction: tx,
            )
        }

        var members = [JoinedMember]()

        if let remoteAci = thread.contactAddress.serviceId as? Aci {
            let remoteDisplayName = contactManager.displayName(
                for: thread.contactAddress,
                tx: tx,
            ).resolvedValue()
            let remoteComparableName: DisplayName.ComparableValue = .nameValue(remoteDisplayName)
            members.append(JoinedMember(
                id: .aci(remoteAci),
                aci: remoteAci,
                displayName: remoteDisplayName,
                comparableName: remoteComparableName,
                avatarImage: avatarImage(aci: remoteAci),
                demuxID: nil,
                isLocalUser: false,
                isUnknown: false,
                isAudioMuted: self.individualCall.isRemoteAudioMuted,
                isVideoMuted: self.individualCall.isRemoteVideoEnabled.negated,
                isPresenting: self.individualCall.isRemoteSharingScreen,
            ))
        }

        // Add yourself
        let displayName = CommonStrings.you
        let comparableName: DisplayName.ComparableValue = .nameValue(displayName)
        if let localAci = tsAccountManager.localIdentifiers(tx: tx)?.aci {
            members.append(JoinedMember(
                id: .aci(localAci),
                aci: localAci,
                displayName: displayName,
                comparableName: comparableName,
                avatarImage: avatarImage(aci: localAci),
                demuxID: nil,
                isLocalUser: true,
                isUnknown: false,
                isAudioMuted: self.call.isOutgoingAudioMuted,
                isVideoMuted: self.call.isOutgoingVideoMuted,
                isPresenting: false,
            ))
        }
        return members
    }

    func raisedHandMemberIds() -> [JoinedMember.ID] { [] }

    private var observers: WeakArray<any CallDrawerSheetDataSourceObserver> = []
    func addObserver(_ observer: any CallDrawerSheetDataSourceObserver, syncStateImmediately: Bool = false) {
        AssertIsOnMainThread()
        observers.append(observer)
        if syncStateImmediately {
            // Synchronize observer with current call state
            observer.callSheetMembershipDidChange(self)
            observer.callSheetRaisedHandsDidChange(self)
        }
    }

    func removeObserver(_ observer: any CallDrawerSheetDataSourceObserver) {
        observers.removeAll(where: { $0 === observer })
    }
}

// MARK: IndividualCallObserver

extension IndividualCallSheetDataSource: IndividualCallObserver {
    func individualCallStateDidChange(_ call: IndividualCall, state: CallState) {
        AssertIsOnMainThread()
        observers.elements.forEach { $0.callSheetMembershipDidChange(self) }
    }

    func individualCallLocalVideoMuteDidChange(_ call: IndividualCall, isVideoMuted: Bool) {
        AssertIsOnMainThread()
        observers.elements.forEach { $0.callSheetMembershipDidChange(self) }
    }

    func individualCallLocalAudioMuteDidChange(_ call: IndividualCall, isAudioMuted: Bool) {
        AssertIsOnMainThread()
        observers.elements.forEach { $0.callSheetMembershipDidChange(self) }
    }

    func individualCallHoldDidChange(_ call: IndividualCall, isOnHold: Bool) {
        AssertIsOnMainThread()
        observers.elements.forEach { $0.callSheetMembershipDidChange(self) }
    }

    func individualCallRemoteAudioMuteDidChange(_ call: IndividualCall, isAudioMuted: Bool) {
        AssertIsOnMainThread()
        observers.elements.forEach { $0.callSheetMembershipDidChange(self) }
    }

    func individualCallRemoteVideoMuteDidChange(_ call: IndividualCall, isVideoMuted: Bool) {
        AssertIsOnMainThread()
        observers.elements.forEach { $0.callSheetMembershipDidChange(self) }
    }

    func individualCallRemoteSharingScreenDidChange(_ call: IndividualCall, isRemoteSharingScreen: Bool) {
        AssertIsOnMainThread()
        observers.elements.forEach { $0.callSheetMembershipDidChange(self) }
    }
}