From 996c6efddd441994e8fe55ef3b77d4b2c3d251d5 Mon Sep 17 00:00:00 2001 From: Arturs Krumins Date: Wed, 21 Aug 2024 21:16:57 +0300 Subject: [PATCH] ios: prevent hangs when opening app from background with async api calls (#4730) * ios: async api calls on entering foreground * rename --------- Co-authored-by: Evgeny Poberezkin --- apps/ios/Shared/Model/SimpleXAPI.swift | 43 ++++++++++++------- apps/ios/Shared/SimpleXApp.swift | 20 +++++---- .../Shared/Views/Call/CallController.swift | 2 +- .../Shared/Views/ChatList/UserPicker.swift | 21 +++++---- 4 files changed, 52 insertions(+), 34 deletions(-) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 3abd1e92d1..ebc58c6a05 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1218,12 +1218,18 @@ func apiEndCall(_ contact: Contact) async throws { try await sendCommandOkResp(.apiEndCall(contact: contact)) } -func apiGetCallInvitations() throws -> [RcvCallInvitation] { +func apiGetCallInvitationsSync() throws -> [RcvCallInvitation] { let r = chatSendCmdSync(.apiGetCallInvitations) if case let .callInvitations(invs) = r { return invs } throw r } +func apiGetCallInvitations() async throws -> [RcvCallInvitation] { + let r = await chatSendCmd(.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)) @@ -1517,7 +1523,7 @@ func startChat(refreshInvitations: Bool = true) throws { try getUserChatData() NtfManager.shared.setNtfBadgeCount(m.totalUnreadCountForAllUsers()) if (refreshInvitations) { - try refreshCallInvitations() + Task { try await refreshCallInvitations() } } (m.savedToken, m.tokenStatus, m.notificationMode, m.notificationServer) = apiGetNtfToken() _ = try apiStartChat() @@ -2161,23 +2167,30 @@ func chatItemSimpleUpdate(_ user: any UserLike, _ aChatItem: AChatItem) async { } } -func refreshCallInvitations() throws { +func refreshCallInvitations() async throws { let m = ChatModel.shared - let callInvitations = try justRefreshCallInvitations() - if let (chatId, ntfAction) = m.ntfCallInvitationAction, - let invitation = m.callInvitations.removeValue(forKey: chatId) { - m.ntfCallInvitationAction = nil - CallController.shared.callAction(invitation: invitation, action: ntfAction) - } else if let invitation = callInvitations.last(where: { $0.user.showNotifications }) { - activateCall(invitation) + let callInvitations = try await apiGetCallInvitations() + await MainActor.run { + m.callInvitations = callsByChat(callInvitations) + if let (chatId, ntfAction) = m.ntfCallInvitationAction, + let invitation = m.callInvitations.removeValue(forKey: chatId) { + m.ntfCallInvitationAction = nil + CallController.shared.callAction(invitation: invitation, action: ntfAction) + } else if let invitation = callInvitations.last(where: { $0.user.showNotifications }) { + activateCall(invitation) + } } } -func justRefreshCallInvitations() throws -> [RcvCallInvitation] { - let m = ChatModel.shared - let callInvitations = try apiGetCallInvitations() - m.callInvitations = callInvitations.reduce(into: [ChatId: RcvCallInvitation]()) { result, inv in result[inv.contact.id] = inv } - return callInvitations +func justRefreshCallInvitations() throws { + let callInvitations = try apiGetCallInvitationsSync() + ChatModel.shared.callInvitations = callsByChat(callInvitations) +} + +private func callsByChat(_ callInvitations: [RcvCallInvitation]) -> [ChatId: RcvCallInvitation] { + callInvitations.reduce(into: [ChatId: RcvCallInvitation]()) { + result, inv in result[inv.contact.id] = inv + } } func activateCall(_ callInvitation: RcvCallInvitation) { diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 621f58dc0c..7f2c3b5866 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -83,9 +83,11 @@ struct SimpleXApp: App { if appState != .stopped { startChatAndActivate { if appState.inactive && chatModel.chatRunning == true { - updateChats() - if !chatModel.showCallView && !CallController.shared.hasActiveCalls() { - updateCallInvitations() + Task { + await updateChats() + if !chatModel.showCallView && !CallController.shared.hasActiveCalls() { + await updateCallInvitations() + } } } } @@ -130,16 +132,16 @@ struct SimpleXApp: App { } } - private func updateChats() { + private func updateChats() async { do { - let chats = try apiGetChats() - chatModel.updateChats(chats) + let chats = try await apiGetChatsAsync() + await MainActor.run { chatModel.updateChats(chats) } if let id = chatModel.chatId, let chat = chatModel.getChat(id) { Task { await loadChat(chat: chat, clearItems: false) } } if let ncr = chatModel.ntfContactRequest { - chatModel.ntfContactRequest = nil + await MainActor.run { chatModel.ntfContactRequest = nil } if case let .contactRequest(contactRequest) = chatModel.getChat(ncr.chatId)?.chatInfo { Task { await acceptContactRequest(incognito: ncr.incognito, contactRequest: contactRequest) } } @@ -149,9 +151,9 @@ struct SimpleXApp: App { } } - private func updateCallInvitations() { + private func updateCallInvitations() async { do { - try refreshCallInvitations() + try await refreshCallInvitations() } catch let error { logger.error("apiGetCallInvitations: cannot update call invitations \(responseError(error))") } diff --git a/apps/ios/Shared/Views/Call/CallController.swift b/apps/ios/Shared/Views/Call/CallController.swift index 64b565e8e6..a8a91057fa 100644 --- a/apps/ios/Shared/Views/Call/CallController.swift +++ b/apps/ios/Shared/Views/Call/CallController.swift @@ -186,7 +186,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse logger.debug("CallController: started chat") self.shouldSuspendChat = true // There are no invitations in the model, as it was processed by NSE - _ = try? justRefreshCallInvitations() + 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 diff --git a/apps/ios/Shared/Views/ChatList/UserPicker.swift b/apps/ios/Shared/Views/ChatList/UserPicker.swift index eeb7bf14f4..5041e093db 100644 --- a/apps/ios/Shared/Views/ChatList/UserPicker.swift +++ b/apps/ios/Shared/Views/ChatList/UserPicker.swift @@ -85,15 +85,18 @@ struct UserPicker: View { .padding(8) .opacity(userPickerVisible ? 1.0 : 0.0) .onAppear { - do { - // This check prevents the call of listUsers after the app is suspended, and the database is closed. - if case .active = scenePhase { - m.users = try listUsers() - } - } catch let error { - logger.error("Error loading users \(responseError(error))") - } - } + // This check prevents the call of listUsers after the app is suspended, and the database is closed. + if case .active = scenePhase { + Task { + do { + let users = try await listUsersAsync() + await MainActor.run { m.users = users } + } catch { + logger.error("Error loading users \(responseError(error))") + } + } + } + } } private func userView(_ u: UserInfo) -> some View {