ios: refresh call invitations and report call on start and activation; core: restore calls on activation (#776)
This commit is contained in:
@@ -35,7 +35,7 @@ final class ChatModel: ObservableObject {
|
||||
@Published var notificationMode = NotificationsMode.off
|
||||
@Published var notificationPreview: NotificationPreviewMode? = .message
|
||||
// current WebRTC call
|
||||
@Published var callInvitations: Dictionary<ChatId, CallInvitation> = [:]
|
||||
@Published var callInvitations: Dictionary<ChatId, RcvCallInvitation> = [:]
|
||||
@Published var activeCall: Call?
|
||||
@Published var callCommand: WCallCommand?
|
||||
@Published var showCallView = false
|
||||
|
||||
@@ -184,7 +184,7 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
|
||||
addNotification(createMessageReceivedNtf(cInfo, cItem))
|
||||
}
|
||||
|
||||
func notifyCallInvitation(_ invitation: CallInvitation) {
|
||||
func notifyCallInvitation(_ invitation: RcvCallInvitation) {
|
||||
logger.debug("NtfManager.notifyCallInvitation")
|
||||
addNotification(createCallInvitationNtf(invitation))
|
||||
}
|
||||
|
||||
@@ -476,6 +476,12 @@ func apiEndCall(_ contact: Contact) async throws {
|
||||
try await sendCommandOkResp(.apiEndCall(contact: contact))
|
||||
}
|
||||
|
||||
func apiGetCallInvitations() throws -> [RcvCallInvitation] {
|
||||
let r = chatSendCmdSync(.apiGetCallInvitations)
|
||||
if case let .callInvitations(invs) = r { return invs }
|
||||
throw r
|
||||
}
|
||||
|
||||
func apiCallStatus(_ contact: Contact, _ status: String) async throws {
|
||||
if let callStatus = WebRTCCallStatus.init(rawValue: status) {
|
||||
try await sendCommandOkResp(.apiCallStatus(contact: contact, callStatus: callStatus))
|
||||
@@ -538,6 +544,7 @@ func startChat() throws {
|
||||
m.userSMPServers = try getUserSMPServers()
|
||||
let chats = try apiGetChats()
|
||||
m.chats = chats.map { Chat.init($0) }
|
||||
try refreshCallInvitations()
|
||||
(m.savedToken, m.tokenStatus, m.notificationMode) = try apiGetNtfToken()
|
||||
if let token = m.deviceToken {
|
||||
registerToken(token: token)
|
||||
@@ -694,19 +701,9 @@ func processReceivedMsg(_ res: ChatResponse) async {
|
||||
let fileName = cItem.file?.filePath {
|
||||
removeFile(fileName)
|
||||
}
|
||||
case let .callInvitation(contact, callType, sharedKey, callTs):
|
||||
let uuid = UUID()
|
||||
var invitation = CallInvitation(contact: contact, callkitUUID: uuid, peerMedia: callType.media, sharedKey: sharedKey, callTs: callTs)
|
||||
m.callInvitations[contact.id] = invitation
|
||||
CallController.shared.reportNewIncomingCall(invitation: invitation) { error in
|
||||
if let error = error {
|
||||
invitation.callkitUUID = nil
|
||||
m.callInvitations[contact.id] = invitation
|
||||
logger.error("reportNewIncomingCall error: \(error.localizedDescription)")
|
||||
} else {
|
||||
logger.debug("reportNewIncomingCall success")
|
||||
}
|
||||
}
|
||||
case let .callInvitation(invitation):
|
||||
m.callInvitations[invitation.contact.id] = invitation
|
||||
activateCall(invitation)
|
||||
|
||||
// This will be called from notification service extension
|
||||
// CXProvider.reportNewIncomingVoIPPushPayload([
|
||||
@@ -790,6 +787,27 @@ func processContactSubError(_ contact: Contact, _ chatError: ChatError) {
|
||||
m.updateNetworkStatus(contact.id, .error(err))
|
||||
}
|
||||
|
||||
func refreshCallInvitations() throws {
|
||||
let m = ChatModel.shared
|
||||
let callInvitations = try apiGetCallInvitations()
|
||||
m.callInvitations = callInvitations.reduce(into: [ChatId: RcvCallInvitation]()) { result, inv in result[inv.contact.id] = inv }
|
||||
if let inv = callInvitations.last {
|
||||
activateCall(inv)
|
||||
}
|
||||
}
|
||||
|
||||
func activateCall(_ callInvitation: RcvCallInvitation) {
|
||||
let m = ChatModel.shared
|
||||
CallController.shared.reportNewIncomingCall(invitation: callInvitation) { error in
|
||||
if let error = error {
|
||||
m.callInvitations[callInvitation.contact.id]?.callkitUUID = nil
|
||||
logger.error("reportNewIncomingCall error: \(error.localizedDescription)")
|
||||
} else {
|
||||
logger.debug("reportNewIncomingCall success")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct UserResponse: Decodable {
|
||||
var user: User?
|
||||
var error: String?
|
||||
|
||||
@@ -63,8 +63,8 @@ struct SimpleXApp: App {
|
||||
let appState = appStateGroupDefault.get()
|
||||
activateChat()
|
||||
if appState.inactive && chatModel.chatRunning == true {
|
||||
// TODO refresh call invitation
|
||||
updateChats()
|
||||
updateCallInvitations()
|
||||
}
|
||||
doAuthenticate = authenticationExpired()
|
||||
default:
|
||||
@@ -123,4 +123,13 @@ struct SimpleXApp: App {
|
||||
logger.error("apiGetChats: cannot update chats \(responseError(error))")
|
||||
}
|
||||
}
|
||||
|
||||
private func updateCallInvitations() {
|
||||
do {
|
||||
try refreshCallInvitations()
|
||||
}
|
||||
catch let error {
|
||||
logger.error("apiGetCallInvitations: cannot update call invitations \(responseError(error))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ class CallController: NSObject, ObservableObject {
|
||||
// private let provider = CXProvider(configuration: CallController.configuration)
|
||||
// private let controller = CXCallController()
|
||||
private let callManager = CallManager()
|
||||
@Published var activeCallInvitation: CallInvitation?
|
||||
@Published var activeCallInvitation: RcvCallInvitation?
|
||||
|
||||
// PKPushRegistry will be used from notification service extension
|
||||
// let registry = PKPushRegistry(queue: nil)
|
||||
@@ -120,7 +120,7 @@ class CallController: NSObject, ObservableObject {
|
||||
// }
|
||||
// }
|
||||
|
||||
func reportNewIncomingCall(invitation: CallInvitation, completion: @escaping (Error?) -> Void) {
|
||||
func reportNewIncomingCall(invitation: RcvCallInvitation, completion: @escaping (Error?) -> Void) {
|
||||
logger.debug("CallController.reportNewIncomingCall")
|
||||
if !UserDefaults.standard.bool(forKey: DEFAULT_EXPERIMENTAL_CALLS) { return }
|
||||
// if CallController.useCallKit, let uuid = invitation.callkitUUID {
|
||||
@@ -142,7 +142,7 @@ class CallController: NSObject, ObservableObject {
|
||||
// }
|
||||
// }
|
||||
|
||||
func reportCallRemoteEnded(invitation: CallInvitation) {
|
||||
func reportCallRemoteEnded(invitation: RcvCallInvitation) {
|
||||
// if CallController.useCallKit, let uuid = invitation.callkitUUID {
|
||||
// provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded)
|
||||
// } else if invitation.contact.id == activeCallInvitation?.contact.id {
|
||||
@@ -172,7 +172,7 @@ class CallController: NSObject, ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
func answerCall(invitation: CallInvitation) {
|
||||
func answerCall(invitation: RcvCallInvitation) {
|
||||
callManager.answerIncomingCall(invitation: invitation)
|
||||
if invitation.contact.id == self.activeCallInvitation?.contact.id {
|
||||
self.activeCallInvitation = nil
|
||||
@@ -193,7 +193,7 @@ class CallController: NSObject, ObservableObject {
|
||||
// }
|
||||
}
|
||||
|
||||
func endCall(invitation: CallInvitation) {
|
||||
func endCall(invitation: RcvCallInvitation) {
|
||||
callManager.endCall(invitation: invitation) {
|
||||
if invitation.contact.id == self.activeCallInvitation?.contact.id {
|
||||
DispatchQueue.main.async {
|
||||
|
||||
@@ -34,7 +34,7 @@ class CallManager {
|
||||
return false
|
||||
}
|
||||
|
||||
func answerIncomingCall(invitation: CallInvitation) {
|
||||
func answerIncomingCall(invitation: RcvCallInvitation) {
|
||||
let m = ChatModel.shared
|
||||
m.callInvitations.removeValue(forKey: invitation.contact.id)
|
||||
m.activeCall = Call(
|
||||
@@ -42,13 +42,13 @@ class CallManager {
|
||||
contact: invitation.contact,
|
||||
callkitUUID: invitation.callkitUUID,
|
||||
callState: .invitationAccepted,
|
||||
localMedia: invitation.peerMedia,
|
||||
localMedia: invitation.callType.media,
|
||||
sharedKey: invitation.sharedKey
|
||||
)
|
||||
m.showCallView = true
|
||||
let useRelay = UserDefaults.standard.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
|
||||
logger.debug("answerIncomingCall useRelay \(useRelay)")
|
||||
m.callCommand = .start(media: invitation.peerMedia, aesKey: invitation.sharedKey, useWorker: true, relay: useRelay)
|
||||
m.callCommand = .start(media: invitation.callType.media, aesKey: invitation.sharedKey, useWorker: true, relay: useRelay)
|
||||
}
|
||||
|
||||
func endCall(callUUID: UUID, completed: @escaping (Bool) -> Void) {
|
||||
@@ -86,7 +86,7 @@ class CallManager {
|
||||
}
|
||||
}
|
||||
|
||||
func endCall(invitation: CallInvitation, completed: @escaping () -> Void) {
|
||||
func endCall(invitation: RcvCallInvitation, completed: @escaping () -> Void) {
|
||||
ChatModel.shared.callInvitations.removeValue(forKey: invitation.contact.id)
|
||||
Task {
|
||||
do {
|
||||
@@ -98,7 +98,7 @@ class CallManager {
|
||||
}
|
||||
}
|
||||
|
||||
private func getCallInvitation(_ callUUID: UUID) -> CallInvitation? {
|
||||
private func getCallInvitation(_ callUUID: UUID) -> RcvCallInvitation? {
|
||||
if let (_, invitation) = ChatModel.shared.callInvitations.first(where: { (_, inv) in inv.callkitUUID == callUUID }) {
|
||||
return invitation
|
||||
}
|
||||
|
||||
@@ -26,10 +26,10 @@ struct IncomingCallView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func incomingCall(_ invitation: CallInvitation) -> some View {
|
||||
private func incomingCall(_ invitation: RcvCallInvitation) -> some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack {
|
||||
Image(systemName: invitation.peerMedia == .video ? "video.fill" : "phone.fill").foregroundColor(.green)
|
||||
Image(systemName: invitation.callType.media == .video ? "video.fill" : "phone.fill").foregroundColor(.green)
|
||||
Text(invitation.callTypeText)
|
||||
}
|
||||
HStack {
|
||||
@@ -81,7 +81,7 @@ struct IncomingCallView: View {
|
||||
|
||||
struct IncomingCallView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CallController.shared.activeCallInvitation = CallInvitation.sampleData
|
||||
CallController.shared.activeCallInvitation = RcvCallInvitation.sampleData
|
||||
return IncomingCallView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ public enum ChatCommand {
|
||||
case apiSendCallAnswer(contact: Contact, answer: WebRTCSession)
|
||||
case apiSendCallExtraInfo(contact: Contact, extraInfo: WebRTCExtraInfo)
|
||||
case apiEndCall(contact: Contact)
|
||||
case apiGetCallInvitations
|
||||
case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus)
|
||||
case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64))
|
||||
case receiveFile(fileId: Int64)
|
||||
@@ -100,6 +101,7 @@ public enum ChatCommand {
|
||||
case let .apiSendCallAnswer(contact, answer): return "/_call answer @\(contact.apiId) \(encodeJSON(answer))"
|
||||
case let .apiSendCallExtraInfo(contact, extraInfo): return "/_call extra @\(contact.apiId) \(encodeJSON(extraInfo))"
|
||||
case let .apiEndCall(contact): return "/_call end @\(contact.apiId)"
|
||||
case .apiGetCallInvitations: return "/_call get"
|
||||
case let .apiCallStatus(contact, callStatus): return "/_call status @\(contact.apiId) \(callStatus.rawValue)"
|
||||
case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)"
|
||||
case let .receiveFile(fileId): return "/freceive \(fileId)"
|
||||
@@ -149,6 +151,7 @@ public enum ChatCommand {
|
||||
case .apiSendCallAnswer: return "apiSendCallAnswer"
|
||||
case .apiSendCallExtraInfo: return "apiSendCallExtraInfo"
|
||||
case .apiEndCall: return "apiEndCall"
|
||||
case .apiGetCallInvitations: return "apiGetCallInvitations"
|
||||
case .apiCallStatus: return "apiCallStatus"
|
||||
case .apiChatRead: return "apiChatRead"
|
||||
case .receiveFile: return "receiveFile"
|
||||
@@ -219,11 +222,12 @@ public enum ChatResponse: Decodable, Error {
|
||||
case sndFileCancelled(chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
|
||||
case sndFileRcvCancelled(chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
|
||||
case sndGroupFileCancelled(chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer])
|
||||
case callInvitation(contact: Contact, callType: CallType, sharedKey: String?, callTs: Date)
|
||||
case callInvitation(callInvitation: RcvCallInvitation)
|
||||
case callOffer(contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool)
|
||||
case callAnswer(contact: Contact, answer: WebRTCSession)
|
||||
case callExtraInfo(contact: Contact, extraInfo: WebRTCExtraInfo)
|
||||
case callEnded(contact: Contact)
|
||||
case callInvitations(callInvitations: [RcvCallInvitation])
|
||||
case ntfTokenStatus(status: NtfTknStatus)
|
||||
case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode)
|
||||
case ntfMessages(connEntity: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo])
|
||||
@@ -287,6 +291,7 @@ public enum ChatResponse: Decodable, Error {
|
||||
case .callAnswer: return "callAnswer"
|
||||
case .callExtraInfo: return "callExtraInfo"
|
||||
case .callEnded: return "callEnded"
|
||||
case .callInvitations: return "callInvitations"
|
||||
case .ntfTokenStatus: return "ntfTokenStatus"
|
||||
case .ntfToken: return "ntfToken"
|
||||
case .ntfMessages: return "ntfMessages"
|
||||
@@ -348,11 +353,12 @@ public enum ChatResponse: Decodable, Error {
|
||||
case let .sndFileCancelled(chatItem, _): return String(describing: chatItem)
|
||||
case let .sndFileRcvCancelled(chatItem, _): return String(describing: chatItem)
|
||||
case let .sndGroupFileCancelled(chatItem, _, _): return String(describing: chatItem)
|
||||
case let .callInvitation(contact, callType, sharedKey, _): return "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")"
|
||||
case let .callInvitation(inv): return String(describing: inv)
|
||||
case let .callOffer(contact, callType, offer, sharedKey, askConfirmation): return "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))"
|
||||
case let .callAnswer(contact, answer): return "contact: \(contact.id)\nanswer: \(String(describing: answer))"
|
||||
case let .callExtraInfo(contact, extraInfo): return "contact: \(contact.id)\nextraInfo: \(String(describing: extraInfo))"
|
||||
case let .callEnded(contact): return "contact: \(contact.id)"
|
||||
case let .callInvitations(invs): return String(describing: invs)
|
||||
case let .ntfTokenStatus(status): return String(describing: status)
|
||||
case let .ntfToken(token, status, ntfMode): return "token: \(token)\nstatus: \(status.rawValue)\nntfMode: \(ntfMode.rawValue)"
|
||||
case let .ntfMessages(connEntity, msgTs, ntfMessages): return "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))"
|
||||
|
||||
@@ -37,32 +37,24 @@ public struct WebRTCExtraInfo: Codable {
|
||||
public var rtcIceCandidates: String
|
||||
}
|
||||
|
||||
public struct CallInvitation {
|
||||
public init(contact: Contact, callkitUUID: UUID? = nil, peerMedia: CallMediaType, sharedKey: String? = nil, callTs: Date) {
|
||||
self.contact = contact
|
||||
self.callkitUUID = callkitUUID
|
||||
self.peerMedia = peerMedia
|
||||
self.sharedKey = sharedKey
|
||||
self.callTs = callTs
|
||||
}
|
||||
|
||||
public struct RcvCallInvitation: Decodable {
|
||||
public var contact: Contact
|
||||
public var callkitUUID: UUID?
|
||||
public var peerMedia: CallMediaType
|
||||
public var callkitUUID: UUID? = UUID()
|
||||
public var callType: CallType
|
||||
public var sharedKey: String?
|
||||
public var callTs: Date
|
||||
public var callTypeText: LocalizedStringKey {
|
||||
get {
|
||||
switch peerMedia {
|
||||
switch callType.media {
|
||||
case .video: return sharedKey == nil ? "video call (not e2e encrypted)" : "**e2e encrypted** video call"
|
||||
case .audio: return sharedKey == nil ? "audio call (not e2e encrypted)" : "**e2e encrypted** audio call"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static let sampleData = CallInvitation(
|
||||
public static let sampleData = RcvCallInvitation(
|
||||
contact: Contact.sampleData,
|
||||
peerMedia: .audio,
|
||||
callType: CallType(media: .audio, capabilities: CallCapabilities(encryption: false)),
|
||||
callTs: .now
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ public func createMessageReceivedNtf(_ cInfo: ChatInfo, _ cItem: ChatItem) -> UN
|
||||
)
|
||||
}
|
||||
|
||||
public func createCallInvitationNtf(_ invitation: CallInvitation) -> UNMutableNotificationContent {
|
||||
let text = invitation.peerMedia == .video
|
||||
public func createCallInvitationNtf(_ invitation: RcvCallInvitation) -> UNMutableNotificationContent {
|
||||
let text = invitation.callType.media == .video
|
||||
? NSLocalizedString("Incoming video call", comment: "notification")
|
||||
: NSLocalizedString("Incoming audio call", comment: "notification")
|
||||
return createNotification(
|
||||
|
||||
@@ -150,7 +150,7 @@ newChatController chatStore user cfg@ChatConfig {agentConfig = aCfg, tbqSize, de
|
||||
startChatController :: (MonadUnliftIO m, MonadReader ChatController m) => User -> Bool -> m (Async ())
|
||||
startChatController user subConns = do
|
||||
asks smpAgent >>= resumeAgentClient
|
||||
restoreCalls
|
||||
restoreCalls user
|
||||
s <- asks agentAsync
|
||||
readTVarIO s >>= maybe (start s) (pure . fst)
|
||||
where
|
||||
@@ -162,11 +162,13 @@ startChatController user subConns = do
|
||||
else pure Nothing
|
||||
atomically . writeTVar s $ Just (a1, a2)
|
||||
pure a1
|
||||
restoreCalls = do
|
||||
savedCalls <- fromRight [] <$> runExceptT (withStore' $ \db -> getCalls db user)
|
||||
let callsMap = M.fromList $ map (\(call@Call {contactId}) -> (contactId, call)) savedCalls
|
||||
calls <- asks currentCalls
|
||||
atomically $ writeTVar calls callsMap
|
||||
|
||||
restoreCalls :: (MonadUnliftIO m, MonadReader ChatController m) => User -> m ()
|
||||
restoreCalls user = do
|
||||
savedCalls <- fromRight [] <$> runExceptT (withStore' $ \db -> getCalls db user)
|
||||
let callsMap = M.fromList $ map (\call@Call {contactId} -> (contactId, call)) savedCalls
|
||||
calls <- asks currentCalls
|
||||
atomically $ writeTVar calls callsMap
|
||||
|
||||
stopChatController :: MonadUnliftIO m => ChatController -> m ()
|
||||
stopChatController ChatController {smpAgent, agentAsync = s} = do
|
||||
@@ -213,7 +215,9 @@ processChatCommand = \case
|
||||
APIStopChat -> do
|
||||
ask >>= stopChatController
|
||||
pure CRChatStopped
|
||||
APIActivateChat -> withAgent activateAgent $> CRCmdOk
|
||||
APIActivateChat -> do
|
||||
withUser $ \user -> restoreCalls user
|
||||
withAgent activateAgent $> CRCmdOk
|
||||
APISuspendChat t -> withAgent (`suspendAgent` t) $> CRCmdOk
|
||||
ResubscribeAllConnections -> withUser (subscribeUserConnections resubscribeConnection) $> CRCmdOk
|
||||
SetFilesFolder filesFolder' -> do
|
||||
@@ -544,8 +548,9 @@ processChatCommand = \case
|
||||
SndMessage {msgId} <- sendDirectContactMessage ct (XCallEnd callId)
|
||||
updateCallItemStatus userId ct call WCSDisconnected $ Just msgId
|
||||
pure Nothing
|
||||
APIGetCallInvitations -> withUser $ \user@User {userId} -> do
|
||||
invs <- mapMaybe callInvitation <$> withStore' (`getCalls` user)
|
||||
APIGetCallInvitations -> withUser $ \User {userId} -> do
|
||||
calls <- asks currentCalls >>= readTVarIO
|
||||
let invs = mapMaybe callInvitation $ M.elems calls
|
||||
CRCallInvitations <$> mapM (rcvCallInvitation userId) invs
|
||||
where
|
||||
callInvitation Call {contactId, callState, callTs} = case callState of
|
||||
|
||||
@@ -33,6 +33,7 @@ data Call = Call
|
||||
callState :: CallState,
|
||||
callTs :: UTCTime
|
||||
}
|
||||
deriving (Show)
|
||||
|
||||
isRcvInvitation :: Call -> Bool
|
||||
isRcvInvitation Call {callState} = case callState of
|
||||
@@ -88,7 +89,7 @@ data CallState
|
||||
peerCallSession :: WebRTCSession,
|
||||
sharedKey :: Maybe C.Key
|
||||
}
|
||||
deriving (Generic)
|
||||
deriving (Show, Generic)
|
||||
|
||||
-- database representation
|
||||
instance FromJSON CallState where
|
||||
|
||||
@@ -3693,6 +3693,7 @@ getCalls db User {userId} = do
|
||||
contact_id, shared_call_id, chat_item_id, call_state, call_ts
|
||||
FROM calls
|
||||
WHERE user_id = ?
|
||||
ORDER BY call_ts ASC
|
||||
|]
|
||||
(Only userId)
|
||||
where
|
||||
|
||||
Reference in New Issue
Block a user