diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index c8f09cfe90..613003c036 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -268,9 +268,8 @@ open class ChatController(val ctrl: ChatCtrl, val alertManager: SimplexApp.Alert // if chatModel.upsertChatItem(cInfo, cItem) { // NtfManager.shared.notifyMessageReceived(cInfo, cItem) // } -// default: -// logger.debug("unsupported event: \(res.responseType)") -// } + else -> + Log.d("SIMPLEX" , "unsupported event: ${r.responseType}") } } @@ -394,6 +393,7 @@ class APIResponse(val resp: CR, val corr: String? = null) { sealed class CR { @Serializable @SerialName("activeUser") class ActiveUser(val user: User): CR() @Serializable @SerialName("chatStarted") class ChatStarted: CR() + @Serializable @SerialName("chatRunning") class ChatRunning: CR() @Serializable @SerialName("apiChats") class ApiChats(val chats: List): CR() @Serializable @SerialName("apiChat") class ApiChat(val chat: Chat): CR() @Serializable @SerialName("invitation") class Invitation(val connReqInvitation: String): CR() @@ -430,6 +430,7 @@ sealed class CR { val responseType: String get() = when(this) { is ActiveUser -> "activeUser" is ChatStarted -> "chatStarted" + is ChatRunning -> "chatRunning" is ApiChats -> "apiChats" is ApiChat -> "apiChats" is Invitation -> "invitation" @@ -467,6 +468,7 @@ sealed class CR { val details: String get() = when(this) { is ActiveUser -> json.encodeToString(user) is ChatStarted -> noDetails() + is ChatRunning -> noDetails() is ApiChats -> json.encodeToString(chats) is ApiChat -> json.encodeToString(chat) is Invitation -> connReqInvitation diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index 36f233972c..ab891a6613 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -13,32 +13,31 @@ struct ContentView: View { @State private var showNotificationAlert = false var body: some View { - ZStack { - if let user = chatModel.currentUser { - ChatListView(user: user) - .onAppear { - do { - try apiStartChat() - chatModel.chats = try apiGetChats() - } catch { - fatalError("Failed to start or load chats: \(error)") - } - ChatReceiver.shared.start() - NtfManager.shared.requestAuthorization(onDeny: { - alertManager.showAlert(notificationAlert()) - }) + if let user = chatModel.currentUser { + ChatListView(user: user) + .onAppear { + do { + try apiStartChat() + chatModel.chats = try apiGetChats() + } catch { + fatalError("Failed to start or load chats: \(error)") } - } else { - WelcomeView() - } + ChatReceiver.shared.start() + NtfManager.shared.requestAuthorization(onDeny: { + alertManager.showAlert(notificationAlert()) + }) + } + .alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! } + } else { + WelcomeView() + .alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! } } - .alert(isPresented: $alertManager.presentAlert) { alertManager.alertView! } } func notificationAlert() -> Alert { Alert( title: Text("Notification are disabled!"), - message: Text("Please open settings to enable"), + message: Text("The app can notify you when you receive messages or contact requests - please open settings to enable."), primaryButton: .default(Text("Open Settings")) { DispatchQueue.main.async { UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 5717791bb6..16b38c19b9 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -95,6 +95,7 @@ enum ChatResponse: Decodable, Error { case response(type: String, json: String) case activeUser(user: User) case chatStarted + case chatRunning case apiChats(chats: [ChatData]) case apiChat(chat: ChatData) case invitation(connReqInvitation: String) @@ -131,6 +132,7 @@ enum ChatResponse: Decodable, Error { case let .response(type, _): return "* \(type)" case .activeUser: return "activeUser" case .chatStarted: return "chatStarted" + case .chatRunning: return "chatRunning" case .apiChats: return "apiChats" case .apiChat: return "apiChat" case .invitation: return "invitation" @@ -170,6 +172,7 @@ enum ChatResponse: Decodable, Error { case let .response(_, json): return json case let .activeUser(user): return String(describing: user) case .chatStarted: return noDetails + case .chatRunning: return noDetails case let .apiChats(chats): return String(describing: chats) case let .apiChat(chat): return String(describing: chat) case let .invitation(connReqInvitation): return connReqInvitation @@ -306,8 +309,8 @@ func apiSendMessage(type: ChatType, id: Int64, msg: MsgContent) async throws -> throw r } -func apiAddContact() async throws -> String { - let r = await chatSendCmd(.addContact) +func apiAddContact() throws -> String { + let r = chatSendCmdSync(.addContact) if case let .invitation(connReqInvitation) = r { return connReqInvitation } throw r } @@ -574,7 +577,10 @@ private func getChatCtrl() -> chat_ctrl { if let controller = chatController { return controller } let dataDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.path + "/mobile_v1" var cstr = dataDir.cString(using: .utf8)! + logger.debug("getChatCtrl: chat_init") + ChatModel.shared.terminalItems.append(.cmd(.now, .string("chat_init"))) chatController = chat_init(&cstr) + ChatModel.shared.terminalItems.append(.resp(.now, .response(type: "chat_controller", json: "chat_controller: no details"))) return chatController! } diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index fe1e24ea39..766784b0e8 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -84,7 +84,7 @@ struct ChatListView: View { let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)") return Alert( title: Text("Connect via \(action) link?"), - message: Text("Your profile will be sent to the contact that you received this link from: \(link)"), + message: Text("Your profile will be sent to the contact that you received this link from"), primaryButton: .default(Text("Connect")) { DispatchQueue.main.async { Task { diff --git a/apps/ios/Shared/Views/NewChat/AddContactView.swift b/apps/ios/Shared/Views/NewChat/AddContactView.swift index f6d62deb26..3c924697ec 100644 --- a/apps/ios/Shared/Views/NewChat/AddContactView.swift +++ b/apps/ios/Shared/Views/NewChat/AddContactView.swift @@ -22,7 +22,9 @@ struct AddContactView: View { .multilineTextAlignment(.center) QRCode(uri: connReqInvitation) .padding() - Text("If you can't show QR code, you can share the invitation link via any channel") + (Text("If you cannot meet in person, you can ") + + Text("scan QR code in the video call").bold() + + Text(", or you can share the invitation link via any other channel.")) .font(.subheadline) .multilineTextAlignment(.center) .padding(.horizontal) diff --git a/apps/ios/Shared/Views/NewChat/NewChatButton.swift b/apps/ios/Shared/Views/NewChat/NewChatButton.swift index db8e1d9f39..d984ddd0df 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatButton.swift @@ -35,18 +35,20 @@ struct NewChatButton: View { } func addContactAction() { - Task { - do { - connReqInvitation = try await apiAddContact() - addContact = true - } catch { - DispatchQueue.global().async { - connectionErrorAlert(error) - } - logger.error("NewChatButton.addContactAction apiAddContact error: \(error.localizedDescription)") + do { + connReqInvitation = try apiAddContact() + addContact = true + } catch { + DispatchQueue.global().async { + connectionErrorAlert(error) } + logger.error("NewChatButton.addContactAction apiAddContact error: \(error.localizedDescription)") } } + + func addContactSheet() -> some View { + AddContactView(connReqInvitation: connReqInvitation) + } func connectContactSheet() -> some View { ConnectContactView(completed: { err in diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index d12a5cfa6e..d5afd8003a 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -144,7 +144,10 @@ processChatCommand = \case user <- withStore $ \st -> createUser st p True atomically . writeTVar u $ Just user pure $ CRActiveUser user - StartChat -> withUser' $ \user -> startChatController user $> CRChatStarted + StartChat -> withUser' $ \user -> + asks agentAsync >>= readTVarIO >>= \case + Just _ -> pure CRChatRunning + _ -> startChatController user $> CRChatStarted APIGetChats -> CRApiChats <$> withUser (\user -> withStore (`getChatPreviews` user)) APIGetChat cType cId pagination -> withUser $ \user -> case cType of CTDirect -> CRApiChat . AChat SCTDirect <$> withStore (\st -> getDirectChat st user cId pagination) @@ -528,7 +531,9 @@ subscribeUserConnections user@User {userId} = do subscribe cId `catchError` (toView . CRRcvFileSubError ft) subscribePendingConnections n = do cs <- withStore (`getPendingConnections` user) - subscribeConns n cs `catchError` \_ -> pure () + summary <- pooledForConcurrentlyN n cs $ \Connection {agentConnId = acId@(AgentConnId cId)} -> + PendingSubStatus acId <$> ((subscribe cId $> Nothing) `catchError` (pure . Just)) + toView $ CRPendingSubSummary summary subscribeUserContactLink n = do cs <- withStore (`getUserContactLinkConnections` userId) (subscribeConns n cs >> toView CRUserContactLinkSubscribed) diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 25c7a10270..079d32fb3d 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -133,6 +133,7 @@ data ChatCommand data ChatResponse = CRActiveUser {user :: User} | CRChatStarted + | CRChatRunning | CRApiChats {chats :: [AChat]} | CRApiChat {chat :: AChat} | CRNewChatItem {chatItem :: AChatItem} @@ -204,6 +205,7 @@ data ChatResponse | CRMemberSubError {groupInfo :: GroupInfo, contactName :: ContactName, chatError :: ChatError} -- TODO Contact? or GroupMember? | CRMemberSubErrors {memberSubErrors :: [MemberSubError]} | CRGroupSubscribed {groupInfo :: GroupInfo} + | CRPendingSubSummary {pendingSubStatus :: [PendingSubStatus]} | CRSndFileSubError {sndFileTransfer :: SndFileTransfer, chatError :: ChatError} | CRRcvFileSubError {rcvFileTransfer :: RcvFileTransfer, chatError :: ChatError} | CRUserContactLinkSubscribed @@ -236,6 +238,16 @@ data MemberSubError = MemberSubError instance ToJSON MemberSubError where toEncoding = J.genericToEncoding J.defaultOptions +data PendingSubStatus = PendingSubStatus + { connId :: AgentConnId, + connError :: Maybe ChatError + } + deriving (Show, Generic) + +instance ToJSON PendingSubStatus where + toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} + toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} + data ChatError = ChatError {errorType :: ChatErrorType} | ChatErrorAgent {agentError :: AgentErrorType} diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index f0286d7251..e753ed7dfb 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -39,6 +39,7 @@ responseToView :: Bool -> ChatResponse -> [StyledString] responseToView testView = \case CRActiveUser User {profile} -> viewUserProfile profile CRChatStarted -> ["chat started"] + CRChatRunning -> [] CRApiChats chats -> if testView then testViewChats chats else [plain . bshow $ J.encode chats] CRApiChat chat -> if testView then testViewChat chat else [plain . bshow $ J.encode chat] CRNewChatItem (AChatItem _ _ chat item) -> viewChatItem chat item @@ -103,9 +104,9 @@ responseToView testView = \case CRContactSubscribed c -> [ttyContact' c <> ": connected to server"] CRContactSubError c e -> [ttyContact' c <> ": contact error " <> sShow e] CRContactSubSummary summary -> - (if null connected then [] else [sShow (length connected) <> " contacts connected (use " <> highlight' "/cs" <> " for the list)"]) <> viewErrorsSummary errors " contact errors" + (if null subscribed then [] else [sShow (length subscribed) <> " contacts connected (use " <> highlight' "/cs" <> " for the list)"]) <> viewErrorsSummary errors " contact errors" where - (errors, connected) = partition (isJust . contactError) summary + (errors, subscribed) = partition (isJust . contactError) summary CRGroupInvitation GroupInfo {localDisplayName = ldn, groupProfile = GroupProfile {fullName}} -> [groupInvitation ldn fullName] CRReceivedGroupInvitation g c role -> viewReceivedGroupInvitation g c role @@ -122,6 +123,10 @@ responseToView testView = \case CRMemberSubError g c e -> [ttyGroup' g <> " member " <> ttyContact c <> " error: " <> sShow e] CRMemberSubErrors summary -> viewErrorsSummary summary " group member errors" CRGroupSubscribed g -> [ttyFullGroup g <> ": connected to server(s)"] + CRPendingSubSummary summary -> + (if null subscribed then [] else [sShow (length subscribed) <> " pending connections subscribed"]) <> viewErrorsSummary errors " connection errors" + where + (errors, subscribed) = partition (isJust . connError) summary CRSndFileSubError SndFileTransfer {fileId, fileName} e -> ["sent file " <> sShow fileId <> " (" <> plain fileName <> ") error: " <> sShow e] CRRcvFileSubError RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}} e ->