diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index 04eadac2c..b69ccbb7c 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -292,10 +292,7 @@ struct ContentView: View { var path = url.path if (path == "/contact" || path == "/invitation") { path.removeFirst() - // TODO normalize in backend; revert - // let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)") - var link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)") - link = link.starts(with: "simplex:/") ? link.replacingOccurrences(of: "simplex:/", with: "https://simplex.chat/") : link + let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)") planAndConnect( link, showAlert: showPlanAndConnectAlert, diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index b1a83dd5e..3cc52d502 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -62,6 +62,7 @@ final class ChatModel: ObservableObject { // current chat @Published var chatId: String? @Published var reversedChatItems: [ChatItem] = [] + var chatItemStatuses: Dictionary = [:] @Published var chatToTop: String? @Published var groupMembers: [GroupMember] = [] // items in the terminal view @@ -306,7 +307,11 @@ final class ChatModel: ObservableObject { return false } else { withAnimation(itemAnimation()) { - reversedChatItems.insert(cItem, at: hasLiveDummy ? 1 : 0) + var ci = cItem + if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus { + ci.meta.itemStatus = status + } + reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0) } return true } @@ -319,23 +324,19 @@ final class ChatModel: ObservableObject { } } - func updateChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) { + func updateChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem, status: CIStatus? = nil) { if chatId == cInfo.id, let i = getChatItemIndex(cItem) { withAnimation { _updateChatItem(at: i, with: cItem) } + } else if let status = status { + chatItemStatuses.updateValue(status, forKey: cItem.id) } } private func _updateChatItem(at i: Int, with cItem: ChatItem) { - let ci = reversedChatItems[i] reversedChatItems[i] = cItem reversedChatItems[i].viewTimestamp = .now - // on some occasions the confirmation of message being accepted by the server (tick) - // arrives earlier than the response from API, and item remains without tick - if case .sndNew = cItem.meta.itemStatus { - reversedChatItems[i].meta.itemStatus = ci.meta.itemStatus - } } private func getChatItemIndex(_ cItem: ChatItem) -> Int? { @@ -474,6 +475,7 @@ final class ChatModel: ObservableObject { } // clear current chat if chatId == cInfo.id { + chatItemStatuses = [:] reversedChatItems = [] } } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 2e724bcd8..bad15ad52 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -312,6 +312,7 @@ func loadChat(chat: Chat, search: String = "") { do { let cInfo = chat.chatInfo let m = ChatModel.shared + m.chatItemStatuses = [:] m.reversedChatItems = [] let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId, search: search) m.updateChatInfo(chat.chatInfo) @@ -593,7 +594,6 @@ func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> P } func apiConnectPlan(connReq: String) async throws -> ConnectionPlan { - logger.error("apiConnectPlan connReq: \(connReq)") let userId = try currentUserId("apiConnectPlan") let r = await chatSendCmd(.apiConnectPlan(userId: userId, connReq: connReq)) if case let .connectionPlan(_, connectionPlan) = r { return connectionPlan } @@ -1421,11 +1421,8 @@ func processReceivedMsg(_ res: ChatResponse) async { case let .chatItemStatusUpdated(user, aChatItem): let cInfo = aChatItem.chatInfo let cItem = aChatItem.chatItem - if !cItem.isDeletedContent { - let added = active(user) ? await MainActor.run { m.upsertChatItem(cInfo, cItem) } : true - if added && cItem.showNotification { - NtfManager.shared.notifyMessageReceived(user, cInfo, cItem) - } + if !cItem.isDeletedContent && active(user) { + await MainActor.run { m.updateChatItem(cInfo, cItem, status: cItem.meta.itemStatus) } } if let endTask = m.messageDelivery[cItem.id] { switch cItem.meta.itemStatus { diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 81412bf31..5438eb13b 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -168,9 +168,9 @@ struct ChatInfoView: View { if let contactLink = contact.contactLink { Section { - QRCode(uri: contactLink) + SimpleXLinkQRCode(uri: contactLink) Button { - showShareSheet(items: [contactLink]) + showShareSheet(items: [simplexChatLink(contactLink)]) } label: { Label("Share address", systemImage: "square.and.arrow.up") } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index 498b3cb2e..8b757ed1a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -121,13 +121,11 @@ private func formatText(_ ft: FormattedText, _ preview: Bool) -> Text { case .secret: return Text(t).foregroundColor(.clear).underline(color: .primary) case let .colored(color): return Text(t).foregroundColor(color.uiColor) case .uri: return linkText(t, t, preview, prefix: "") - case let .simplexLink(linkType, simplexUri, trustedUri, smpHosts): + case let .simplexLink(linkType, simplexUri, smpHosts): switch privacySimplexLinkModeDefault.get() { case .description: return linkText(simplexLinkText(linkType, smpHosts), simplexUri, preview, prefix: "") case .full: return linkText(t, simplexUri, preview, prefix: "") - case .browser: return trustedUri - ? linkText(t, t, preview, prefix: "") - : linkText(t, t, preview, prefix: "", color: .red, uiColor: .red) + case .browser: return linkText(t, simplexUri, preview, prefix: "") } case .email: return linkText(t, t, preview, prefix: "mailto:") case .phone: return linkText(t, t.replacingOccurrences(of: " ", with: ""), preview, prefix: "tel:") diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 81473709c..21af0ebe1 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -91,6 +91,7 @@ struct ChatView: View { chatModel.chatId = nil DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { if chatModel.chatId == nil { + chatModel.chatItemStatuses = [:] chatModel.reversedChatItems = [] } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift index 3731e0c4d..781870bf5 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift @@ -41,9 +41,9 @@ struct GroupLinkView: View { } } .frame(height: 36) - QRCode(uri: groupLink) + SimpleXLinkQRCode(uri: groupLink) Button { - showShareSheet(items: [groupLink]) + showShareSheet(items: [simplexChatLink(groupLink)]) } label: { Label("Share link", systemImage: "square.and.arrow.up") } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 3d10101de..4b6445814 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -94,9 +94,9 @@ struct GroupMemberInfoView: View { if let contactLink = member.contactLink { Section { - QRCode(uri: contactLink) + SimpleXLinkQRCode(uri: contactLink) Button { - showShareSheet(items: [contactLink]) + showShareSheet(items: [simplexChatLink(contactLink)]) } label: { Label("Share address", systemImage: "square.and.arrow.up") } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift index af1a778ad..860a6febb 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift @@ -27,8 +27,7 @@ struct GroupPreferencesView: View { featureSection(.directMessages, $preferences.directMessages.enable) featureSection(.reactions, $preferences.reactions.enable) featureSection(.voice, $preferences.voice.enable) -// TODO uncomment in 5.3 -// featureSection(.files, $preferences.files.enable) + featureSection(.files, $preferences.files.enable) if groupInfo.canEdit { Section { diff --git a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift index 3e42d2f20..7c973c73c 100644 --- a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift +++ b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift @@ -61,7 +61,7 @@ struct ContactConnectionInfo: View { if contactConnection.initiated, let connReqInv = contactConnection.connReqInv { - QRCode(uri: connReqInv) + SimpleXLinkQRCode(uri: simplexChatLink(connReqInv)) incognitoEnabled() shareLinkButton(connReqInv) oneTimeLinkLearnMoreButton() diff --git a/apps/ios/Shared/Views/NewChat/AddContactView.swift b/apps/ios/Shared/Views/NewChat/AddContactView.swift index 31b6b64f3..344a8d1f9 100644 --- a/apps/ios/Shared/Views/NewChat/AddContactView.swift +++ b/apps/ios/Shared/Views/NewChat/AddContactView.swift @@ -21,7 +21,7 @@ struct AddContactView: View { List { Section { if connReqInvitation != "" { - QRCode(uri: connReqInvitation) + SimpleXLinkQRCode(uri: connReqInvitation) } else { ProgressView() .progressViewStyle(.circular) @@ -99,7 +99,7 @@ func sharedProfileInfo(_ incognito: Bool) -> Text { func shareLinkButton(_ connReqInvitation: String) -> some View { Button { - showShareSheet(items: [connReqInvitation]) + showShareSheet(items: [simplexChatLink(connReqInvitation)]) } label: { settingsRow("square.and.arrow.up") { Text("Share 1-time link") diff --git a/apps/ios/Shared/Views/NewChat/NewChatButton.swift b/apps/ios/Shared/Views/NewChat/NewChatButton.swift index e4fbd8bd4..8d095e907 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatButton.swift @@ -62,7 +62,9 @@ enum PlanAndConnectAlert: Identifiable { case ownInvitationLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool) case invitationLinkConnecting(connectionLink: String) case ownContactAddressConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool) + case contactAddressConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool) case groupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool) + case groupLinkConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool) case groupLinkConnecting(connectionLink: String, groupInfo: GroupInfo?) var id: String { @@ -70,7 +72,9 @@ enum PlanAndConnectAlert: Identifiable { case let .ownInvitationLinkConfirmConnect(connectionLink, _, _): return "ownInvitationLinkConfirmConnect \(connectionLink)" case let .invitationLinkConnecting(connectionLink): return "invitationLinkConnecting \(connectionLink)" case let .ownContactAddressConfirmConnect(connectionLink, _, _): return "ownContactAddressConfirmConnect \(connectionLink)" + case let .contactAddressConnectingConfirmReconnect(connectionLink, _, _): return "contactAddressConnectingConfirmReconnect \(connectionLink)" case let .groupLinkConfirmConnect(connectionLink, _, _): return "groupLinkConfirmConnect \(connectionLink)" + case let .groupLinkConnectingConfirmReconnect(connectionLink, _, _): return "groupLinkConnectingConfirmReconnect \(connectionLink)" case let .groupLinkConnecting(connectionLink, _): return "groupLinkConnecting \(connectionLink)" } } @@ -103,6 +107,16 @@ func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool) -> Alert { ), secondaryButton: .cancel() ) + case let .contactAddressConnectingConfirmReconnect(connectionLink, connectionPlan, incognito): + return Alert( + title: Text("Repeat connection request?"), + message: Text("You have already requested connection via this address!"), + primaryButton: .destructive( + Text(incognito ? "Connect incognito" : "Connect"), + action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) } + ), + secondaryButton: .cancel() + ) case let .groupLinkConfirmConnect(connectionLink, connectionPlan, incognito): return Alert( title: Text("Join group?"), @@ -113,6 +127,16 @@ func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool) -> Alert { ), secondaryButton: .cancel() ) + case let .groupLinkConnectingConfirmReconnect(connectionLink, connectionPlan, incognito): + return Alert( + title: Text("Repeat join request?"), + message: Text("You are already joining the group via this link!"), + primaryButton: .destructive( + Text(incognito ? "Join incognito" : "Join"), + action: { connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito) } + ), + secondaryButton: .cancel() + ) case let .groupLinkConnecting(_, groupInfo): if let groupInfo = groupInfo { return Alert( @@ -130,13 +154,13 @@ func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool) -> Alert { enum PlanAndConnectActionSheet: Identifiable { case askCurrentOrIncognitoProfile(connectionLink: String, connectionPlan: ConnectionPlan?, title: LocalizedStringKey) - case ownLinkAskCurrentOrIncognitoProfile(connectionLink: String, connectionPlan: ConnectionPlan, title: LocalizedStringKey) + case askCurrentOrIncognitoProfileDestructive(connectionLink: String, connectionPlan: ConnectionPlan, title: LocalizedStringKey) case ownGroupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool?, groupInfo: GroupInfo) var id: String { switch self { case let .askCurrentOrIncognitoProfile(connectionLink, _, _): return "askCurrentOrIncognitoProfile \(connectionLink)" - case let .ownLinkAskCurrentOrIncognitoProfile(connectionLink, _, _): return "ownLinkAskCurrentOrIncognitoProfile \(connectionLink)" + case let .askCurrentOrIncognitoProfileDestructive(connectionLink, _, _): return "askCurrentOrIncognitoProfileDestructive \(connectionLink)" case let .ownGroupLinkConfirmConnect(connectionLink, _, _, _): return "ownGroupLinkConfirmConnect \(connectionLink)" } } @@ -153,7 +177,7 @@ func planAndConnectActionSheet(_ sheet: PlanAndConnectActionSheet, dismiss: Bool .cancel() ] ) - case let .ownLinkAskCurrentOrIncognitoProfile(connectionLink, connectionPlan, title): + case let .askCurrentOrIncognitoProfileDestructive(connectionLink, connectionPlan, title): return ActionSheet( title: Text(title), buttons: [ @@ -211,7 +235,7 @@ func planAndConnect( if let incognito = incognito { showAlert(.ownInvitationLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) } else { - showActionSheet(.ownLinkAskCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own one-time link!")) + showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own one-time link!")) } case let .connecting(contact_): logger.debug("planAndConnect, .invitationLink, .connecting, incognito=\(incognito?.description ?? "nil")") @@ -238,10 +262,17 @@ func planAndConnect( if let incognito = incognito { showAlert(.ownContactAddressConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) } else { - showActionSheet(.ownLinkAskCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own SimpleX address!")) + showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own SimpleX address!")) } - case let .connecting(contact): - logger.debug("planAndConnect, .contactAddress, .connecting, incognito=\(incognito?.description ?? "nil")") + case .connectingConfirmReconnect: + logger.debug("planAndConnect, .contactAddress, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")") + if let incognito = incognito { + showAlert(.contactAddressConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) + } else { + showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You have already requested connection!\nRepeat connection request?")) + } + case let .connectingProhibit(contact): + logger.debug("planAndConnect, .contactAddress, .connectingProhibit, incognito=\(incognito?.description ?? "nil")") openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) } case let .known(contact): logger.debug("planAndConnect, .contactAddress, .known, incognito=\(incognito?.description ?? "nil")") @@ -258,8 +289,15 @@ func planAndConnect( case let .ownLink(groupInfo): logger.debug("planAndConnect, .groupLink, .ownLink, incognito=\(incognito?.description ?? "nil")") showActionSheet(.ownGroupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito, groupInfo: groupInfo)) - case let .connecting(groupInfo_): - logger.debug("planAndConnect, .groupLink, .connecting, incognito=\(incognito?.description ?? "nil")") + case .connectingConfirmReconnect: + logger.debug("planAndConnect, .groupLink, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")") + if let incognito = incognito { + showAlert(.groupLinkConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) + } else { + showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You are already joining the group!\nRepeat join request?")) + } + case let .connectingProhibit(groupInfo_): + logger.debug("planAndConnect, .groupLink, .connectingProhibit, incognito=\(incognito?.description ?? "nil")") showAlert(.groupLinkConnecting(connectionLink: connectionLink, groupInfo: groupInfo_)) case let .known(groupInfo): logger.debug("planAndConnect, .groupLink, .known, incognito=\(incognito?.description ?? "nil")") diff --git a/apps/ios/Shared/Views/NewChat/QRCode.swift b/apps/ios/Shared/Views/NewChat/QRCode.swift index 0785826fa..4bec3f8e6 100644 --- a/apps/ios/Shared/Views/NewChat/QRCode.swift +++ b/apps/ios/Shared/Views/NewChat/QRCode.swift @@ -28,6 +28,22 @@ struct MutableQRCode: View { } } +struct SimpleXLinkQRCode: View { + let uri: String + var withLogo: Bool = true + var tintColor = UIColor(red: 0.023, green: 0.176, blue: 0.337, alpha: 1) + + var body: some View { + QRCode(uri: simplexChatLink(uri), withLogo: withLogo, tintColor: tintColor) + } +} + +func simplexChatLink(_ uri: String) -> String { + uri.starts(with: "simplex:/") + ? uri.replacingOccurrences(of: "simplex:/", with: "https://simplex.chat/") + : uri +} + struct QRCode: View { let uri: String var withLogo: Bool = true diff --git a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift index 049c4bee0..935f09cc1 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateSimpleXAddress.swift @@ -31,7 +31,7 @@ struct CreateSimpleXAddress: View { Spacer() if let userAddress = m.userAddress { - QRCode(uri: userAddress.connReqContact) + SimpleXLinkQRCode(uri: userAddress.connReqContact) .frame(maxHeight: g.size.width) shareQRCodeButton(userAddress) .frame(maxWidth: .infinity) @@ -126,7 +126,7 @@ struct CreateSimpleXAddress: View { private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View { Button { - showShareSheet(items: [userAddress.connReqContact]) + showShareSheet(items: [simplexChatLink(userAddress.connReqContact)]) } label: { Label("Share", systemImage: "square.and.arrow.up") } @@ -194,7 +194,7 @@ struct SendAddressMailView: View { let messageBody = String(format: NSLocalizedString("""

Hi!

Connect to me via SimpleX Chat

- """, comment: "email text"), userAddress.connReqContact) + """, comment: "email text"), simplexChatLink(userAddress.connReqContact)) MailView( isShowing: self.$showMailView, result: $mailViewResult, diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index 16555fad3..71ff7b88b 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -93,7 +93,9 @@ struct PrivacySettings: View { } settingsRow("link") { Picker("SimpleX links", selection: $simplexLinkMode) { - ForEach(SimpleXLinkMode.values) { mode in + ForEach( + SimpleXLinkMode.values + (SimpleXLinkMode.values.contains(simplexLinkMode) ? [] : [simplexLinkMode]) + ) { mode in Text(mode.text) } } @@ -104,10 +106,6 @@ struct PrivacySettings: View { } } header: { Text("Chats") - } footer: { - if case .browser = simplexLinkMode { - Text("Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red.") - } } Section { diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index f076a4eb7..1cc859f49 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -93,7 +93,7 @@ enum SimpleXLinkMode: String, Identifiable { case full case browser - static var values: [SimpleXLinkMode] = [.description, .full, .browser] + static var values: [SimpleXLinkMode] = [.description, .full] public var id: Self { self } diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index 86bf0048b..60e5cc7b0 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -190,7 +190,7 @@ struct UserAddressView: View { @ViewBuilder private func existingAddressView(_ userAddress: UserContactLink) -> some View { Section { - MutableQRCode(uri: Binding.constant(userAddress.connReqContact)) + MutableQRCode(uri: Binding.constant(simplexChatLink(userAddress.connReqContact))) shareQRCodeButton(userAddress) if MFMailComposeViewController.canSendMail() { shareViaEmailButton(userAddress) @@ -248,7 +248,7 @@ struct UserAddressView: View { private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View { Button { - showShareSheet(items: [userAddress.connReqContact]) + showShareSheet(items: [simplexChatLink(userAddress.connReqContact)]) } label: { settingsRow("square.and.arrow.up") { Text("Share address") diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index ddab4c73b..53da91b04 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -886,14 +886,16 @@ public enum InvitationLinkPlan: Decodable { public enum ContactAddressPlan: Decodable { case ok case ownLink - case connecting(contact: Contact) + case connectingConfirmReconnect + case connectingProhibit(contact: Contact) case known(contact: Contact) } public enum GroupLinkPlan: Decodable { case ok case ownLink(groupInfo: GroupInfo) - case connecting(groupInfo_: GroupInfo?) + case connectingConfirmReconnect + case connectingProhibit(groupInfo_: GroupInfo?) case known(groupInfo: GroupInfo) } diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index caaccb545..c31786c70 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -3068,7 +3068,7 @@ public enum Format: Decodable, Equatable { case secret case colored(color: FormatColor) case uri - case simplexLink(linkType: SimplexLinkType, simplexUri: String, trustedUri: Bool, smpHosts: [String]) + case simplexLink(linkType: SimplexLinkType, simplexUri: String, smpHosts: [String]) case email case phone } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index a4b76cc79..ad033387a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -53,6 +53,7 @@ object ChatModel { // current chat val chatId = mutableStateOf(null) val chatItems = mutableStateListOf() + val chatItemStatuses = mutableMapOf() val groupMembers = mutableStateListOf() val terminalItems = mutableStateListOf() @@ -135,6 +136,7 @@ object ChatModel { fun hasChat(id: String): Boolean = chats.toList().firstOrNull { it.id == id } != null fun getChat(id: String): Chat? = chats.toList().firstOrNull { it.id == id } fun getContactChat(contactId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId } + fun getGroupChat(groupId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Group && it.chatInfo.apiId == groupId } private fun getChatIndex(id: String): Int = chats.toList().indexOfFirst { it.id == id } fun addChat(chat: Chat) = chats.add(index = 0, chat) @@ -272,7 +274,13 @@ object ChatModel { Log.d(TAG, "TODOCHAT: upsertChatItem: updated in chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}") false } else { - chatItems.add(cItem) + val status = chatItemStatuses.remove(cItem.id) + val ci = if (status != null && cItem.meta.itemStatus is CIStatus.SndNew) { + cItem.copy(meta = cItem.meta.copy(itemStatus = status)) + } else { + cItem + } + chatItems.add(ci) Log.d(TAG, "TODOCHAT: upsertChatItem: added to chat $chatId from ${cInfo.id} ${cItem.id}, size ${chatItems.size}") true } @@ -282,13 +290,15 @@ object ChatModel { } } - suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem) { + suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) { withContext(Dispatchers.Main) { if (chatId.value == cInfo.id) { val itemIndex = chatItems.indexOfFirst { it.id == cItem.id } if (itemIndex >= 0) { chatItems[itemIndex] = cItem } + } else if (status != null) { + chatItemStatuses[cItem.id] = status } } } @@ -326,6 +336,7 @@ object ChatModel { } // clear current chat if (chatId.value == cInfo.id) { + chatItemStatuses.clear() chatItems.clear() } } @@ -2416,7 +2427,7 @@ sealed class Format { @Serializable @SerialName("secret") class Secret: Format() @Serializable @SerialName("colored") class Colored(val color: FormatColor): Format() @Serializable @SerialName("uri") class Uri: Format() - @Serializable @SerialName("simplexLink") class SimplexLink(val linkType: SimplexLinkType, val simplexUri: String, val trustedUri: Boolean, val smpHosts: List): Format() + @Serializable @SerialName("simplexLink") class SimplexLink(val linkType: SimplexLinkType, val simplexUri: String, val smpHosts: List): Format() @Serializable @SerialName("email") class Email: Format() @Serializable @SerialName("phone") class Phone: Format() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index cc8d481b5..8397d2edb 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -855,6 +855,14 @@ object ChatController { return null } + suspend fun apiConnectPlan(connReq: String): ConnectionPlan? { + val userId = kotlin.runCatching { currentUserId("apiConnectPlan") }.getOrElse { return null } + val r = sendCmd(CC.APIConnectPlan(userId, connReq)) + if (r is CR.CRConnectionPlan) return r.connectionPlan + Log.e(TAG, "apiConnectPlan bad response: ${r.responseType} ${r.details}") + return null + } + suspend fun apiConnect(incognito: Boolean, connReq: String): Boolean { val userId = chatModel.currentUser.value?.userId ?: run { Log.e(TAG, "apiConnect: no current user") @@ -1489,11 +1497,8 @@ object ChatController { is CR.ChatItemStatusUpdated -> { val cInfo = r.chatItem.chatInfo val cItem = r.chatItem.chatItem - if (!cItem.isDeletedContent) { - val added = if (active(r.user)) chatModel.upsertChatItem(cInfo, cItem) else true - if (added && cItem.showNotification) { - ntfManager.notifyMessageReceived(r.user, cInfo, cItem) - } + if (!cItem.isDeletedContent && active(r.user)) { + chatModel.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus) } } is CR.ChatItemUpdated -> @@ -1917,6 +1922,7 @@ sealed class CC { class APIVerifyGroupMember(val groupId: Long, val groupMemberId: Long, val connectionCode: String?): CC() class APIAddContact(val userId: Long, val incognito: Boolean): CC() class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC() + class APIConnectPlan(val userId: Long, val connReq: String): CC() class APIConnect(val userId: Long, val incognito: Boolean, val connReq: String): CC() class ApiDeleteChat(val type: ChatType, val id: Long): CC() class ApiClearChat(val type: ChatType, val id: Long): CC() @@ -2026,6 +2032,7 @@ sealed class CC { is APIVerifyGroupMember -> "/_verify code #$groupId $groupMemberId" + if (connectionCode != null) " $connectionCode" else "" is APIAddContact -> "/_connect $userId incognito=${onOff(incognito)}" is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}" + is APIConnectPlan -> "/_connect plan $userId $connReq" is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq" is ApiDeleteChat -> "/_delete ${chatRef(type, id)}" is ApiClearChat -> "/_clear chat ${chatRef(type, id)}" @@ -2127,6 +2134,7 @@ sealed class CC { is APIVerifyGroupMember -> "apiVerifyGroupMember" is APIAddContact -> "apiAddContact" is ApiSetConnectionIncognito -> "apiSetConnectionIncognito" + is APIConnectPlan -> "apiConnectPlan" is APIConnect -> "apiConnect" is ApiDeleteChat -> "apiDeleteChat" is ApiClearChat -> "apiClearChat" @@ -3340,6 +3348,7 @@ sealed class CR { @Serializable @SerialName("connectionVerified") class ConnectionVerified(val user: UserRef, val verified: Boolean, val expectedCode: String): CR() @Serializable @SerialName("invitation") class Invitation(val user: UserRef, val connReqInvitation: String, val connection: PendingContactConnection): CR() @Serializable @SerialName("connectionIncognitoUpdated") class ConnectionIncognitoUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR() + @Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connectionPlan: ConnectionPlan): CR() @Serializable @SerialName("sentConfirmation") class SentConfirmation(val user: UserRef): CR() @Serializable @SerialName("sentInvitation") class SentInvitation(val user: UserRef): CR() @Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val user: UserRef, val contact: Contact): CR() @@ -3475,6 +3484,7 @@ sealed class CR { is ConnectionVerified -> "connectionVerified" is Invitation -> "invitation" is ConnectionIncognitoUpdated -> "connectionIncognitoUpdated" + is CRConnectionPlan -> "connectionPlan" is SentConfirmation -> "sentConfirmation" is SentInvitation -> "sentInvitation" is ContactAlreadyExists -> "contactAlreadyExists" @@ -3605,6 +3615,7 @@ sealed class CR { is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode") is Invitation -> withUser(user, connReqInvitation) is ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection)) + is CRConnectionPlan -> withUser(user, json.encodeToString(connectionPlan)) is SentConfirmation -> withUser(user, noDetails()) is SentInvitation -> withUser(user, noDetails()) is ContactAlreadyExists -> withUser(user, json.encodeToString(contact)) @@ -3718,6 +3729,39 @@ fun chatError(r: CR): ChatErrorType? { ) } +@Serializable +sealed class ConnectionPlan { + @Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan() + @Serializable @SerialName("contactAddress") class ContactAddress(val contactAddressPlan: ContactAddressPlan): ConnectionPlan() + @Serializable @SerialName("groupLink") class GroupLink(val groupLinkPlan: GroupLinkPlan): ConnectionPlan() +} + +@Serializable +sealed class InvitationLinkPlan { + @Serializable @SerialName("ok") object Ok: InvitationLinkPlan() + @Serializable @SerialName("ownLink") object OwnLink: InvitationLinkPlan() + @Serializable @SerialName("connecting") class Connecting(val contact_: Contact? = null): InvitationLinkPlan() + @Serializable @SerialName("known") class Known(val contact: Contact): InvitationLinkPlan() +} + +@Serializable +sealed class ContactAddressPlan { + @Serializable @SerialName("ok") object Ok: ContactAddressPlan() + @Serializable @SerialName("ownLink") object OwnLink: ContactAddressPlan() + @Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: ContactAddressPlan() + @Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val contact: Contact): ContactAddressPlan() + @Serializable @SerialName("known") class Known(val contact: Contact): ContactAddressPlan() +} + +@Serializable +sealed class GroupLinkPlan { + @Serializable @SerialName("ok") object Ok: GroupLinkPlan() + @Serializable @SerialName("ownLink") class OwnLink(val groupInfo: GroupInfo): GroupLinkPlan() + @Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: GroupLinkPlan() + @Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val groupInfo_: GroupInfo? = null): GroupLinkPlan() + @Serializable @SerialName("known") class Known(val groupInfo: GroupInfo): GroupLinkPlan() +} + abstract class TerminalItem { abstract val id: Long val date: Instant = Clock.System.now() @@ -3877,6 +3921,7 @@ sealed class ChatErrorType { is ChatNotStarted -> "chatNotStarted" is ChatNotStopped -> "chatNotStopped" is ChatStoreChanged -> "chatStoreChanged" + is ConnectionPlanChatError -> "connectionPlan" is InvalidConnReq -> "invalidConnReq" is InvalidChatMessage -> "invalidChatMessage" is ContactNotReady -> "contactNotReady" @@ -3953,6 +3998,7 @@ sealed class ChatErrorType { @Serializable @SerialName("chatNotStarted") object ChatNotStarted: ChatErrorType() @Serializable @SerialName("chatNotStopped") object ChatNotStopped: ChatErrorType() @Serializable @SerialName("chatStoreChanged") object ChatStoreChanged: ChatErrorType() + @Serializable @SerialName("connectionPlan") class ConnectionPlanChatError(val connectionPlan: ConnectionPlan): ChatErrorType() @Serializable @SerialName("invalidConnReq") object InvalidConnReq: ChatErrorType() @Serializable @SerialName("invalidChatMessage") class InvalidChatMessage(val connection: Connection, val message: String): ChatErrorType() @Serializable @SerialName("contactNotReady") class ContactNotReady(val contact: Contact): ChatErrorType() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index 1ba742b03..01173157a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -31,10 +31,10 @@ import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.newchat.QRCode import chat.simplex.common.views.usersettings.* import chat.simplex.common.platform.* import chat.simplex.common.views.chatlist.updateChatSettings +import chat.simplex.common.views.newchat.* import chat.simplex.res.MR import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* @@ -309,9 +309,9 @@ fun ChatInfoLayout( if (contact.contactLink != null) { SectionView(stringResource(MR.strings.address_section_title).uppercase()) { - QRCode(contact.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) + SimpleXLinkQRCode(contact.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) val clipboard = LocalClipboardManager.current - ShareAddressButton { clipboard.shareText(contact.contactLink) } + ShareAddressButton { clipboard.shareText(simplexChatLink(contact.contactLink)) } SectionTextFooter(stringResource(MR.strings.you_can_share_this_address_with_your_contacts).format(contact.displayName)) } SectionDividerSpaced() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt index 7c767f9b7..7e1c03130 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt @@ -19,7 +19,7 @@ import chat.simplex.common.model.* import chat.simplex.common.platform.shareText import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.newchat.QRCode +import chat.simplex.common.views.newchat.* import chat.simplex.res.MR @Composable @@ -44,14 +44,12 @@ fun GroupLinkView(chatModel: ChatModel, groupInfo: GroupInfo, connReqContact: St createLink() } } - val clipboard = LocalClipboardManager.current GroupLinkLayout( groupLink = groupLink, groupInfo, groupLinkMemberRole, creatingLink, createLink = ::createLink, - share = { clipboard.shareText(groupLink ?: return@GroupLinkLayout) }, updateLink = { val role = groupLinkMemberRole.value if (role != null) { @@ -95,7 +93,6 @@ fun GroupLinkLayout( groupLinkMemberRole: MutableState, creatingLink: Boolean, createLink: () -> Unit, - share: () -> Unit, updateLink: () -> Unit, deleteLink: () -> Unit ) { @@ -125,16 +122,17 @@ fun GroupLinkLayout( } initialLaunch = false } - QRCode(groupLink, Modifier.aspectRatio(1f).padding(horizontal = DEFAULT_PADDING)) + SimpleXLinkQRCode(groupLink, Modifier.aspectRatio(1f).padding(horizontal = DEFAULT_PADDING)) Row( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = DEFAULT_PADDING, vertical = 10.dp) ) { + val clipboard = LocalClipboardManager.current SimpleButton( stringResource(MR.strings.share_link), icon = painterResource(MR.images.ic_share), - click = share + click = { clipboard.shareText(simplexChatLink(groupLink)) } ) SimpleButton( stringResource(MR.strings.delete_link), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index e14089ec5..e281c5762 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -73,6 +73,7 @@ fun GroupMemberInfoView( chatModel.addChat(c) } chatModel.chatItems.clear() + chatModel.chatItemStatuses.clear() chatModel.chatItems.addAll(c.chatItems) chatModel.chatId.value = c.id closeAll() @@ -283,9 +284,9 @@ fun GroupMemberInfoLayout( if (member.contactLink != null) { SectionView(stringResource(MR.strings.address_section_title).uppercase()) { - QRCode(member.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) + SimpleXLinkQRCode(member.contactLink, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) val clipboard = LocalClipboardManager.current - ShareAddressButton { clipboard.shareText(member.contactLink) } + ShareAddressButton { clipboard.shareText(simplexChatLink(member.contactLink)) } if (contactId != null) { if (knownDirectChat(contactId) == null && !groupInfo.fullGroupPreferences.directMessages.on) { ConnectViaAddressButton(onClick = { connectViaAddress(member.contactLink) }) @@ -472,43 +473,17 @@ private fun updateMemberRoleDialog( } fun connectViaMemberAddressAlert(connReqUri: String) { - AlertManager.shared.showAlertDialogButtonsColumn( - title = generalGetString(MR.strings.connect_via_member_address_alert_title), - text = AnnotatedString(generalGetString(MR.strings.connect_via_member_address_alert_desc)), - buttons = { - Column { - SectionItemView({ - AlertManager.shared.hideAlert() - val uri = URI(connReqUri) - withUriAction(uri) { linkType -> - withApi { - Log.d(TAG, "connectViaUri: connecting") - connectViaUri(chatModel, linkType, uri, incognito = false) - } - } - }) { - Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - SectionItemView({ - AlertManager.shared.hideAlert() - val uri = URI(connReqUri) - withUriAction(uri) { linkType -> - withApi { - Log.d(TAG, "connectViaUri: connecting incognito") - connectViaUri(chatModel, linkType, uri, incognito = true) - } - } - }) { - Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - SectionItemView({ - AlertManager.shared.hideAlert() - }) { - Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - } + try { + val uri = URI(connReqUri) + withApi { + planAndConnect(chatModel, uri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() }) } - ) + } catch (e: RuntimeException) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.invalid_connection_link), + text = generalGetString(MR.strings.this_string_is_not_a_connection_link) + ) + } } @Preview diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt index d95d2a4cf..4571a38c1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt @@ -103,12 +103,11 @@ private fun GroupPreferencesLayout( FeatureSection(GroupFeature.Voice, allowVoice, groupInfo, preferences, onTTLUpdated) { applyPrefs(preferences.copy(voice = GroupPreference(enable = it))) } -// TODO uncomment in 5.3 -// SectionDividerSpaced(true, maxBottomPadding = false) -// val allowFiles = remember(preferences) { mutableStateOf(preferences.files.enable) } -// FeatureSection(GroupFeature.Files, allowFiles, groupInfo, preferences, onTTLUpdated) { -// applyPrefs(preferences.copy(files = GroupPreference(enable = it))) -// } + SectionDividerSpaced(true, maxBottomPadding = false) + val allowFiles = remember(preferences) { mutableStateOf(preferences.files.enable) } + FeatureSection(GroupFeature.Files, allowFiles, groupInfo, preferences, onTTLUpdated) { + applyPrefs(preferences.copy(files = GroupPreference(enable = it))) + } if (groupInfo.canEdit) { SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) ResetSaveButtons( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt index eabab138b..ff1267d0f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt @@ -136,12 +136,8 @@ fun MarkdownText ( val link = ft.link(linkMode) if (link != null) { hasLinks = true - val ftStyle = if (ft.format is Format.SimplexLink && !ft.format.trustedUri && linkMode == SimplexLinkMode.BROWSER) { - SpanStyle(color = Color.Red, textDecoration = TextDecoration.Underline) - } else { - ft.format.style - } - withAnnotation(tag = if (ft.format is Format.SimplexLink && linkMode != SimplexLinkMode.BROWSER) "SIMPLEX_URL" else "URL", annotation = link) { + val ftStyle = ft.format.style + withAnnotation(tag = if (ft.format is Format.SimplexLink) "SIMPLEX_URL" else "URL", annotation = link) { withStyle(ftStyle) { append(ft.viewText(linkMode)) } } } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 566c98181..b16bf4c9b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -130,6 +130,13 @@ suspend fun openDirectChat(contactId: Long, chatModel: ChatModel) { } } +suspend fun openGroupChat(groupId: Long, chatModel: ChatModel) { + val chat = chatModel.controller.apiGetChat(ChatType.Group, groupId) + if (chat != null) { + openChat(chat, chatModel) + } +} + suspend fun openChat(chatInfo: ChatInfo, chatModel: ChatModel) { Log.d(TAG, "TODOCHAT: openChat: opening ${chatInfo.id}, current chatId ${ChatModel.chatId.value}, size ${ChatModel.chatItems.size}") val chat = chatModel.controller.apiGetChat(chatInfo.chatType, chatInfo.apiId) @@ -141,6 +148,7 @@ suspend fun openChat(chatInfo: ChatInfo, chatModel: ChatModel) { suspend fun openChat(chat: Chat, chatModel: ChatModel) { chatModel.chatItems.clear() + chatModel.chatItemStatuses.clear() chatModel.chatItems.addAll(chat.chatItems) chatModel.chatId.value = chat.chatInfo.id } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 6d7450a21..66ef1cf9f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -316,46 +316,8 @@ fun connectIfOpenedViaUri(uri: URI, chatModel: ChatModel) { if (chatModel.currentUser.value == null) { chatModel.appOpenUrl.value = uri } else { - withUriAction(uri) { linkType -> - val title = when (linkType) { - ConnectionLinkType.CONTACT -> generalGetString(MR.strings.connect_via_contact_link) - ConnectionLinkType.INVITATION -> generalGetString(MR.strings.connect_via_invitation_link) - ConnectionLinkType.GROUP -> generalGetString(MR.strings.connect_via_group_link) - } - AlertManager.shared.showAlertDialogButtonsColumn( - title = title, - text = if (linkType == ConnectionLinkType.GROUP) - AnnotatedString(generalGetString(MR.strings.you_will_join_group)) - else - AnnotatedString(generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link)), - buttons = { - Column { - SectionItemView({ - AlertManager.shared.hideAlert() - withApi { - Log.d(TAG, "connectIfOpenedViaUri: connecting") - connectViaUri(chatModel, linkType, uri, incognito = false) - } - }) { - Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - SectionItemView({ - AlertManager.shared.hideAlert() - withApi { - Log.d(TAG, "connectIfOpenedViaUri: connecting incognito") - connectViaUri(chatModel, linkType, uri, incognito = true) - } - }) { - Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - SectionItemView({ - AlertManager.shared.hideAlert() - }) { - Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) - } - } - } - ) + withApi { + planAndConnect(chatModel, uri, incognito = null, close = null) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactView.kt index 8542ea52a..ef3633d1f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddContactView.kt @@ -34,7 +34,6 @@ fun AddContactView( incognitoPref = chatModel.controller.appPrefs.incognito, connReq = connReqInvitation, contactConnection = contactConnection, - share = { clipboard.shareText(connReqInvitation) }, learnMore = { ModalManager.center.showModal { Column( @@ -56,7 +55,6 @@ fun AddContactLayout( incognitoPref: SharedPreference, connReq: String, contactConnection: MutableState, - share: () -> Unit, learnMore: () -> Unit ) { val incognito = remember { mutableStateOf(incognitoPref.get()) } @@ -82,7 +80,7 @@ fun AddContactLayout( SectionView(stringResource(MR.strings.one_time_link_short).uppercase()) { if (connReq.isNotEmpty()) { - QRCode( + SimpleXLinkQRCode( connReq, Modifier .padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF) .aspectRatio(1f) @@ -99,7 +97,7 @@ fun AddContactLayout( } IncognitoToggle(incognitoPref, incognito) { ModalManager.start.showModal { IncognitoView() } } - ShareLinkButton(share) + ShareLinkButton(connReq) OneTimeLinkLearnMoreButton(learnMore) } SectionTextFooter(sharedProfileInfo(chatModel, incognito.value)) @@ -109,11 +107,12 @@ fun AddContactLayout( } @Composable -fun ShareLinkButton(onClick: () -> Unit) { +fun ShareLinkButton(connReqInvitation: String) { + val clipboard = LocalClipboardManager.current SettingsActionItem( painterResource(MR.images.ic_share), stringResource(MR.strings.share_invitation_link), - onClick, + click = { clipboard.shareText(simplexChatLink(connReqInvitation)) }, iconColor = MaterialTheme.colors.primary, textColor = MaterialTheme.colors.primary, ) @@ -177,7 +176,6 @@ fun PreviewAddContactView() { incognitoPref = SharedPreference({ false }, {}), connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", contactConnection = mutableStateOf(PendingContactConnection.getSampleData()), - share = {}, learnMore = {}, ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt index 6ad919c27..be446f608 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt @@ -41,6 +41,7 @@ fun AddGroupView(chatModel: ChatModel, close: () -> Unit) { if (groupInfo != null) { chatModel.addChat(Chat(chatInfo = ChatInfo.Group(groupInfo), chatItems = listOf())) chatModel.chatItems.clear() + chatModel.chatItemStatuses.clear() chatModel.chatId.value = groupInfo.id setGroupMembers(groupInfo, chatModel) close.invoke() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt index 934c050d8..d04a85d90 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt @@ -131,13 +131,13 @@ private fun ContactConnectionInfoLayout( SectionView { if (!connReq.isNullOrEmpty() && contactConnection.initiated) { - QRCode( + SimpleXLinkQRCode( connReq, Modifier .padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF) .aspectRatio(1f) ) incognitoEnabled() - ShareLinkButton(share) + ShareLinkButton(connReq) OneTimeLinkLearnMoreButton(learnMore) } else { incognitoEnabled() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt index 0c13bd4f6..f7a5a1e86 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/PasteToConnect.kt @@ -53,32 +53,14 @@ fun PasteToConnectLayout( fun connectViaLink(connReqUri: String) { try { val uri = URI(connReqUri) - withUriAction(uri) { linkType -> - val action = suspend { - Log.d(TAG, "connectViaUri: connecting") - if (connectViaUri(chatModel, linkType, uri, incognito = incognito.value)) { - close() - } - } - if (linkType == ConnectionLinkType.GROUP) { - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.connect_via_group_link), - text = generalGetString(MR.strings.you_will_join_group), - confirmText = if (incognito.value) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), - onConfirm = { withApi { action() } } - ) - } else action() + withApi { + planAndConnect(chatModel, uri, incognito = incognito.value, close) } } catch (e: RuntimeException) { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.invalid_connection_link), text = generalGetString(MR.strings.this_string_is_not_a_connection_link) ) - } catch (e: URISyntaxException) { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.invalid_connection_link), - text = generalGetString(MR.strings.this_string_is_not_a_connection_link) - ) } } @@ -115,6 +97,7 @@ fun PasteToConnectLayout( painterResource(MR.images.ic_link), stringResource(MR.strings.connect_button), click = { connectViaLink(connectionLink.value) }, + disabled = connectionLink.value.isEmpty() || connectionLink.value.trim().contains(" ") ) IncognitoToggle(incognitoPref, incognito) { ModalManager.start.showModal { IncognitoView() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt index 663292596..763addae6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/QRCode.kt @@ -19,6 +19,29 @@ import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import kotlinx.coroutines.launch +@Composable +fun SimpleXLinkQRCode( + connReq: String, + modifier: Modifier = Modifier, + tintColor: Color = Color(0xff062d56), + withLogo: Boolean = true +) { + QRCode( + simplexChatLink(connReq), + modifier, + tintColor, + withLogo + ) +} + +fun simplexChatLink(uri: String): String { + return if (uri.startsWith("simplex:/")) { + uri.replace("simplex:/", "https://simplex.chat/") + } else { + uri + } +} + @Composable fun QRCode( connReq: String, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt index e3fa92275..2f52c2cac 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ScanToConnectView.kt @@ -1,6 +1,7 @@ package chat.simplex.common.views.newchat import SectionBottomSpacer +import SectionItemView import SectionTextFooter import androidx.compose.desktop.ui.tooling.preview.Preview import chat.simplex.common.platform.Log @@ -10,66 +11,265 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import chat.simplex.common.model.* import chat.simplex.common.platform.TAG import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chatlist.openDirectChat +import chat.simplex.common.views.chatlist.openGroupChat import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable import java.net.URI @Composable expect fun ScanToConnectView(chatModel: ChatModel, close: () -> Unit) enum class ConnectionLinkType { - CONTACT, INVITATION, GROUP + INVITATION, CONTACT, GROUP } -@Serializable -sealed class CReqClientData { - @Serializable @SerialName("group") data class Group(val groupLinkId: String): CReqClientData() -} - -fun withUriAction(uri: URI, run: suspend (ConnectionLinkType) -> Unit) { - val action = uri.path?.drop(1)?.replace("/", "") - val data = URI(uri.toString().replaceFirst("#/", "/")).getQueryParameter("data") - val type = when { - data != null -> { - val parsed = runCatching { - json.decodeFromString(CReqClientData.serializer(), data) +suspend fun planAndConnect( + chatModel: ChatModel, + uri: URI, + incognito: Boolean?, + close: (() -> Unit)? +) { + val connectionPlan = chatModel.controller.apiConnectPlan(uri.toString()) + if (connectionPlan != null) { + when (connectionPlan) { + is ConnectionPlan.InvitationLink -> when (connectionPlan.invitationLinkPlan) { + InvitationLinkPlan.Ok -> { + Log.d(TAG, "planAndConnect, .InvitationLink, .Ok, incognito=$incognito") + if (incognito != null) { + connectViaUri(chatModel, uri, incognito, connectionPlan, close) + } else { + askCurrentOrIncognitoProfileAlert( + chatModel, uri, connectionPlan, close, + title = generalGetString(MR.strings.connect_via_invitation_link), + text = AnnotatedString(generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link)), + connectDestructive = false + ) + } + } + InvitationLinkPlan.OwnLink -> { + Log.d(TAG, "planAndConnect, .InvitationLink, .OwnLink, incognito=$incognito") + if (incognito != null) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.connect_plan_connect_to_yourself), + text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link), + confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), + onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } }, + destructive = true, + ) + } else { + askCurrentOrIncognitoProfileAlert( + chatModel, uri, connectionPlan, close, + title = generalGetString(MR.strings.connect_plan_connect_to_yourself), + text = AnnotatedString(generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link)), + connectDestructive = true + ) + } + } + is InvitationLinkPlan.Connecting -> { + Log.d(TAG, "planAndConnect, .InvitationLink, .Connecting, incognito=$incognito") + val contact = connectionPlan.invitationLinkPlan.contact_ + if (contact != null) { + openKnownContact(chatModel, close, contact) + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.contact_already_exists), + String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName) + ) + } else { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.connect_plan_already_connecting), + generalGetString(MR.strings.connect_plan_you_are_already_connecting_via_this_one_time_link) + ) + } + } + is InvitationLinkPlan.Known -> { + Log.d(TAG, "planAndConnect, .InvitationLink, .Known, incognito=$incognito") + val contact = connectionPlan.invitationLinkPlan.contact + openKnownContact(chatModel, close, contact) + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.contact_already_exists), + String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName) + ) + } } - when { - parsed.getOrNull() is CReqClientData.Group -> ConnectionLinkType.GROUP - else -> null + is ConnectionPlan.ContactAddress -> when (connectionPlan.contactAddressPlan) { + ContactAddressPlan.Ok -> { + Log.d(TAG, "planAndConnect, .ContactAddress, .Ok, incognito=$incognito") + if (incognito != null) { + connectViaUri(chatModel, uri, incognito, connectionPlan, close) + } else { + askCurrentOrIncognitoProfileAlert( + chatModel, uri, connectionPlan, close, + title = generalGetString(MR.strings.connect_via_contact_link), + text = AnnotatedString(generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link)), + connectDestructive = false + ) + } + } + ContactAddressPlan.OwnLink -> { + Log.d(TAG, "planAndConnect, .ContactAddress, .OwnLink, incognito=$incognito") + if (incognito != null) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.connect_plan_connect_to_yourself), + text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address), + confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), + onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } }, + destructive = true, + ) + } else { + askCurrentOrIncognitoProfileAlert( + chatModel, uri, connectionPlan, close, + title = generalGetString(MR.strings.connect_plan_connect_to_yourself), + text = AnnotatedString(generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address)), + connectDestructive = true + ) + } + } + ContactAddressPlan.ConnectingConfirmReconnect -> { + Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingConfirmReconnect, incognito=$incognito") + if (incognito != null) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.connect_plan_repeat_connection_request), + text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address), + confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), + onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } }, + destructive = true, + ) + } else { + askCurrentOrIncognitoProfileAlert( + chatModel, uri, connectionPlan, close, + title = generalGetString(MR.strings.connect_plan_repeat_connection_request), + text = AnnotatedString(generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address)), + connectDestructive = true + ) + } + } + is ContactAddressPlan.ConnectingProhibit -> { + Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingProhibit, incognito=$incognito") + val contact = connectionPlan.contactAddressPlan.contact + openKnownContact(chatModel, close, contact) + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.contact_already_exists), + String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName) + ) + } + is ContactAddressPlan.Known -> { + Log.d(TAG, "planAndConnect, .ContactAddress, .Known, incognito=$incognito") + val contact = connectionPlan.contactAddressPlan.contact + openKnownContact(chatModel, close, contact) + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.contact_already_exists), + String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName) + ) + } + } + is ConnectionPlan.GroupLink -> when (connectionPlan.groupLinkPlan) { + GroupLinkPlan.Ok -> { + Log.d(TAG, "planAndConnect, .GroupLink, .Ok, incognito=$incognito") + if (incognito != null) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.connect_via_group_link), + text = generalGetString(MR.strings.you_will_join_group), + confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), + onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } } + ) + } else { + askCurrentOrIncognitoProfileAlert( + chatModel, uri, connectionPlan, close, + title = generalGetString(MR.strings.connect_via_group_link), + text = AnnotatedString(generalGetString(MR.strings.you_will_join_group)), + connectDestructive = false + ) + } + } + is GroupLinkPlan.OwnLink -> { + Log.d(TAG, "planAndConnect, .GroupLink, .OwnLink, incognito=$incognito") + val groupInfo = connectionPlan.groupLinkPlan.groupInfo + ownGroupLinkConfirmConnect(chatModel, uri, incognito, connectionPlan, groupInfo, close) + } + GroupLinkPlan.ConnectingConfirmReconnect -> { + Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingConfirmReconnect, incognito=$incognito") + if (incognito != null) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.connect_plan_repeat_join_request), + text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link), + confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), + onConfirm = { withApi { connectViaUri(chatModel, uri, incognito, connectionPlan, close) } }, + destructive = true, + ) + } else { + askCurrentOrIncognitoProfileAlert( + chatModel, uri, connectionPlan, close, + title = generalGetString(MR.strings.connect_plan_repeat_join_request), + text = AnnotatedString(generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link)), + connectDestructive = true + ) + } + } + is GroupLinkPlan.ConnectingProhibit -> { + Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingProhibit, incognito=$incognito") + val groupInfo = connectionPlan.groupLinkPlan.groupInfo_ + if (groupInfo != null) { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.connect_plan_group_already_exists), + String.format(generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_vName), groupInfo.displayName) + ) + } else { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.connect_plan_already_joining_the_group), + generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + ) + } + } + is GroupLinkPlan.Known -> { + Log.d(TAG, "planAndConnect, .GroupLink, .Known, incognito=$incognito") + val groupInfo = connectionPlan.groupLinkPlan.groupInfo + openKnownGroup(chatModel, close, groupInfo) + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.connect_plan_group_already_exists), + String.format(generalGetString(MR.strings.connect_plan_you_are_already_in_group_vName), groupInfo.displayName) + ) + } } } - - action == "contact" -> ConnectionLinkType.CONTACT - action == "invitation" -> ConnectionLinkType.INVITATION - else -> null - } - if (type != null) { - withApi { run(type) } } else { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.invalid_contact_link), - text = generalGetString(MR.strings.this_link_is_not_a_valid_connection_link) - ) + Log.d(TAG, "planAndConnect, plan error") + if (incognito != null) { + connectViaUri(chatModel, uri, incognito, connectionPlan = null, close) + } else { + askCurrentOrIncognitoProfileAlert( + chatModel, uri, connectionPlan = null, close, + title = generalGetString(MR.strings.connect_plan_connect_via_link), + connectDestructive = false + ) + } } } -suspend fun connectViaUri(chatModel: ChatModel, action: ConnectionLinkType, uri: URI, incognito: Boolean): Boolean { +suspend fun connectViaUri( + chatModel: ChatModel, + uri: URI, + incognito: Boolean, + connectionPlan: ConnectionPlan?, + close: (() -> Unit)? +): Boolean { val r = chatModel.controller.apiConnect(incognito, uri.toString()) + val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION if (r) { + close?.invoke() AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.connection_request_sent), text = - when (action) { + when (connLinkType) { ConnectionLinkType.CONTACT -> generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted) ConnectionLinkType.INVITATION -> generalGetString(MR.strings.you_will_be_connected_when_your_contacts_device_is_online) ConnectionLinkType.GROUP -> generalGetString(MR.strings.you_will_be_connected_when_group_host_device_is_online) @@ -79,6 +279,139 @@ suspend fun connectViaUri(chatModel: ChatModel, action: ConnectionLinkType, uri: return r } +fun planToConnectionLinkType(connectionPlan: ConnectionPlan): ConnectionLinkType { + return when(connectionPlan) { + is ConnectionPlan.InvitationLink -> ConnectionLinkType.INVITATION + is ConnectionPlan.ContactAddress -> ConnectionLinkType.CONTACT + is ConnectionPlan.GroupLink -> ConnectionLinkType.GROUP + } +} + +fun askCurrentOrIncognitoProfileAlert( + chatModel: ChatModel, + uri: URI, + connectionPlan: ConnectionPlan?, + close: (() -> Unit)?, + title: String, + text: AnnotatedString? = null, + connectDestructive: Boolean, +) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = title, + text = text, + buttons = { + Column { + val connectColor = if (connectDestructive) MaterialTheme.colors.error else MaterialTheme.colors.primary + SectionItemView({ + AlertManager.shared.hideAlert() + withApi { + connectViaUri(chatModel, uri, incognito = false, connectionPlan, close) + } + }) { + Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor) + } + SectionItemView({ + AlertManager.shared.hideAlert() + withApi { + connectViaUri(chatModel, uri, incognito = true, connectionPlan, close) + } + }) { + Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor) + } + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + ) +} + +fun openKnownContact(chatModel: ChatModel, close: (() -> Unit)?, contact: Contact) { + withApi { + val c = chatModel.getContactChat(contact.contactId) + if (c != null) { + close?.invoke() + openDirectChat(contact.contactId, chatModel) + } + } +} + +fun ownGroupLinkConfirmConnect( + chatModel: ChatModel, + uri: URI, + incognito: Boolean?, + connectionPlan: ConnectionPlan?, + groupInfo: GroupInfo, + close: (() -> Unit)?, +) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.connect_plan_join_your_group), + text = AnnotatedString(String.format(generalGetString(MR.strings.connect_plan_this_is_your_link_for_group_vName), groupInfo.displayName)), + buttons = { + Column { + // Open group + SectionItemView({ + AlertManager.shared.hideAlert() + openKnownGroup(chatModel, close, groupInfo) + }) { + Text(generalGetString(MR.strings.connect_plan_open_group), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + if (incognito != null) { + // Join incognito / Join with current profile + SectionItemView({ + AlertManager.shared.hideAlert() + withApi { + connectViaUri(chatModel, uri, incognito, connectionPlan, close) + } + }) { + Text( + if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), + Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error + ) + } + } else { + // Use current profile + SectionItemView({ + AlertManager.shared.hideAlert() + withApi { + connectViaUri(chatModel, uri, incognito = false, connectionPlan, close) + } + }) { + Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + // Use new incognito profile + SectionItemView({ + AlertManager.shared.hideAlert() + withApi { + connectViaUri(chatModel, uri, incognito = true, connectionPlan, close) + } + }) { + Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + } + // Cancel + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + ) +} + +fun openKnownGroup(chatModel: ChatModel, close: (() -> Unit)?, groupInfo: GroupInfo) { + withApi { + val g = chatModel.getGroupChat(groupInfo.groupId) + if (g != null) { + close?.invoke() + openGroupChat(groupInfo.groupId, chatModel) + } + } +} + @Composable fun ConnectContactLayout( chatModel: ChatModel, @@ -92,21 +425,8 @@ fun ConnectContactLayout( QRCodeScanner { connReqUri -> try { val uri = URI(connReqUri) - withUriAction(uri) { linkType -> - val action = suspend { - Log.d(TAG, "connectViaUri: connecting") - if (connectViaUri(ChatModel, linkType, uri, incognito = incognito.value)) { - close() - } - } - if (linkType == ConnectionLinkType.GROUP) { - AlertManager.shared.showAlertDialog( - title = generalGetString(MR.strings.connect_via_group_link), - text = generalGetString(MR.strings.you_will_join_group), - confirmText = if (incognito.value) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), - onConfirm = { withApi { action() } } - ) - } else action() + withApi { + planAndConnect(chatModel, uri, incognito = incognito.value, close) } } catch (e: RuntimeException) { AlertManager.shared.showAlertMsg( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt index 72cbc3a62..013222338 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt @@ -18,7 +18,8 @@ import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.newchat.QRCode +import chat.simplex.common.views.newchat.SimpleXLinkQRCode +import chat.simplex.common.views.newchat.simplexChatLink import chat.simplex.res.MR @Composable @@ -38,7 +39,7 @@ fun CreateSimpleXAddress(m: ChatModel) { sendEmail = { address -> uriHandler.sendEmail( generalGetString(MR.strings.email_invite_subject), - generalGetString(MR.strings.email_invite_body).format(address.connReqContact) + generalGetString(MR.strings.email_invite_body).format(simplexChatLink(address.connReqContact)) ) }, createAddress = { @@ -91,8 +92,8 @@ private fun CreateSimpleXAddressLayout( Spacer(Modifier.weight(1f)) if (userAddress != null) { - QRCode(userAddress.connReqContact, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) - ShareAddressButton { share(userAddress.connReqContact) } + SimpleXLinkQRCode(userAddress.connReqContact, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) + ShareAddressButton { share(simplexChatLink(userAddress.connReqContact)) } Spacer(Modifier.weight(1f)) ShareViaEmailButton { sendEmail(userAddress) } Spacer(Modifier.weight(1f)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index 210d3c613..211fe5967 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -92,9 +92,6 @@ fun PrivacySettingsView( chatModel.simplexLinkMode.value = it }) } - if (chatModel.simplexLinkMode.value == SimplexLinkMode.BROWSER) { - SectionTextFooter(stringResource(MR.strings.simplex_link_mode_browser_warning)) - } SectionDividerSpaced() val currentUser = chatModel.currentUser.value @@ -185,8 +182,10 @@ fun PrivacySettingsView( @Composable private fun SimpleXLinkOptions(simplexLinkModeState: State, onSelected: (SimplexLinkMode) -> Unit) { + val modeValues = listOf(SimplexLinkMode.DESCRIPTION, SimplexLinkMode.FULL) + val pickerValues = modeValues + if (modeValues.contains(simplexLinkModeState.value)) emptyList() else listOf(simplexLinkModeState.value) val values = remember { - SimplexLinkMode.values().map { + pickerValues.map { when (it) { SimplexLinkMode.DESCRIPTION -> it to generalGetString(MR.strings.simplex_link_mode_description) SimplexLinkMode.FULL -> it to generalGetString(MR.strings.simplex_link_mode_full) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt index 63f06a2ae..d03b75856 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt @@ -24,10 +24,10 @@ import chat.simplex.common.model.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.ShareAddressButton import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.newchat.QRCode import chat.simplex.common.model.ChatModel import chat.simplex.common.model.MsgContent import chat.simplex.common.platform.* +import chat.simplex.common.views.newchat.* import chat.simplex.res.MR @Composable @@ -100,7 +100,7 @@ fun UserAddressView( sendEmail = { userAddress -> uriHandler.sendEmail( generalGetString(MR.strings.email_invite_subject), - generalGetString(MR.strings.email_invite_body).format(userAddress.connReqContact) + generalGetString(MR.strings.email_invite_body).format(simplexChatLink( userAddress.connReqContact)) ) }, setProfileAddress = ::setProfileAddress, @@ -201,8 +201,8 @@ private fun UserAddressLayout( val autoAcceptState = remember { mutableStateOf(AutoAcceptState(userAddress)) } val autoAcceptStateSaved = remember { mutableStateOf(autoAcceptState.value) } SectionView(stringResource(MR.strings.address_section_title).uppercase()) { - QRCode(userAddress.connReqContact, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) - ShareAddressButton { share(userAddress.connReqContact) } + SimpleXLinkQRCode(userAddress.connReqContact, Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).aspectRatio(1f)) + ShareAddressButton { share(simplexChatLink(userAddress.connReqContact)) } ShareViaEmailButton { sendEmail(userAddress) } ShareWithContactsButton(shareViaProfile, setProfileAddress) AutoAcceptToggle(autoAcceptState) { saveAas(autoAcceptState.value, autoAcceptStateSaved) } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 114fe49e9..bd59d236d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -4,13 +4,13 @@ k - Connect via contact link? - Connect via invitation link? - Connect via group link? + Connect via contact address? + Connect via one-time link? + Join group? Use current profile Use new incognito profile Your profile will be sent to the contact that you received this link from. - You will join a group this link refers to and connect to its group members. + You will connect to all group members. Connect Connect incognito @@ -1600,4 +1600,25 @@ Coming soon! This feature is not yet supported. Try the next release. + + + Connect to yourself? + This is your own one-time link! + You are already connecting to %1$s. + Already connecting! + You are already connecting via this one-time link! + This is your own SimpleX address! + Repeat connection request? + You have already requested connection via this address! + Join your group? + This is your link for group %1$s! + Open group + Repeat join request? + You are already joining the group via this link! + Group already exists! + You are already joining the group %1$s. + Already joining the group! + You are already joining the group via this link. + You are already in group %1$s. + Connect via link? \ No newline at end of file diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 3f377fbd7..0dafa4bd0 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -580,11 +580,11 @@ processChatCommand = \case timed_ <- sndContactCITimed live ct itemTTL (msgContainer, quotedItem_) <- prepareMsg fInv_ timed_ (msg@SndMessage {sharedMsgId}, _) <- sendDirectContactMessage ct (XMsgNew msgContainer) + ci <- saveSndChatItem' user (CDDirectSnd ct) msg (CISndMsgContent mc) ciFile_ quotedItem_ timed_ live case ft_ of Just ft@FileTransferMeta {fileInline = Just IFMSent} -> sendDirectFileInline ct ft sharedMsgId _ -> pure () - ci <- saveSndChatItem' user (CDDirectSnd ct) msg (CISndMsgContent mc) ciFile_ quotedItem_ timed_ live forM_ (timed_ >>= timedDeleteAt') $ startProximateTimedItemThread user (ChatRef CTDirect contactId, chatItemId' ci) pure $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) @@ -645,11 +645,11 @@ processChatCommand = \case timed_ <- sndGroupCITimed live gInfo itemTTL (msgContainer, quotedItem_) <- prepareMsg fInv_ timed_ membership (msg@SndMessage {sharedMsgId}, sentToMembers) <- sendGroupMessage user gInfo ms (XMsgNew msgContainer) - mapM_ (sendGroupFileInline ms sharedMsgId) ft_ ci <- saveSndChatItem' user (CDGroupSnd gInfo) msg (CISndMsgContent mc) ciFile_ quotedItem_ timed_ live withStore' $ \db -> forM_ sentToMembers $ \GroupMember {groupMemberId} -> createGroupSndStatus db (chatItemId' ci) groupMemberId CISSndNew + mapM_ (sendGroupFileInline ms sharedMsgId) ft_ forM_ (timed_ >>= timedDeleteAt') $ startProximateTimedItemThread user (ChatRef CTGroup groupId, chatItemId' ci) pure $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) @@ -754,8 +754,9 @@ processChatCommand = \case unzipMaybe3 _ = (Nothing, Nothing, Nothing) APIUpdateChatItem (ChatRef cType chatId) itemId live mc -> withUser $ \user -> withChatLock "updateChatItem" $ case cType of CTDirect -> do - (ct@Contact {contactId}, cci) <- withStore $ \db -> (,) <$> getContact db user chatId <*> getDirectChatItem db user chatId itemId + ct@Contact {contactId} <- withStore $ \db -> getContact db user chatId assertDirectAllowed user MDSnd ct XMsgUpdate_ + cci <- withStore $ \db -> getDirectCIWithReactions db user ct itemId case cci of CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable}, content = ciContent} -> do case (ciContent, itemSharedMsgId, editable) of @@ -777,7 +778,7 @@ processChatCommand = \case CTGroup -> do Group gInfo@GroupInfo {groupId} ms <- withStore $ \db -> getGroup db user chatId assertUserGroupRole gInfo GRAuthor - cci <- withStore $ \db -> getGroupChatItem db user chatId itemId + cci <- withStore $ \db -> getGroupCIWithReactions db user gInfo itemId case cci of CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable}, content = ciContent} -> do case (ciContent, itemSharedMsgId, editable) of @@ -2384,8 +2385,8 @@ updateCallItemStatus user ct Call {chatItemId} receivedStatus msgId_ = do forM_ aciContent_ $ \aciContent -> updateDirectChatItemView user ct chatItemId aciContent False msgId_ updateDirectChatItemView :: ChatMonad m => User -> Contact -> ChatItemId -> ACIContent -> Bool -> Maybe MessageId -> m () -updateDirectChatItemView user ct@Contact {contactId} chatItemId (ACIContent msgDir ciContent) live msgId_ = do - ci' <- withStore $ \db -> updateDirectChatItem db user contactId chatItemId ciContent live msgId_ +updateDirectChatItemView user ct chatItemId (ACIContent msgDir ciContent) live msgId_ = do + ci' <- withStore $ \db -> updateDirectChatItem db user ct chatItemId ciContent live msgId_ toView $ CRChatItemUpdated user (AChatItem SCTDirect msgDir (DirectChat ct) ci') callStatusItemContent :: ChatMonad m => User -> Contact -> ChatItemId -> WebRTCCallStatus -> m (Maybe ACIContent) @@ -3989,7 +3990,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do ci' <- withStore' $ \db -> do when changed $ addInitialAndNewCIVersions db (chatItemId' ci) (chatItemTs' ci, oldMC) (brokerTs, mc) - updateDirectChatItem' db user contactId ci content live $ Just msgId + reactions <- getDirectCIReactions db ct sharedMsgId + updateDirectChatItem' db user contactId ci {reactions} content live $ Just msgId toView $ CRChatItemUpdated user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci') startUpdatedTimedItemThread user (ChatRef CTDirect contactId) ci ci' else toView $ CRChatItemNotChanged user (AChatItem SCTDirect SMDRcv (DirectChat ct) ci) @@ -4127,7 +4129,8 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do ci' <- withStore' $ \db -> do when changed $ addInitialAndNewCIVersions db (chatItemId' ci) (chatItemTs' ci, oldMC) (brokerTs, mc) - updateGroupChatItem db user groupId ci content live $ Just msgId + reactions <- getGroupCIReactions db gInfo memberId sharedMsgId + updateGroupChatItem db user groupId ci {reactions} content live $ Just msgId toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci') startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci' else toView $ CRChatItemNotChanged user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci) @@ -4932,7 +4935,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemId, itemStatus}}) | itemStatus == newStatus -> pure () | otherwise -> do - chatItem <- withStore $ \db -> updateDirectChatItemStatus db user contactId itemId newStatus + chatItem <- withStore $ \db -> updateDirectChatItemStatus db user ct itemId newStatus toView $ CRChatItemStatusUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) chatItem) _ -> pure () @@ -4955,7 +4958,7 @@ processAgentMessageConn user@User {userId} corrId agentConnId agentMessage = do memStatusCounts <- withStore' (`getGroupSndStatusCounts` itemId) let newStatus = membersGroupItemStatus memStatusCounts when (newStatus /= itemStatus) $ do - chatItem <- withStore $ \db -> updateGroupChatItemStatus db user groupId itemId newStatus + chatItem <- withStore $ \db -> updateGroupChatItemStatus db user gInfo itemId newStatus toView $ CRChatItemStatusUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) chatItem) _ -> pure () @@ -5198,9 +5201,7 @@ deliverMessage conn@Connection {connId} cmEventTag msgBody msgId = do let msgFlags = MsgFlags {notification = hasNotification cmEventTag} agentMsgId <- withAgent $ \a -> sendMessage a (aConnId conn) msgFlags msgBody let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId} - withStoreCtx' - (Just $ "createSndMsgDelivery, sndMsgDelivery: " <> show sndMsgDelivery <> ", msgId: " <> show msgId <> ", cmEventTag: " <> show cmEventTag <> ", msgDeliveryStatus: MDSSndAgent") - $ \db -> createSndMsgDelivery db sndMsgDelivery msgId + withStore' $ \db -> createSndMsgDelivery db sndMsgDelivery msgId sendGroupMessage :: (MsgEncodingI e, ChatMonad m) => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> m (SndMessage, [GroupMember]) sendGroupMessage user GroupInfo {groupId} members chatMsgEvent = diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index 1e6263277..79574f8e2 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -930,7 +930,7 @@ instance ToJSON (CIDeleted d) where data JSONCIDeleted = JCIDDeleted {deletedTs :: Maybe UTCTime} - | JCIBlocked {deletedTs :: Maybe UTCTime} + | JCIDBlocked {deletedTs :: Maybe UTCTime} | JCIDModerated {deletedTs :: Maybe UTCTime, byGroupMember :: GroupMember} deriving (Show, Generic) @@ -941,7 +941,7 @@ instance ToJSON JSONCIDeleted where jsonCIDeleted :: CIDeleted d -> JSONCIDeleted jsonCIDeleted = \case CIDeleted ts -> JCIDDeleted ts - CIBlocked ts -> JCIBlocked ts + CIBlocked ts -> JCIDBlocked ts CIModerated ts m -> JCIDModerated ts m itemDeletedTs :: CIDeleted d -> Maybe UTCTime diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 1034236c3..1e17eb3d5 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -13,7 +13,6 @@ module Simplex.Chat.Store.Messages ( getContactConnIds_, - getDirectChatReactions_, -- * Message and chat item functions deleteContactCIs, @@ -66,9 +65,11 @@ module Simplex.Chat.Store.Messages setGroupReaction, getChatItemIdByAgentMsgId, getDirectChatItem, + getDirectCIWithReactions, getDirectChatItemBySharedMsgId, getDirectChatItemByAgentMsgId, getGroupChatItem, + getGroupCIWithReactions, getGroupChatItemBySharedMsgId, getGroupMemberCIBySharedMsgId, getGroupChatItemByAgentMsgId, @@ -751,7 +752,7 @@ getGroupChat :: DB.Connection -> User -> Int64 -> ChatPagination -> Maybe String getGroupChat db user groupId pagination search_ = do let search = fromMaybe "" search_ g <- getGroupInfo db user groupId - liftIO . getGroupChatReactions_ db g =<< case pagination of + case pagination of CPLast count -> getGroupChatLast_ db user g count search CPAfter afterId count -> getGroupChatAfter_ db user g afterId count search CPBefore beforeId count -> getGroupChatBefore_ db user g beforeId count search @@ -760,7 +761,7 @@ getGroupChatLast_ :: DB.Connection -> User -> GroupInfo -> Int -> String -> Exce getGroupChatLast_ db user@User {userId} g@GroupInfo {groupId} count search = do let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} chatItemIds <- liftIO getGroupChatItemIdsLast_ - chatItems <- mapM (getGroupChatItem db user groupId) chatItemIds + chatItems <- mapM (getGroupCIWithReactions db user g) chatItemIds pure $ Chat (GroupChat g) (reverse chatItems) stats where getGroupChatItemIdsLast_ :: IO [ChatItemId] @@ -798,7 +799,7 @@ getGroupChatAfter_ db user@User {userId} g@GroupInfo {groupId} afterChatItemId c let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} afterChatItem <- getGroupChatItem db user groupId afterChatItemId chatItemIds <- liftIO $ getGroupChatItemIdsAfter_ (chatItemTs afterChatItem) - chatItems <- mapM (getGroupChatItem db user groupId) chatItemIds + chatItems <- mapM (getGroupCIWithReactions db user g) chatItemIds pure $ Chat (GroupChat g) chatItems stats where getGroupChatItemIdsAfter_ :: UTCTime -> IO [ChatItemId] @@ -821,7 +822,7 @@ getGroupChatBefore_ db user@User {userId} g@GroupInfo {groupId} beforeChatItemId let stats = ChatStats {unreadCount = 0, minUnreadItemId = 0, unreadChat = False} beforeChatItem <- getGroupChatItem db user groupId beforeChatItemId chatItemIds <- liftIO $ getGroupChatItemIdsBefore_ (chatItemTs beforeChatItem) - chatItems <- mapM (getGroupChatItem db user groupId) chatItemIds + chatItems <- mapM (getGroupCIWithReactions db user g) chatItemIds pure $ Chat (GroupChat g) (reverse chatItems) stats where getGroupChatItemIdsBefore_ :: UTCTime -> IO [ChatItemId] @@ -1145,23 +1146,24 @@ getChatItemIdByAgentMsgId db connId msgId = |] (connId, msgId) -updateDirectChatItemStatus :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItemId -> CIStatus d -> ExceptT StoreError IO (ChatItem 'CTDirect d) -updateDirectChatItemStatus db user@User {userId} contactId itemId itemStatus = do - ci <- liftEither . correctDir =<< getDirectChatItem db user contactId itemId +updateDirectChatItemStatus :: forall d. MsgDirectionI d => DB.Connection -> User -> Contact -> ChatItemId -> CIStatus d -> ExceptT StoreError IO (ChatItem 'CTDirect d) +updateDirectChatItemStatus db user@User {userId} ct@Contact {contactId} itemId itemStatus = do + ci <- liftEither . correctDir =<< getDirectCIWithReactions db user ct itemId currentTs <- liftIO getCurrentTime liftIO $ DB.execute db "UPDATE chat_items SET item_status = ?, updated_at = ? WHERE user_id = ? AND contact_id = ? AND chat_item_id = ?" (itemStatus, currentTs, userId, contactId, itemId) pure ci {meta = (meta ci) {itemStatus}} - where - correctDir :: CChatItem c -> Either StoreError (ChatItem c d) - correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci -updateDirectChatItem :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItemId -> CIContent d -> Bool -> Maybe MessageId -> ExceptT StoreError IO (ChatItem 'CTDirect d) -updateDirectChatItem db user contactId itemId newContent live msgId_ = do - ci <- liftEither . correctDir =<< getDirectChatItem db user contactId itemId +updateDirectChatItem :: MsgDirectionI d => DB.Connection -> User -> Contact -> ChatItemId -> CIContent d -> Bool -> Maybe MessageId -> ExceptT StoreError IO (ChatItem 'CTDirect d) +updateDirectChatItem db user ct@Contact {contactId} itemId newContent live msgId_ = do + ci <- liftEither . correctDir =<< getDirectCIWithReactions db user ct itemId liftIO $ updateDirectChatItem' db user contactId ci newContent live msgId_ - where - correctDir :: CChatItem c -> Either StoreError (ChatItem c d) - correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci + +getDirectCIWithReactions :: DB.Connection -> User -> Contact -> ChatItemId -> ExceptT StoreError IO (CChatItem 'CTDirect) +getDirectCIWithReactions db user ct@Contact {contactId} itemId = + liftIO . directCIWithReactions db ct =<< getDirectChatItem db user contactId itemId + +correctDir :: MsgDirectionI d => CChatItem c -> Either StoreError (ChatItem c d) +correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci updateDirectChatItem' :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTDirect d -> CIContent d -> Bool -> Maybe MessageId -> IO (ChatItem 'CTDirect d) updateDirectChatItem' db User {userId} contactId ci newContent live msgId_ = do @@ -1299,7 +1301,7 @@ getDirectChatItemIdBySharedMsgId_ db userId contactId sharedMsgId = getDirectChatItem :: DB.Connection -> User -> Int64 -> ChatItemId -> ExceptT StoreError IO (CChatItem 'CTDirect) getDirectChatItem db User {userId} contactId itemId = ExceptT $ do currentTs <- getCurrentTime - join <$> firstRow (toDirectChatItem currentTs) (SEChatItemNotFound itemId) getItem + firstRow' (toDirectChatItem currentTs) (SEChatItemNotFound itemId) getItem where getItem = DB.query @@ -1347,17 +1349,26 @@ getDirectChatItemIdByText' db User {userId} contactId msg = |] (userId, contactId, msg <> "%") -updateGroupChatItemStatus :: forall d. MsgDirectionI d => DB.Connection -> User -> GroupId -> ChatItemId -> CIStatus d -> ExceptT StoreError IO (ChatItem 'CTGroup d) -updateGroupChatItemStatus db user@User {userId} groupId itemId itemStatus = do - ci <- liftEither . correctDir =<< getGroupChatItem db user groupId itemId +updateGroupChatItemStatus :: MsgDirectionI d => DB.Connection -> User -> GroupInfo -> ChatItemId -> CIStatus d -> ExceptT StoreError IO (ChatItem 'CTGroup d) +updateGroupChatItemStatus db user@User {userId} g@GroupInfo {groupId} itemId itemStatus = do + ci <- liftEither . correctDir =<< getGroupCIWithReactions db user g itemId currentTs <- liftIO getCurrentTime liftIO $ DB.execute db "UPDATE chat_items SET item_status = ?, updated_at = ? WHERE user_id = ? AND group_id = ? AND chat_item_id = ?" (itemStatus, currentTs, userId, groupId, itemId) pure ci {meta = (meta ci) {itemStatus}} - where - correctDir :: CChatItem c -> Either StoreError (ChatItem c d) - correctDir (CChatItem _ ci) = first SEInternalError $ checkDirection ci -updateGroupChatItem :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTGroup d -> CIContent d -> Bool -> Maybe MessageId -> IO (ChatItem 'CTGroup d) +getGroupCIWithReactions :: DB.Connection -> User -> GroupInfo -> ChatItemId -> ExceptT StoreError IO (CChatItem 'CTGroup) +getGroupCIWithReactions db user g@GroupInfo {groupId} itemId = do + liftIO . groupCIWithReactions db g =<< getGroupChatItem db user groupId itemId + +groupCIWithReactions :: DB.Connection -> GroupInfo -> CChatItem 'CTGroup -> IO (CChatItem 'CTGroup) +groupCIWithReactions db g cci@(CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId}}) = case itemSharedMsgId of + Just sharedMsgId -> do + let GroupMember {memberId} = chatItemMember g ci + reactions <- getGroupCIReactions db g memberId sharedMsgId + pure $ CChatItem md ci {reactions} + Nothing -> pure cci + +updateGroupChatItem :: MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTGroup d -> CIContent d -> Bool -> Maybe MessageId -> IO (ChatItem 'CTGroup d) updateGroupChatItem db user groupId ci newContent live msgId_ = do currentTs <- liftIO getCurrentTime let ci' = updatedChatItem ci newContent live currentTs @@ -1366,7 +1377,7 @@ updateGroupChatItem db user groupId ci newContent live msgId_ = do -- this function assumes that the group item with correct chat direction already exists, -- it should be checked before calling it -updateGroupChatItem_ :: forall d. MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTGroup d -> Maybe MessageId -> IO () +updateGroupChatItem_ :: MsgDirectionI d => DB.Connection -> User -> Int64 -> ChatItem 'CTGroup d -> Maybe MessageId -> IO () updateGroupChatItem_ db User {userId} groupId ChatItem {content, meta} msgId_ = do let CIMeta {itemId, itemText, itemStatus, itemDeleted, itemEdited, itemTimed, itemLive, updatedAt} = meta itemDeleted' = isJust itemDeleted @@ -1497,7 +1508,7 @@ getGroupChatItemByAgentMsgId db user groupId connId msgId = do getGroupChatItem :: DB.Connection -> User -> Int64 -> ChatItemId -> ExceptT StoreError IO (CChatItem 'CTGroup) getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do currentTs <- getCurrentTime - join <$> firstRow (toGroupChatItem currentTs userContactId) (SEChatItemNotFound itemId) getItem + firstRow' (toGroupChatItem currentTs userContactId) (SEChatItemNotFound itemId) getItem where getItem = DB.query @@ -1667,18 +1678,15 @@ getChatItemVersions db itemId = do getDirectChatReactions_ :: DB.Connection -> Contact -> Chat 'CTDirect -> IO (Chat 'CTDirect) getDirectChatReactions_ db ct c@Chat {chatItems} = do - chatItems' <- forM chatItems $ \(CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId}}) -> do - reactions <- maybe (pure []) (getDirectCIReactions db ct) itemSharedMsgId - pure $ CChatItem md ci {reactions} + chatItems' <- mapM (directCIWithReactions db ct) chatItems pure c {chatItems = chatItems'} -getGroupChatReactions_ :: DB.Connection -> GroupInfo -> Chat 'CTGroup -> IO (Chat 'CTGroup) -getGroupChatReactions_ db g c@Chat {chatItems} = do - chatItems' <- forM chatItems $ \(CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId}}) -> do - let GroupMember {memberId} = chatItemMember g ci - reactions <- maybe (pure []) (getGroupCIReactions db g memberId) itemSharedMsgId +directCIWithReactions :: DB.Connection -> Contact -> CChatItem 'CTDirect -> IO (CChatItem 'CTDirect) +directCIWithReactions db ct cci@(CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId}}) = case itemSharedMsgId of + Just sharedMsgId -> do + reactions <- getDirectCIReactions db ct sharedMsgId pure $ CChatItem md ci {reactions} - pure c {chatItems = chatItems'} + Nothing -> pure cci getDirectCIReactions :: DB.Connection -> Contact -> SharedMsgId -> IO [CIReactionCount] getDirectCIReactions db Contact {contactId} itemSharedMsgId =