diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index fe5f9f5dbc..020b40f73e 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -2199,9 +2199,11 @@ func refreshCallInvitations() async throws { } } -func justRefreshCallInvitations() throws { +func justRefreshCallInvitations() async throws { let callInvitations = try apiGetCallInvitationsSync() - ChatModel.shared.callInvitations = callsByChat(callInvitations) + await MainActor.run { + ChatModel.shared.callInvitations = callsByChat(callInvitations) + } } private func callsByChat(_ callInvitations: [RcvCallInvitation]) -> [ChatId: RcvCallInvitation] { @@ -2211,12 +2213,13 @@ private func callsByChat(_ callInvitations: [RcvCallInvitation]) -> [ChatId: Rcv } func activateCall(_ callInvitation: RcvCallInvitation) { - if !callInvitation.user.showNotifications { return } let m = ChatModel.shared + logger.debug("reportNewIncomingCall activeCallUUID \(String(describing: m.activeCall?.callUUID)) invitationUUID \(String(describing: callInvitation.callUUID))") + if !callInvitation.user.showNotifications || m.activeCall?.callUUID == callInvitation.callUUID { return } CallController.shared.reportNewIncomingCall(invitation: callInvitation) { error in if let error = error { DispatchQueue.main.async { - m.callInvitations[callInvitation.contact.id]?.callkitUUID = nil + m.callInvitations[callInvitation.contact.id]?.callUUID = nil } logger.error("reportNewIncomingCall error: \(error.localizedDescription)") } else { diff --git a/apps/ios/Shared/Views/Call/ActiveCallView.swift b/apps/ios/Shared/Views/Call/ActiveCallView.swift index 97415018bf..d238c2dbae 100644 --- a/apps/ios/Shared/Views/Call/ActiveCallView.swift +++ b/apps/ios/Shared/Views/Call/ActiveCallView.swift @@ -185,7 +185,7 @@ struct ActiveCallView: View { case .ended: closeCallView(client) call.callState = .ended - if let uuid = call.callkitUUID { + if let uuid = call.callUUID { CallController.shared.endCall(callUUID: uuid) } case .ok: @@ -382,7 +382,7 @@ struct ActiveCallOverlay: View { private func endCallButton() -> some View { let cc = CallController.shared return callButton("phone.down.fill", width: 60, height: 60) { - if let uuid = call.callkitUUID { + if let uuid = call.callUUID { cc.endCall(callUUID: uuid) } else { cc.endCall(call: call) {} @@ -462,9 +462,9 @@ struct ActiveCallOverlay: View { struct ActiveCallOverlay_Previews: PreviewProvider { static var previews: some View { Group{ - ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callkitUUID: UUID(), callState: .offerSent, localMedia: .video), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil))) + ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callUUID: UUID().uuidString.lowercased(), callState: .offerSent, localMedia: .video), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil))) .background(.black) - ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callkitUUID: UUID(), callState: .offerSent, localMedia: .audio), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil))) + ActiveCallOverlay(call: Call(direction: .incoming, contact: Contact.sampleData, callUUID: UUID().uuidString.lowercased(), callState: .offerSent, localMedia: .audio), client: WebRTCClient(Binding.constant(nil), { _ in }, Binding.constant(nil))) .background(.black) } } diff --git a/apps/ios/Shared/Views/Call/CallController.swift b/apps/ios/Shared/Views/Call/CallController.swift index a8a91057fa..36887d6184 100644 --- a/apps/ios/Shared/Views/Call/CallController.swift +++ b/apps/ios/Shared/Views/Call/CallController.swift @@ -51,7 +51,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse func provider(_ provider: CXProvider, perform action: CXStartCallAction) { logger.debug("CallController.provider CXStartCallAction") - if callManager.startOutgoingCall(callUUID: action.callUUID) { + if callManager.startOutgoingCall(callUUID: action.callUUID.uuidString.lowercased()) { action.fulfill() provider.reportOutgoingCall(with: action.callUUID, startedConnectingAt: nil) } else { @@ -61,12 +61,30 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { logger.debug("CallController.provider CXAnswerCallAction") - if callManager.answerIncomingCall(callUUID: action.callUUID) { - // WebRTC call should be in connected state to fulfill. - // Otherwise no audio and mic working on lockscreen - fulfillOnConnect = action - } else { - action.fail() + Task { + let chatIsReady = await waitUntilChatStarted(timeoutMs: 30_000, stepMs: 500) + logger.debug("CallController chat started \(chatIsReady) \(ChatModel.shared.chatInitialized) \(ChatModel.shared.chatRunning == true) \(String(describing: AppChatState.shared.value))") + if !chatIsReady { + action.fail() + return + } + if !ChatModel.shared.callInvitations.values.contains(where: { inv in inv.callUUID == action.callUUID.uuidString.lowercased() }) { + try? await justRefreshCallInvitations() + logger.debug("CallController: updated call invitations chat") + } + await MainActor.run { + logger.debug("CallController.provider will answer on call") + + if callManager.answerIncomingCall(callUUID: action.callUUID.uuidString.lowercased()) { + logger.debug("CallController.provider answered on call") + // WebRTC call should be in connected state to fulfill. + // Otherwise no audio and mic working on lockscreen + fulfillOnConnect = action + } else { + logger.debug("CallController.provider will fail the call") + action.fail() + } + } } } @@ -75,7 +93,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse // Should be nil here if connection was in connected state fulfillOnConnect?.fail() fulfillOnConnect = nil - callManager.endCall(callUUID: action.callUUID) { ok in + callManager.endCall(callUUID: action.callUUID.uuidString.lowercased()) { ok in if ok { action.fulfill() } else { @@ -86,7 +104,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse } func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) { - if callManager.enableMedia(media: .audio, enable: !action.isMuted, callUUID: action.callUUID) { + if callManager.enableMedia(media: .audio, enable: !action.isMuted, callUUID: action.callUUID.uuidString.lowercased()) { action.fulfill() } else { action.fail() @@ -156,6 +174,19 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse } } + private func waitUntilChatStarted(timeoutMs: UInt64, stepMs: UInt64) async -> Bool { + logger.debug("CallController waiting until chat started") + var t: UInt64 = 0 + repeat { + if ChatModel.shared.chatInitialized, ChatModel.shared.chatRunning == true, case .active = AppChatState.shared.value { + return true + } + _ = try? await Task.sleep(nanoseconds: stepMs * 1000000) + t += stepMs + } while t < timeoutMs + return false + } + @objc(pushRegistry:didUpdatePushCredentials:forType:) func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) { logger.debug("CallController: didUpdate push credentials for type \(type.rawValue)") @@ -171,32 +202,19 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse self.reportExpiredCall(payload: payload, completion) return } - if (!ChatModel.shared.chatInitialized) { - logger.debug("CallController: initializing chat") - do { - try initializeChat(start: true, refreshInvitations: false) - } catch let error { - logger.error("CallController: initializing chat error: \(error)") - self.reportExpiredCall(payload: payload, completion) - return - } - } - logger.debug("CallController: initialized chat") - startChatForCall() - logger.debug("CallController: started chat") - self.shouldSuspendChat = true - // There are no invitations in the model, as it was processed by NSE - try? justRefreshCallInvitations() - logger.debug("CallController: updated call invitations chat") - // logger.debug("CallController justRefreshCallInvitations: \(String(describing: m.callInvitations))") // Extract the call information from the push notification payload let m = ChatModel.shared if let contactId = payload.dictionaryPayload["contactId"] as? String, - let invitation = m.callInvitations[contactId] { - let update = self.cxCallUpdate(invitation: invitation) - if let uuid = invitation.callkitUUID { + let displayName = payload.dictionaryPayload["displayName"] as? String, + let callUUID = payload.dictionaryPayload["callUUID"] as? String, + let uuid = UUID(uuidString: callUUID), + let callTsInterval = payload.dictionaryPayload["callTs"] as? TimeInterval, + let mediaStr = payload.dictionaryPayload["media"] as? String, + let media = CallMediaType(rawValue: mediaStr) { + let update = self.cxCallUpdate(contactId, displayName, media) + let callTs = Date(timeIntervalSince1970: callTsInterval) + if callTs.timeIntervalSinceNow >= -180 { logger.debug("CallController: report pushkit call via CallKit") - let update = self.cxCallUpdate(invitation: invitation) self.provider.reportNewIncomingCall(with: uuid, update: update) { error in if error != nil { m.callInvitations.removeValue(forKey: contactId) @@ -205,11 +223,31 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse completion() } } else { + logger.debug("CallController will expire call 1") self.reportExpiredCall(update: update, completion) } } else { + logger.debug("CallController will expire call 2") self.reportExpiredCall(payload: payload, completion) } + + //DispatchQueue.main.asyncAfter(deadline: .now() + 10) { + if (!ChatModel.shared.chatInitialized) { + logger.debug("CallController: initializing chat") + do { + try initializeChat(start: true, refreshInvitations: false) + } catch let error { + logger.error("CallController: initializing chat error: \(error)") + if let call = ChatModel.shared.activeCall { + self.endCall(call: call, completed: completion) + } + return + } + } + logger.debug("CallController: initialized chat") + startChatForCall() + logger.debug("CallController: started chat") + self.shouldSuspendChat = true } // This function fulfils the requirement to always report a call when PushKit notification is received, @@ -239,8 +277,8 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse } func reportNewIncomingCall(invitation: RcvCallInvitation, completion: @escaping (Error?) -> Void) { - logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callkitUUID))") - if CallController.useCallKit(), let uuid = invitation.callkitUUID { + logger.debug("CallController.reportNewIncomingCall, UUID=\(String(describing: invitation.callUUID))") + if CallController.useCallKit(), let callUUID = invitation.callUUID, let uuid = UUID(uuidString: callUUID) { if invitation.callTs.timeIntervalSinceNow >= -180 { let update = cxCallUpdate(invitation: invitation) provider.reportNewIncomingCall(with: uuid, update: update, completion: completion) @@ -261,6 +299,14 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse return update } + private func cxCallUpdate(_ contactId: String, _ displayName: String, _ media: CallMediaType) -> CXCallUpdate { + let update = CXCallUpdate() + update.remoteHandle = CXHandle(type: .generic, value: contactId) + update.hasVideo = media == .video + update.localizedCallerName = displayName + return update + } + func reportIncomingCall(call: Call, connectedAt dateConnected: Date?) { logger.debug("CallController: reporting incoming call connected") if CallController.useCallKit() { @@ -272,14 +318,14 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse func reportOutgoingCall(call: Call, connectedAt dateConnected: Date?) { logger.debug("CallController: reporting outgoing call connected") - if CallController.useCallKit(), let uuid = call.callkitUUID { + if CallController.useCallKit(), let callUUID = call.callUUID, let uuid = UUID(uuidString: callUUID) { provider.reportOutgoingCall(with: uuid, connectedAt: dateConnected) } } func reportCallRemoteEnded(invitation: RcvCallInvitation) { logger.debug("CallController: reporting remote ended") - if CallController.useCallKit(), let uuid = invitation.callkitUUID { + if CallController.useCallKit(), let callUUID = invitation.callUUID, let uuid = UUID(uuidString: callUUID) { provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded) } else if invitation.contact.id == activeCallInvitation?.contact.id { activeCallInvitation = nil @@ -288,14 +334,17 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse func reportCallRemoteEnded(call: Call) { logger.debug("CallController: reporting remote ended") - if CallController.useCallKit(), let uuid = call.callkitUUID { + if CallController.useCallKit(), let callUUID = call.callUUID, let uuid = UUID(uuidString: callUUID) { provider.reportCall(with: uuid, endedAt: nil, reason: .remoteEnded) } } func startCall(_ contact: Contact, _ media: CallMediaType) { logger.debug("CallController.startCall") - let uuid = callManager.newOutgoingCall(contact, media) + let callUUID = callManager.newOutgoingCall(contact, media) + guard let uuid = UUID(uuidString: callUUID) else { + return + } if CallController.useCallKit() { let handle = CXHandle(type: .generic, value: contact.id) let action = CXStartCallAction(call: uuid, handle: handle) @@ -307,8 +356,8 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse update.localizedCallerName = contact.displayName self.provider.reportCall(with: uuid, updated: update) } - } else if callManager.startOutgoingCall(callUUID: uuid) { - if callManager.startOutgoingCall(callUUID: uuid) { + } else if callManager.startOutgoingCall(callUUID: callUUID) { + if callManager.startOutgoingCall(callUUID: callUUID) { logger.debug("CallController.startCall: call started") } else { logger.error("CallController.startCall: no active call") @@ -318,8 +367,8 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse func answerCall(invitation: RcvCallInvitation) { logger.debug("CallController: answering a call") - if CallController.useCallKit(), let callUUID = invitation.callkitUUID { - requestTransaction(with: CXAnswerCallAction(call: callUUID)) + if CallController.useCallKit(), let callUUID = invitation.callUUID, let uuid = UUID(uuidString: callUUID) { + requestTransaction(with: CXAnswerCallAction(call: uuid)) } else { callManager.answerIncomingCall(invitation: invitation) } @@ -328,10 +377,13 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse } } - func endCall(callUUID: UUID) { - logger.debug("CallController: ending the call with UUID \(callUUID.uuidString)") + func endCall(callUUID: String) { + let uuid = UUID(uuidString: callUUID) + logger.debug("CallController: ending the call with UUID \(callUUID)") if CallController.useCallKit() { - requestTransaction(with: CXEndCallAction(call: callUUID)) + if let uuid { + requestTransaction(with: CXEndCallAction(call: uuid)) + } } else { callManager.endCall(callUUID: callUUID) { ok in if ok { diff --git a/apps/ios/Shared/Views/Call/CallManager.swift b/apps/ios/Shared/Views/Call/CallManager.swift index a6d5ea17c4..f3021815af 100644 --- a/apps/ios/Shared/Views/Call/CallManager.swift +++ b/apps/ios/Shared/Views/Call/CallManager.swift @@ -10,17 +10,17 @@ import Foundation import SimpleXChat class CallManager { - func newOutgoingCall(_ contact: Contact, _ media: CallMediaType) -> UUID { - let uuid = UUID() - let call = Call(direction: .outgoing, contact: contact, callkitUUID: uuid, callState: .waitCapabilities, localMedia: media) + func newOutgoingCall(_ contact: Contact, _ media: CallMediaType) -> String { + let uuid = UUID().uuidString.lowercased() + let call = Call(direction: .outgoing, contact: contact, callUUID: uuid, callState: .waitCapabilities, localMedia: media) call.speakerEnabled = media == .video ChatModel.shared.activeCall = call return uuid } - func startOutgoingCall(callUUID: UUID) -> Bool { + func startOutgoingCall(callUUID: String) -> Bool { let m = ChatModel.shared - if let call = m.activeCall, call.callkitUUID == callUUID { + if let call = m.activeCall, call.callUUID == callUUID { m.showCallView = true Task { await m.callCommand.processCommand(.capabilities(media: call.localMedia)) } return true @@ -28,7 +28,7 @@ class CallManager { return false } - func answerIncomingCall(callUUID: UUID) -> Bool { + func answerIncomingCall(callUUID: String) -> Bool { if let invitation = getCallInvitation(callUUID) { answerIncomingCall(invitation: invitation) return true @@ -42,7 +42,7 @@ class CallManager { let call = Call( direction: .incoming, contact: invitation.contact, - callkitUUID: invitation.callkitUUID, + callUUID: invitation.callUUID, callState: .invitationAccepted, localMedia: invitation.callType.media, sharedKey: invitation.sharedKey @@ -68,8 +68,8 @@ class CallManager { } } - func enableMedia(media: CallMediaType, enable: Bool, callUUID: UUID) -> Bool { - if let call = ChatModel.shared.activeCall, call.callkitUUID == callUUID { + func enableMedia(media: CallMediaType, enable: Bool, callUUID: String) -> Bool { + if let call = ChatModel.shared.activeCall, call.callUUID == callUUID { let m = ChatModel.shared Task { await m.callCommand.processCommand(.media(media: media, enable: enable)) } return true @@ -77,8 +77,8 @@ class CallManager { return false } - func endCall(callUUID: UUID, completed: @escaping (Bool) -> Void) { - if let call = ChatModel.shared.activeCall, call.callkitUUID == callUUID { + func endCall(callUUID: String, completed: @escaping (Bool) -> Void) { + if let call = ChatModel.shared.activeCall, call.callUUID == callUUID { endCall(call: call) { completed(true) } } else if let invitation = getCallInvitation(callUUID) { endCall(invitation: invitation) { completed(true) } @@ -126,8 +126,8 @@ class CallManager { } } - private func getCallInvitation(_ callUUID: UUID) -> RcvCallInvitation? { - if let (_, invitation) = ChatModel.shared.callInvitations.first(where: { (_, inv) in inv.callkitUUID == callUUID }) { + private func getCallInvitation(_ callUUID: String) -> RcvCallInvitation? { + if let (_, invitation) = ChatModel.shared.callInvitations.first(where: { (_, inv) in inv.callUUID == callUUID }) { return invitation } return nil diff --git a/apps/ios/Shared/Views/Call/WebRTC.swift b/apps/ios/Shared/Views/Call/WebRTC.swift index 333dc082d5..ba990981a1 100644 --- a/apps/ios/Shared/Views/Call/WebRTC.swift +++ b/apps/ios/Shared/Views/Call/WebRTC.swift @@ -18,7 +18,7 @@ class Call: ObservableObject, Equatable { var direction: CallDirection var contact: Contact - var callkitUUID: UUID? + var callUUID: String? var localMedia: CallMediaType @Published var callState: CallState @Published var localCapabilities: CallCapabilities? @@ -33,14 +33,14 @@ class Call: ObservableObject, Equatable { init( direction: CallDirection, contact: Contact, - callkitUUID: UUID?, + callUUID: String?, callState: CallState, localMedia: CallMediaType, sharedKey: String? = nil ) { self.direction = direction self.contact = contact - self.callkitUUID = callkitUUID + self.callUUID = callUUID self.callState = callState self.localMedia = localMedia self.sharedKey = sharedKey diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index acdcabd7e2..d74b7b88e6 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -568,8 +568,8 @@ struct ChatView: View { private func endCallButton(_ call: Call) -> some View { Button { - if let uuid = call.callkitUUID { - CallController.shared.endCall(callUUID: uuid) + if CallController.useCallKit(), let callUUID = call.callUUID { + CallController.shared.endCall(callUUID: callUUID) } else { CallController.shared.endCall(call: call) {} } diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 7f1ad18ec2..5411a6c14b 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -339,7 +339,9 @@ class NotificationService: UNNotificationServiceExtension { CXProvider.reportNewIncomingVoIPPushPayload([ "displayName": invitation.contact.displayName, "contactId": invitation.contact.id, - "media": invitation.callType.media.rawValue + "callUUID": invitation.callUUID ?? "", + "media": invitation.callType.media.rawValue, + "callTs": invitation.callTs.timeIntervalSince1970 ]) { error in logger.debug("reportNewIncomingVoIPPushPayload result: \(error)") deliver(error == nil ? nil : createCallInvitationNtf(invitation)) diff --git a/apps/ios/SimpleXChat/CallTypes.swift b/apps/ios/SimpleXChat/CallTypes.swift index 227a1fbda5..9f6d98e518 100644 --- a/apps/ios/SimpleXChat/CallTypes.swift +++ b/apps/ios/SimpleXChat/CallTypes.swift @@ -42,6 +42,7 @@ public struct RcvCallInvitation: Decodable { public var contact: Contact public var callType: CallType public var sharedKey: String? + public var callUUID: String? public var callTs: Date public var callTypeText: LocalizedStringKey { get { @@ -52,10 +53,8 @@ public struct RcvCallInvitation: Decodable { } } - public var callkitUUID: UUID? = UUID() - private enum CodingKeys: String, CodingKey { - case user, contact, callType, sharedKey, callTs + case user, contact, callType, sharedKey, callUUID, callTs } public static let sampleData = RcvCallInvitation( diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/CallActivity.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/CallActivity.kt index 3d29737128..a9697069c0 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/CallActivity.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/views/call/CallActivity.kt @@ -424,6 +424,7 @@ fun PreviewIncomingCallLockScreenAlert() { ) { IncomingCallLockScreenAlertLayout( invitation = RcvCallInvitation( + callUUID = "", remoteHostId = null, user = User.sampleData, contact = Contact.sampleData, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt index 285658ec1d..7704509148 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.kt @@ -47,6 +47,7 @@ class CallManager(val chatModel: ChatModel) { remoteHostId = invitation.remoteHostId, userProfile = userProfile, contact = invitation.contact, + callUUID = invitation.callUUID, callState = CallState.InvitationAccepted, localMedia = invitation.callType.media, sharedKey = invitation.sharedKey, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt index 829a849ddc..32681234fa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/IncomingCallAlertView.kt @@ -115,6 +115,7 @@ fun PreviewIncomingCallAlertLayout() { contact = Contact.sampleData, callType = CallType(media = CallMediaType.Audio, capabilities = CallCapabilities(encryption = false)), sharedKey = null, + callUUID = "", callTs = Clock.System.now() ), chatModel = ChatModel, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt index 0a7231370b..5332bc650e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt @@ -13,6 +13,7 @@ data class Call( val remoteHostId: Long?, val userProfile: Profile, val contact: Contact, + val callUUID: String?, val callState: CallState, val localMedia: CallMediaType, val localCapabilities: CallCapabilities? = null, @@ -105,6 +106,7 @@ sealed class WCallResponse { val contact: Contact, val callType: CallType, val sharedKey: String? = null, + val callUUID: String, val callTs: Instant ) { val callTypeText: String get() = generalGetString(when(callType.media) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index e90eed547d..a4fe622a6f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -544,7 +544,7 @@ fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType) if (chatInfo is ChatInfo.Direct) { val contactInfo = chatModel.controller.apiContactInfo(remoteHostId, chatInfo.contact.contactId) val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi - chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile) + chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callUUID = null, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile) chatModel.showCallView.value = true chatModel.callCommand.add(WCallCommand.Capabilities(media)) } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt index faef957705..974578882d 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt @@ -19,12 +19,76 @@ import chat.simplex.res.MR import kotlinx.coroutines.* import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient import okhttp3.OkHttpClient import okhttp3.Request import java.io.Closeable import java.io.File import java.net.InetSocketAddress import java.net.Proxy +import kotlin.math.min + +data class SemVer( + val major: Int, + val minor: Int, + val patch: Int, + val preRelease: String? = null, + val buildNumber: Int? = null, +): Comparable { + + val isNotStable: Boolean = preRelease != null + + override fun compareTo(other: SemVer?): Int { + if (other == null) return 1 + return when { + major != other.major -> major.compareTo(other.major) + minor != other.minor -> minor.compareTo(other.minor) + patch != other.patch -> patch.compareTo(other.patch) + preRelease != null && other.preRelease != null -> { + val pr = preRelease.compareTo(other.preRelease, ignoreCase = true) + when { + pr != 0 -> pr + buildNumber != null && other.buildNumber != null -> buildNumber.compareTo(other.buildNumber) + buildNumber != null -> -1 + other.buildNumber != null -> 1 + else -> 0 + } + } + preRelease != null -> -1 + other.preRelease != null -> 1 + else -> 0 + } + } + + companion object { + private val regex = Regex("^(\\d+)\\.(\\d+)\\.(\\d+)(?:-([A-Za-z]+)\\.(\\d+))?\$") + fun from(tagName: String): SemVer? { + val trimmed = tagName.trimStart { it == 'v' } + val redacted = when { + trimmed.contains('-') && trimmed.substringBefore('-').count { it == '.' } == 1 -> "${trimmed.substringBefore('-')}.0-${trimmed.substringAfter('-')}" + trimmed.substringBefore('-').count { it == '.' } == 1 -> "${trimmed}.0" + else -> trimmed + } + val group = regex.matchEntire(redacted)?.groups + return if (group != null) { + SemVer( + major = group[1]?.value?.toIntOrNull() ?: return null, + minor = group[2]?.value?.toIntOrNull() ?: return null, + patch = group[3]?.value?.toIntOrNull() ?: return null, + preRelease = group[4]?.value, + buildNumber = group[5]?.value?.toIntOrNull(), + ) + } else { + null + } + } + + fun fromCurrentVersionName(): SemVer? { + val currentVersionName = if (appPlatform.isAndroid) BuildConfigCommon.ANDROID_VERSION_NAME else BuildConfigCommon.DESKTOP_VERSION_NAME + return from(currentVersionName) + } + } +} @Serializable data class GitHubRelease( @@ -34,12 +98,18 @@ data class GitHubRelease( val htmlUrl: String, val name: String, val draft: Boolean, - val prerelease: Boolean, + @SerialName("prerelease") + private val preRelease: Boolean, val body: String, @SerialName("published_at") val publishedAt: String, val assets: List -) +) { + @Transient + val semVer: SemVer? = SemVer.from(tagName) + + val isConsideredBeta: Boolean = preRelease || semVer == null || semVer.isNotStable +} @Serializable data class GitHubAsset( @@ -105,25 +175,25 @@ private fun createUpdateJob() { fun checkForUpdate() { Log.d(TAG, "Checking for update") + val currentSemVer = SemVer.fromCurrentVersionName() + if (currentSemVer == null) { + Log.e(TAG, "Current SemVer cannot be parsed") + return + } val client = setupHttpClient() try { val request = Request.Builder().url("https://api.github.com/repos/simplex-chat/simplex-chat/releases").addHeader("User-agent", "curl").build() client.newCall(request).execute().use { response -> response.body?.use { val body = it.string() - val releases = json.decodeFromString>(body).filterNot { it.draft } + val releases = json.decodeFromString>(body) val release = when (appPrefs.appUpdateChannel.get()) { - AppUpdatesChannel.STABLE -> releases.firstOrNull { !it.prerelease } - AppUpdatesChannel.BETA -> releases.firstOrNull() + AppUpdatesChannel.STABLE -> releases.firstOrNull { r -> !r.draft && !r.isConsideredBeta && currentSemVer < r.semVer } + AppUpdatesChannel.BETA -> releases.firstOrNull { r -> !r.draft && currentSemVer < r.semVer } AppUpdatesChannel.DISABLED -> return - } ?: return - val currentVersionName = "v" + (if (appPlatform.isAndroid) BuildConfigCommon.ANDROID_VERSION_NAME else BuildConfigCommon.DESKTOP_VERSION_NAME) - val redactedCurrentVersionName = when { - currentVersionName.contains('-') && currentVersionName.substringBefore('-').count { it == '.' } == 1 -> "${currentVersionName.substringBefore('-')}.0-${currentVersionName.substringAfter('-')}" - currentVersionName.substringBefore('-').count { it == '.' } == 1 -> "${currentVersionName}.0" - else -> currentVersionName } - if (release.tagName == appPrefs.appSkippedUpdate.get() || release.tagName == currentVersionName || release.tagName == redactedCurrentVersionName) { + + if (release == null || release.tagName == appPrefs.appSkippedUpdate.get()) { Log.d(TAG, "Skipping update because of the same version or skipped version") return } @@ -298,13 +368,15 @@ private suspend fun downloadAsset(asset: GitHubAsset) { } } +private fun isRunningFromAppImage(): Boolean = System.getenv("APPIMAGE") != null + private fun isRunningFromFlatpak(): Boolean = System.getenv("container") == "flatpak" private fun chooseGitHubReleaseAssets(release: GitHubRelease): List { val res = if (isRunningFromFlatpak()) { // No need to show download options for Flatpak users emptyList() - } else if (Runtime.getRuntime().exec("which dpkg").onExit().join().exitValue() == 0) { + } else if (!isRunningFromAppImage() && Runtime.getRuntime().exec("which dpkg").onExit().join().exitValue() == 0) { // Show all available .deb packages and user will choose the one that works on his system (for Debian derivatives) release.assets.filter { it.name.lowercase().endsWith(".deb") } } else { diff --git a/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/SemVerTest.kt b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/SemVerTest.kt new file mode 100644 index 0000000000..561911773f --- /dev/null +++ b/apps/multiplatform/common/src/desktopTest/kotlin/chat/simplex/app/SemVerTest.kt @@ -0,0 +1,63 @@ +package chat.simplex.app + +import chat.simplex.common.views.helpers.SemVer +import kotlin.test.Test +import kotlin.test.assertEquals + +// use this command for testing: +// ./gradlew desktopTest +class SemVerTest { + @Test + fun testValidSemVer() { + assertEquals(SemVer.from("1.0.0"), SemVer(1, 0, 0)) + assertEquals(SemVer.from("1.0"), SemVer(1, 0, 0)) + assertEquals(SemVer.from("v1.0"), SemVer(1, 0, 0)) + assertEquals(SemVer.from("v1.0-beta.1"), SemVer(1, 0, 0, "beta", 1)) + val r = listOf>( + "0.0.4" to SemVer(0, 0, 4), + "1.2.3" to SemVer(1, 2, 3), + "10.20.30" to SemVer(10, 20, 30), + "1.0.0-alpha.1" to SemVer(1, 0, 0, "alpha", buildNumber = 1), + "1.0.0" to SemVer(1, 0, 0), + "2.0.0" to SemVer(2, 0, 0), + "1.1.7" to SemVer(1, 1, 7), + "2.0.1-alpha.1227" to SemVer(2, 0, 1, "alpha", 1227), + ) + r.forEach { (value, correct) -> + assertEquals(SemVer.from(value), correct) + } + } + + @Test + fun testComparisonSemVer() { + assert(SemVer(0, 1, 0) == SemVer.from("0.1.0")) + assert(SemVer(1, 1, 0) == SemVer.from("v1.1.0")) + assert(SemVer(0, 1, 0) > SemVer(0, 0, 1)) + assert(SemVer(1, 0, 0) > SemVer(0, 100, 100)) + assert(SemVer(0, 200, 0) > SemVer(0, 100, 100)) + assert(SemVer(0, 1, 0, "beta") > SemVer(0, 1, 0, "alpha")) + assert(SemVer(0, 1, 0) > SemVer(0, 1, 0, "alpha")) + assert(SemVer(0, 1, 0) > SemVer(0, 1, 0, "beta")) + assert(SemVer(0, 1, 0) > SemVer(0, 1, 0, "beta.0")) + assert(SemVer(0, 1, 0, "beta", 1) > SemVer(0, 1, 0, "beta", 0)) + assert(SemVer(0, 1, 0, "beta", 11) > SemVer(0, 1, 0, "beta", 10)) + assert(SemVer(0, 1, 0, "beta", 11) > SemVer(0, 1, 0, "beta", 9)) + assert(SemVer(0, 1, 0, "beta.1") > SemVer(0, 1, 0, "alpha.2")) + assert(SemVer(1, 1, 0, "beta.1") > SemVer(0, 1, 0, "beta.1")) + assert(SemVer(1, 0, 0) > SemVer(1, 0, 0, "beta.1")) + assert(SemVer(1, 0, 0) > null) + assert(SemVer.from("v6.0.0")!! > SemVer.from("v6.0.0-beta.3")) + assert(SemVer.from("v6.0.0-beta.3")!! > SemVer.from("v6.0.0-beta.2")) + assert(SemVer.from("0.1.0") == SemVer.from("0.1.0")) + assert(SemVer.from("0.1.1")!! > SemVer.from("0.1.0")) + assert(SemVer.from("0.2.1")!! > SemVer.from("0.1.1")) + assert(SemVer.from("2.0.1")!! > SemVer.from("0.1.1")) + assert(SemVer.from("0.1.1-beta.0")!! > SemVer.from("0.1.0-beta.0")) + assert(SemVer.from("0.1.1-beta.0")!! == SemVer.from("0.1.1-beta.0")) + assert(SemVer.from("0.1.1-beta.1")!! > SemVer.from("0.1.1-beta.0")) + assert(SemVer.from("10.0.0-beta.12")!! > SemVer.from("1.1.1")) + assert(SemVer.from("1.1.1-beta.120")!! > SemVer.from("1.1.1-alpha.9")) + assert(SemVer.from("1.1.1-beta.120")!! > SemVer.from("1.1.1-alpha.120")) + assert(SemVer.from("2.0.1")!! > SemVer.from("0.1.1")) + } +} diff --git a/package.yaml b/package.yaml index 3899ff4b68..3d5422612a 100644 --- a/package.yaml +++ b/package.yaml @@ -48,6 +48,7 @@ dependencies: - tls >= 1.9.0 && < 1.10 - unliftio == 0.2.* - unliftio-core == 0.2.* + - uuid == 1.3.* - zip == 2.0.* flags: diff --git a/scripts/flatpak/chat.simplex.simplex.metainfo.xml b/scripts/flatpak/chat.simplex.simplex.metainfo.xml index a74a7b86a0..4ac5f88e14 100644 --- a/scripts/flatpak/chat.simplex.simplex.metainfo.xml +++ b/scripts/flatpak/chat.simplex.simplex.metainfo.xml @@ -38,6 +38,38 @@ + + https://simplex.chat/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html + +

New in v6.0.1-3:

+
    +
  • reduce app memory usage and start time.
  • +
  • faster sending files to groups.
  • +
  • fix rare delivery bug.
  • +
+

New in v6.0:

+

New chat experience:

+
    +
  • connect to your friends faster.
  • +
  • archive contacts to chat later.
  • +
  • delete up to 20 messages at once.
  • +
  • increase font size.
  • +
+

New media options:

+
    +
  • play from the chat list.
  • +
  • blur for better privacy.
  • +
+

Private routing:

+
    +
  • it protects your IP address and connections and is now enabled by default.
  • +
+

Connection and servers information:

+
    +
  • to control your network status and usage.
  • +
+
+
https://github.com/simplex-chat/simplex-chat/releases/tag/v6.0.0 diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 72ece2f2d6..b58285a349 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -146,6 +146,7 @@ library Simplex.Chat.Migrations.M20240510_chat_items_via_proxy Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays Simplex.Chat.Migrations.M20240528_quota_err_counter + Simplex.Chat.Migrations.M20240827_calls_uuid Simplex.Chat.Mobile Simplex.Chat.Mobile.File Simplex.Chat.Mobile.Shared @@ -229,6 +230,7 @@ library , tls >=1.9.0 && <1.10 , unliftio ==0.2.* , unliftio-core ==0.2.* + , uuid ==1.3.* , zip ==2.0.* default-language: Haskell2010 if flag(swift) @@ -292,6 +294,7 @@ executable simplex-bot , tls >=1.9.0 && <1.10 , unliftio ==0.2.* , unliftio-core ==0.2.* + , uuid ==1.3.* , zip ==2.0.* default-language: Haskell2010 if flag(swift) @@ -355,6 +358,7 @@ executable simplex-bot-advanced , tls >=1.9.0 && <1.10 , unliftio ==0.2.* , unliftio-core ==0.2.* + , uuid ==1.3.* , zip ==2.0.* default-language: Haskell2010 if flag(swift) @@ -421,6 +425,7 @@ executable simplex-broadcast-bot , tls >=1.9.0 && <1.10 , unliftio ==0.2.* , unliftio-core ==0.2.* + , uuid ==1.3.* , zip ==2.0.* default-language: Haskell2010 if flag(swift) @@ -485,6 +490,7 @@ executable simplex-chat , tls >=1.9.0 && <1.10 , unliftio ==0.2.* , unliftio-core ==0.2.* + , uuid ==1.3.* , websockets ==0.12.* , zip ==2.0.* default-language: Haskell2010 @@ -555,6 +561,7 @@ executable simplex-directory-service , tls >=1.9.0 && <1.10 , unliftio ==0.2.* , unliftio-core ==0.2.* + , uuid ==1.3.* , zip ==2.0.* default-language: Haskell2010 if flag(swift) @@ -655,6 +662,7 @@ test-suite simplex-chat-test , tls >=1.9.0 && <1.10 , unliftio ==0.2.* , unliftio-core ==0.2.* + , uuid ==1.3.* , zip ==2.0.* default-language: Haskell2010 if flag(swift) diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 796a128abe..825e3efa8e 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -56,6 +56,8 @@ import Data.Time (NominalDiffTime, addUTCTime, defaultTimeLocale, formatTime) import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDay, nominalDiffTimeToSeconds) import Data.Time.Clock.System (systemToUTCTime) import Data.Word (Word32) +import qualified Data.UUID as UUID +import qualified Data.UUID.V4 as V4 import qualified Database.SQLite.Simple as SQL import Simplex.Chat.Archive import Simplex.Chat.Call @@ -1291,12 +1293,13 @@ processChatCommand' vr = \case withContactLock "sendCallInvitation" contactId $ do g <- asks random callId <- atomically $ CallId <$> C.randomBytes 16 g + callUUID <- UUID.toText <$> liftIO V4.nextRandom dhKeyPair <- atomically $ if encryptedCall callType then Just <$> C.generateKeyPair g else pure Nothing let invitation = CallInvitation {callType, callDhPubKey = fst <$> dhKeyPair} callState = CallInvitationSent {localCallType = callType, localDhPrivKey = snd <$> dhKeyPair} (msg, _) <- sendDirectContactMessage user ct (XCallInv callId invitation) ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndCall CISCallPending 0) - let call' = Call {contactId, callId, chatItemId = chatItemId' ci, callState, callTs = chatItemTs' ci} + let call' = Call {contactId, callId, callUUID, chatItemId = chatItemId' ci, callState, callTs = chatItemTs' ci} call_ <- atomically $ TM.lookupInsert contactId call' calls forM_ call_ $ \call -> updateCallItemStatus user ct call WCSDisconnected Nothing toView $ CRNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci] @@ -1366,13 +1369,13 @@ processChatCommand' vr = \case rcvCallInvitations <- rights <$> mapM rcvCallInvitation invs pure $ CRCallInvitations rcvCallInvitations where - callInvitation Call {contactId, callState, callTs} = case callState of - CallInvitationReceived {peerCallType, sharedKey} -> Just (contactId, callTs, peerCallType, sharedKey) + callInvitation Call {contactId, callUUID, callState, callTs} = case callState of + CallInvitationReceived {peerCallType, sharedKey} -> Just (contactId, callUUID, callTs, peerCallType, sharedKey) _ -> Nothing - rcvCallInvitation (contactId, callTs, peerCallType, sharedKey) = runExceptT . withFastStore $ \db -> do + rcvCallInvitation (contactId, callUUID, callTs, peerCallType, sharedKey) = runExceptT . withFastStore $ \db -> do user <- getUserByContactId db contactId contact <- getContact db vr user contactId - pure RcvCallInvitation {user, contact, callType = peerCallType, sharedKey, callTs} + pure RcvCallInvitation {user, contact, callType = peerCallType, sharedKey, callUUID, callTs} APIGetNetworkStatuses -> withUser $ \_ -> CRNetworkStatuses Nothing . map (uncurry ConnNetworkStatus) . M.toList <$> chatReadVar connNetworkStatuses APICallStatus contactId receivedStatus -> @@ -6093,9 +6096,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = g <- asks random dhKeyPair <- atomically $ if encryptedCall callType then Just <$> C.generateKeyPair g else pure Nothing ci <- saveCallItem CISCallPending + callUUID <- UUID.toText <$> liftIO V4.nextRandom let sharedKey = C.Key . C.dhBytes' <$> (C.dh' <$> callDhPubKey <*> (snd <$> dhKeyPair)) callState = CallInvitationReceived {peerCallType = callType, localDhPubKey = fst <$> dhKeyPair, sharedKey} - call' = Call {contactId, callId, chatItemId = chatItemId' ci, callState, callTs = chatItemTs' ci} + call' = Call {contactId, callId, callUUID, chatItemId = chatItemId' ci, callState, callTs = chatItemTs' ci} calls <- asks currentCalls -- theoretically, the new call invitation for the current contact can mark the in-progress call as ended -- (and replace it in ChatController) @@ -6103,7 +6107,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withStore' $ \db -> createCall db user call' $ chatItemTs' ci call_ <- atomically (TM.lookupInsert contactId call' calls) forM_ call_ $ \call -> updateCallItemStatus user ct call WCSDisconnected Nothing - toView $ CRCallInvitation RcvCallInvitation {user, contact = ct, callType, sharedKey, callTs = chatItemTs' ci} + toView $ CRCallInvitation RcvCallInvitation {user, contact = ct, callType, sharedKey, callUUID, callTs = chatItemTs' ci} toView $ CRNewChatItems user [AChatItem SCTDirect SMDRcv (DirectChat ct) ci] else featureRejected CFCalls where diff --git a/src/Simplex/Chat/Call.hs b/src/Simplex/Chat/Call.hs index 9968d170aa..882ec8ccd0 100644 --- a/src/Simplex/Chat/Call.hs +++ b/src/Simplex/Chat/Call.hs @@ -29,6 +29,7 @@ import Simplex.Messaging.Util (decodeJSON, encodeJSON) data Call = Call { contactId :: ContactId, callId :: CallId, + callUUID :: Text, chatItemId :: Int64, callState :: CallState, callTs :: UTCTime @@ -111,6 +112,7 @@ data RcvCallInvitation = RcvCallInvitation contact :: Contact, callType :: CallType, sharedKey :: Maybe C.Key, + callUUID :: Text, callTs :: UTCTime } deriving (Show) diff --git a/src/Simplex/Chat/Migrations/M20240827_calls_uuid.hs b/src/Simplex/Chat/Migrations/M20240827_calls_uuid.hs new file mode 100644 index 0000000000..eb1e8db65a --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20240827_calls_uuid.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20240827_calls_uuid where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240827_calls_uuid :: Query +m20240827_calls_uuid = + [sql| +ALTER TABLE calls ADD COLUMN call_uuid TEXT NOT NULL DEFAULT ""; +|] + +down_m20240827_calls_uuid :: Query +down_m20240827_calls_uuid = + [sql| +ALTER TABLE calls DROP COLUMN call_uuid; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index fdbc44a9c3..25cf886384 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -415,6 +415,8 @@ CREATE TABLE calls( user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) + , + call_uuid TEXT NOT NULL DEFAULT "" ); CREATE TABLE commands( command_id INTEGER PRIMARY KEY AUTOINCREMENT, -- used as ACorrId diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs index 5c9082b361..be3f4027ca 100644 --- a/src/Simplex/Chat/Store/Migrations.hs +++ b/src/Simplex/Chat/Store/Migrations.hs @@ -110,6 +110,7 @@ import Simplex.Chat.Migrations.M20240501_chat_deleted import Simplex.Chat.Migrations.M20240510_chat_items_via_proxy import Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays import Simplex.Chat.Migrations.M20240528_quota_err_counter +import Simplex.Chat.Migrations.M20240827_calls_uuid import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -219,7 +220,8 @@ schemaMigrations = ("20240501_chat_deleted", m20240501_chat_deleted, Just down_m20240501_chat_deleted), ("20240510_chat_items_via_proxy", m20240510_chat_items_via_proxy, Just down_m20240510_chat_items_via_proxy), ("20240515_rcv_files_user_approved_relays", m20240515_rcv_files_user_approved_relays, Just down_m20240515_rcv_files_user_approved_relays), - ("20240528_quota_err_counter", m20240528_quota_err_counter, Just down_m20240528_quota_err_counter) + ("20240528_quota_err_counter", m20240528_quota_err_counter, Just down_m20240528_quota_err_counter), + ("20240827_calls_uuid", m20240827_calls_uuid, Just down_m20240827_calls_uuid) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index fb87662c27..a29460d5b1 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -549,17 +549,17 @@ overwriteProtocolServers db User {userId} servers = protocol = decodeLatin1 $ strEncode $ protocolTypeI @p createCall :: DB.Connection -> User -> Call -> UTCTime -> IO () -createCall db user@User {userId} Call {contactId, callId, chatItemId, callState} callTs = do +createCall db user@User {userId} Call {contactId, callId, callUUID, chatItemId, callState} callTs = do currentTs <- getCurrentTime deleteCalls db user contactId DB.execute db [sql| INSERT INTO calls - (contact_id, shared_call_id, chat_item_id, call_state, call_ts, user_id, created_at, updated_at) - VALUES (?,?,?,?,?,?,?,?) + (contact_id, shared_call_id, call_uuid, chat_item_id, call_state, call_ts, user_id, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?) |] - (contactId, callId, chatItemId, callState, callTs, userId, currentTs, currentTs) + (contactId, callId, callUUID, chatItemId, callState, callTs, userId, currentTs, currentTs) deleteCalls :: DB.Connection -> User -> ContactId -> IO () deleteCalls db User {userId} contactId = do @@ -572,13 +572,13 @@ getCalls db = db [sql| SELECT - contact_id, shared_call_id, chat_item_id, call_state, call_ts + contact_id, shared_call_id, call_uuid, chat_item_id, call_state, call_ts FROM calls ORDER BY call_ts ASC |] where - toCall :: (ContactId, CallId, ChatItemId, CallState, UTCTime) -> Call - toCall (contactId, callId, chatItemId, callState, callTs) = Call {contactId, callId, chatItemId, callState, callTs} + toCall :: (ContactId, CallId, Text, ChatItemId, CallState, UTCTime) -> Call + toCall (contactId, callId, callUUID, chatItemId, callState, callTs) = Call {contactId, callId, callUUID, chatItemId, callState, callTs} createCommand :: DB.Connection -> User -> Maybe Int64 -> CommandFunction -> IO CommandId createCommand db User {userId} connId commandFunction = do