From 8a4760a2cb4a7663a9136e854d4868efda3fdf0b Mon Sep 17 00:00:00 2001 From: spaced4ndy <8711996+spaced4ndy@users.noreply.github.com> Date: Wed, 4 Jun 2025 07:47:10 +0000 Subject: [PATCH] core: set short links data, prepare entity, etc.; ios: connect to prepared contact (#5951) --- apps/ios/Shared/ContentView.swift | 1 + apps/ios/Shared/Model/AppAPITypes.swift | 27 +- apps/ios/Shared/Model/SimpleXAPI.swift | 28 +- apps/ios/Shared/Views/Chat/ChatInfoView.swift | 2 +- apps/ios/Shared/Views/Chat/ChatView.swift | 2 +- .../Chat/ComposeMessage/ComposeView.swift | 274 ++++++++------- .../ContextInvitingContactMemberView.swift | 31 -- .../Chat/ComposeMessage/SendMessageView.swift | 52 +-- .../Chat/Group/GroupMemberInfoView.swift | 1 + .../Shared/Views/ChatList/ChatListView.swift | 1 + .../Views/ChatList/ChatPreviewView.swift | 10 +- .../ios/Shared/Views/Helpers/ShareSheet.swift | 187 ++++++++++ .../Views/NewChat/NewChatMenuButton.swift | 1 + .../Shared/Views/NewChat/NewChatView.swift | 122 +++++-- .../bg.xcloc/Localized Contents/bg.xliff | 4 +- .../cs.xcloc/Localized Contents/cs.xliff | 4 +- .../de.xcloc/Localized Contents/de.xliff | 4 +- .../en.xcloc/Localized Contents/en.xliff | 6 +- .../es.xcloc/Localized Contents/es.xliff | 4 +- .../fi.xcloc/Localized Contents/fi.xliff | 4 +- .../fr.xcloc/Localized Contents/fr.xliff | 4 +- .../hu.xcloc/Localized Contents/hu.xliff | 4 +- .../it.xcloc/Localized Contents/it.xliff | 4 +- .../ja.xcloc/Localized Contents/ja.xliff | 4 +- .../nl.xcloc/Localized Contents/nl.xliff | 4 +- .../pl.xcloc/Localized Contents/pl.xliff | 4 +- .../ru.xcloc/Localized Contents/ru.xliff | 4 +- .../th.xcloc/Localized Contents/th.xliff | 4 +- .../tr.xcloc/Localized Contents/tr.xliff | 4 +- .../uk.xcloc/Localized Contents/uk.xliff | 4 +- .../Localized Contents/zh-Hans.xliff | 4 +- apps/ios/SimpleX.xcodeproj/project.pbxproj | 20 +- apps/ios/SimpleXChat/ChatTypes.swift | 17 +- apps/ios/SimpleXChat/ChatUtils.swift | 2 +- apps/ios/bg.lproj/Localizable.strings | 2 +- apps/ios/cs.lproj/Localizable.strings | 2 +- apps/ios/de.lproj/Localizable.strings | 2 +- apps/ios/es.lproj/Localizable.strings | 2 +- apps/ios/fr.lproj/Localizable.strings | 2 +- apps/ios/hu.lproj/Localizable.strings | 2 +- apps/ios/it.lproj/Localizable.strings | 2 +- apps/ios/nl.lproj/Localizable.strings | 2 +- apps/ios/pl.lproj/Localizable.strings | 2 +- apps/ios/ru.lproj/Localizable.strings | 2 +- apps/ios/tr.lproj/Localizable.strings | 2 +- apps/ios/uk.lproj/Localizable.strings | 2 +- apps/ios/zh-Hans.lproj/Localizable.strings | 2 +- .../commonMain/resources/MR/base/strings.xml | 2 +- src/Simplex/Chat/Controller.hs | 11 +- src/Simplex/Chat/Library/Commands.hs | 329 +++++++++++------- src/Simplex/Chat/Library/Subscriber.hs | 2 + src/Simplex/Chat/Store/Connections.hs | 9 +- src/Simplex/Chat/Store/Direct.hs | 107 ++++-- src/Simplex/Chat/Store/Groups.hs | 78 +++-- .../Migrations/M20250526_short_links.hs | 12 +- .../SQLite/Migrations/chat_query_plans.txt | 54 ++- .../Store/SQLite/Migrations/chat_schema.sql | 6 +- src/Simplex/Chat/Store/Shared.hs | 47 ++- src/Simplex/Chat/Types.hs | 12 +- src/Simplex/Chat/View.hs | 23 +- tests/ChatTests/Direct.hs | 7 + tests/ChatTests/Groups.hs | 3 + tests/ChatTests/Profiles.hs | 71 +++- tests/ChatTests/Utils.hs | 17 +- 64 files changed, 1117 insertions(+), 547 deletions(-) delete mode 100644 apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index 180af685e5..00cc25c1d2 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -437,6 +437,7 @@ struct ContentView: View { let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)") planAndConnect( link, + theme: theme, dismiss: false ) } else { diff --git a/apps/ios/Shared/Model/AppAPITypes.swift b/apps/ios/Shared/Model/AppAPITypes.swift index 6c47d654e0..ce44f25d7a 100644 --- a/apps/ios/Shared/Model/AppAPITypes.swift +++ b/apps/ios/Shared/Model/AppAPITypes.swift @@ -120,6 +120,10 @@ enum ChatCommand: ChatCmdProtocol { case apiSetConnectionIncognito(connId: Int64, incognito: Bool) case apiChangeConnectionUser(connId: Int64, userId: Int64) case apiConnectPlan(userId: Int64, connLink: String) + case apiPrepareContact(userId: Int64, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData) + case apiPrepareGroup(userId: Int64, connLink: CreatedConnLink, groupShortLinkData: GroupShortLinkData) + case apiConnectPreparedContact(contactId: Int64, incognito: Bool, msg: MsgContent) + case apiConnectPreparedGroup(groupId: Int64, incognito: Bool) case apiConnect(userId: Int64, incognito: Bool, connLink: CreatedConnLink) case apiConnectContactViaAddress(userId: Int64, incognito: Bool, contactId: Int64) case apiDeleteChat(type: ChatType, id: Int64, chatDeleteMode: ChatDeleteMode) @@ -314,6 +318,10 @@ enum ChatCommand: ChatCmdProtocol { case let .apiSetConnectionIncognito(connId, incognito): return "/_set incognito :\(connId) \(onOff(incognito))" case let .apiChangeConnectionUser(connId, userId): return "/_set conn user :\(connId) \(userId)" case let .apiConnectPlan(userId, connLink): return "/_connect plan \(userId) \(connLink)" + case let .apiPrepareContact(userId, connLink, contactShortLinkData): return "/_prepare contact \(userId) \(connLink.connFullLink) \(connLink.connShortLink ?? "") \(encodeJSON(contactShortLinkData))" + case let .apiPrepareGroup(userId, connLink, groupShortLinkData): return "/_prepare group \(userId) \(connLink.connFullLink) \(connLink.connShortLink ?? "") \(encodeJSON(groupShortLinkData))" + case let .apiConnectPreparedContact(contactId, incognito, mc): return "/_connect contact @\(contactId) incognito=\(onOff(incognito)) \(mc.cmdString)" + case let .apiConnectPreparedGroup(groupId, incognito): return "/_connect group #\(groupId) incognito=\(onOff(incognito))" case let .apiConnect(userId, incognito, connLink): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connLink.connFullLink) \(connLink.connShortLink ?? "")" case let .apiConnectContactViaAddress(userId, incognito, contactId): return "/_connect contact \(userId) incognito=\(onOff(incognito)) \(contactId)" case let .apiDeleteChat(type, id, chatDeleteMode): return "/_delete \(ref(type, id, scope: nil)) \(chatDeleteMode.cmdString)" @@ -482,6 +490,10 @@ enum ChatCommand: ChatCmdProtocol { case .apiSetConnectionIncognito: return "apiSetConnectionIncognito" case .apiChangeConnectionUser: return "apiChangeConnectionUser" case .apiConnectPlan: return "apiConnectPlan" + case .apiPrepareContact: return "apiPrepareContact" + case .apiPrepareGroup: return "apiPrepareGroup" + case .apiConnectPreparedContact: return "apiConnectPreparedContact" + case .apiConnectPreparedGroup: return "apiConnectPreparedGroup" case .apiConnect: return "apiConnect" case .apiDeleteChat: return "apiDeleteChat" case .apiClearChat: return "apiClearChat" @@ -729,8 +741,11 @@ enum ChatResponse1: Decodable, ChatAPIResult { case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection) case connectionUserChanged(user: UserRef, fromConnection: PendingContactConnection, toConnection: PendingContactConnection, newUser: UserRef) case connectionPlan(user: UserRef, connLink: CreatedConnLink, connectionPlan: ConnectionPlan) + case newPreparedContact(user: UserRef, contact: Contact) + case newPreparedGroup(user: UserRef, groupInfo: GroupInfo) case sentConfirmation(user: UserRef, connection: PendingContactConnection) case sentInvitation(user: UserRef, connection: PendingContactConnection) + case startedConnectionToContact(user: UserRef, contact: Contact) case sentInvitationToContact(user: UserRef, contact: Contact, customUserProfile: Profile?) case contactAlreadyExists(user: UserRef, contact: Contact) case contactDeleted(user: UserRef, contact: Contact) @@ -768,8 +783,11 @@ enum ChatResponse1: Decodable, ChatAPIResult { case .connectionIncognitoUpdated: "connectionIncognitoUpdated" case .connectionUserChanged: "connectionUserChanged" case .connectionPlan: "connectionPlan" + case .newPreparedContact: "newPreparedContact" + case .newPreparedGroup: "newPreparedContact" case .sentConfirmation: "sentConfirmation" case .sentInvitation: "sentInvitation" + case .startedConnectionToContact: "startedConnectionToContact" case .sentInvitationToContact: "sentInvitationToContact" case .contactAlreadyExists: "contactAlreadyExists" case .contactDeleted: "contactDeleted" @@ -843,8 +861,11 @@ enum ChatResponse1: Decodable, ChatAPIResult { case let .connectionIncognitoUpdated(u, toConnection): return withUser(u, String(describing: toConnection)) case let .connectionUserChanged(u, fromConnection, toConnection, newUser): return withUser(u, "fromConnection: \(String(describing: fromConnection))\ntoConnection: \(String(describing: toConnection))\nnewUserId: \(String(describing: newUser.userId))") case let .connectionPlan(u, connLink, connectionPlan): return withUser(u, "connLink: \(String(describing: connLink))\nconnectionPlan: \(String(describing: connectionPlan))") + case let .newPreparedContact(u, contact): return withUser(u, String(describing: contact)) + case let .newPreparedGroup(u, groupInfo): return withUser(u, String(describing: groupInfo)) case let .sentConfirmation(u, connection): return withUser(u, String(describing: connection)) case let .sentInvitation(u, connection): return withUser(u, String(describing: connection)) + case let .startedConnectionToContact(u, contact): return withUser(u, String(describing: contact)) case let .sentInvitationToContact(u, contact, _): return withUser(u, String(describing: contact)) case let .contactAlreadyExists(u, contact): return withUser(u, String(describing: contact)) } @@ -1261,14 +1282,14 @@ enum ConnectionPlan: Decodable, Hashable { } enum InvitationLinkPlan: Decodable, Hashable { - case ok + case ok(contactSLinkData_: ContactShortLinkData?) case ownLink case connecting(contact_: Contact?) case known(contact: Contact) } enum ContactAddressPlan: Decodable, Hashable { - case ok + case ok(contactSLinkData_: ContactShortLinkData?) case ownLink case connectingConfirmReconnect case connectingProhibit(contact: Contact) @@ -1277,7 +1298,7 @@ enum ContactAddressPlan: Decodable, Hashable { } enum GroupLinkPlan: Decodable, Hashable { - case ok + case ok(groupSLinkData_: GroupShortLinkData?) case ownLink(groupInfo: GroupInfo) case connectingConfirmReconnect case connectingProhibit(groupInfo_: GroupInfo?) diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 4f9d75bada..ad15c135ec 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -1003,6 +1003,32 @@ private func connectionErrorAlert(_ r: APIResult) -> Alert { } } +func apiPrepareContact(connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData) async throws -> Contact { + let userId = try currentUserId("apiPrepareContact") + let r: ChatResponse1 = try await chatSendCmd(.apiPrepareContact(userId: userId, connLink: connLink, contactShortLinkData: contactShortLinkData)) + if case let .newPreparedContact(_, contact) = r { return contact } + throw r.unexpected +} + +func apiPrepareGroup(connLink: CreatedConnLink, groupShortLinkData: GroupShortLinkData) async throws -> GroupInfo { + let userId = try currentUserId("apiPrepareGroup") + let r: ChatResponse1 = try await chatSendCmd(.apiPrepareGroup(userId: userId, connLink: connLink, groupShortLinkData: groupShortLinkData)) + if case let .newPreparedGroup(_, groupInfo) = r { return groupInfo } + throw r.unexpected +} + +func apiConnectPreparedContact(contactId: Int64, incognito: Bool, msg: MsgContent) async throws -> Contact { + let r: ChatResponse1 = try await chatSendCmd(.apiConnectPreparedContact(contactId: contactId, incognito: incognito, msg: msg)) + if case let .startedConnectionToContact(_, contact) = r { return contact } + throw r.unexpected +} + +func apiConnectPreparedGroup(groupId: Int64, incognito: Bool) async throws -> GroupInfo { + let r: ChatResponse1 = try await chatSendCmd(.apiConnectPreparedGroup(groupId: groupId, incognito: incognito)) + // if case let .startedConnectionToGroup(_, groupInfo) = r { return groupInfo } // TODO [short links] startedConnectionToGroup response + throw r.unexpected +} + func apiConnectContactViaAddress(incognito: Bool, contactId: Int64) async -> (Contact?, Alert?) { guard let userId = ChatModel.shared.currentUser?.userId else { logger.error("apiConnectContactViaAddress: no current user") @@ -1703,7 +1729,7 @@ func filterMembersToAdd(_ ms: [GMember]) -> [Contact] { let memberContactIds = ms.compactMap{ m in m.wrapped.memberCurrent ? m.wrapped.memberContactId : nil } return ChatModel.shared.chats .compactMap{ c in c.chatInfo.sendMsgEnabled ? c.chatInfo.contact : nil } - .filter{ c in !c.nextSendGrpInv && !memberContactIds.contains(c.apiId) } + .filter{ c in !c.sendMsgToConnect && !memberContactIds.contains(c.apiId) } .sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() } } diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 0498dc5d70..dee034b5a1 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -833,7 +833,7 @@ private struct CallButton: View { )) } } - } else if contact.nextSendGrpInv { + } else if contact.sendMsgToConnect { showAlert(SomeAlert( alert: mkAlert( title: "Can't call contact", diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 8ce0c50849..d8facb3820 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -760,7 +760,7 @@ struct ChatView: View { if case let .direct(contact) = chat.chatInfo, !contact.sndReady, contact.active, - !contact.nextSendGrpInv { + !contact.sendMsgToConnect { Text("connecting…") .font(.caption) .foregroundColor(theme.colors.secondary) diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 68a2f6d7b1..90cb671367 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -1,10 +1,3 @@ -// -// ComposeView.swift -// SimpleX -// -// Created by Evgeny on 13/03/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// import SwiftUI import SimpleXChat @@ -114,7 +107,7 @@ struct ComposeState { mentions: mentions ?? self.mentions ) } - + func mentionMemberName(_ name: String) -> String { var n = 0 var tryName = name @@ -124,11 +117,11 @@ struct ComposeState { } return tryName } - + var memberMentions: [String: Int64] { self.mentions.compactMapValues { $0.memberRef?.groupMemberId } } - + var editing: Bool { switch contextItem { case .editingItem: return true @@ -149,14 +142,14 @@ struct ComposeState { default: return false } } - + var reporting: Bool { switch contextItem { case .reportedItem: return true default: return false } } - + var submittingValidReport: Bool { switch contextItem { case let .reportedItem(_, reason): @@ -167,7 +160,7 @@ struct ComposeState { default: return false } } - + var sendEnabled: Bool { switch preview { case let .mediaPreviews(media): return !media.isEmpty @@ -371,11 +364,6 @@ struct ComposeView: View { Divider() } - if chat.chatInfo.contact?.nextSendGrpInv ?? false { - ContextInvitingContactMemberView() - Divider() - } - if case let .reportedItem(_, reason) = composeState.contextItem { reportReasonView(reason) Divider() @@ -401,73 +389,19 @@ struct ComposeView: View { default: previewView() } HStack (alignment: .bottom) { - let b = Button { - showChooseSource = true - } label: { - Image(systemName: "paperclip") - .resizable() + if !(chat.chatInfo.contact?.sendMsgToConnect ?? false) { + attachmentButton() } - .disabled(composeState.attachmentDisabled || !chat.chatInfo.sendMsgEnabled || (chat.chatInfo.contact?.nextSendGrpInv ?? false)) - .frame(width: 25, height: 25) - .padding(.bottom, 16) - .padding(.leading, 12) - .tint(theme.colors.primary) - if im.secondaryIMFilter == nil, - case let .group(g, _) = chat.chatInfo, - !g.fullGroupPreferences.files.on(for: g.membership) { - b.disabled(true).onTapGesture { - AlertManager.shared.showAlertMsg( - title: "Files and media prohibited!", - message: "Only group owners can enable files and media." - ) - } - } else { - b - } - ZStack(alignment: .leading) { - SendMessageView( - composeState: $composeState, - selectedRange: $selectedRange, - sendMessage: { ttl in - sendMessage(ttl: ttl) - resetLinkPreview() - }, - sendLiveMessage: chat.chatInfo.chatType != .local ? sendLiveMessage : nil, - updateLiveMessage: updateLiveMessage, - cancelLiveMessage: { - composeState.liveMessage = nil - chatModel.removeLiveDummy() - }, - nextSendGrpInv: chat.chatInfo.contact?.nextSendGrpInv ?? false, - voiceMessageAllowed: chat.chatInfo.featureEnabled(.voice), - disableSendButton: simplexLinkProhibited || fileProhibited || voiceProhibited, - showEnableVoiceMessagesAlert: chat.chatInfo.showEnableVoiceMessagesAlert, - startVoiceMessageRecording: { - Task { - await startVoiceMessageRecording() - } - }, - finishVoiceMessageRecording: finishVoiceMessageRecording, - allowVoiceMessagesToContact: allowVoiceMessagesToContact, - timedMessageAllowed: chat.chatInfo.featureEnabled(.timedMessages), - onMediaAdded: { media in if !media.isEmpty { chosenMedia = media }}, - keyboardVisible: $keyboardVisible, - keyboardHiddenDate: $keyboardHiddenDate, - sendButtonColor: chat.chatInfo.incognito - ? .indigo.opacity(colorScheme == .dark ? 1 : 0.7) - : theme.colors.primary - ) - .padding(.trailing, 12) - .disabled(!chat.chatInfo.sendMsgEnabled) - if let disabledText { - Text(disabledText) - .italic() - .foregroundColor(theme.colors.secondary) - .padding(.horizontal, 12) - } + sendMessageView(simplexLinkProhibited, fileProhibited, voiceProhibited) + + if chat.chatInfo.contact?.sendMsgToConnect ?? false { + sendToConnectButton() + .padding(.bottom, 16) + .padding(.horizontal, 8) } } + .padding(.horizontal, 12) } .background { Color.clear @@ -632,6 +566,141 @@ struct ComposeView: View { } } + private func sendMessageView(_ simplexLinkProhibited: Bool, _ fileProhibited: Bool, _ voiceProhibited: Bool) -> some View { + ZStack(alignment: .leading) { + SendMessageView( + composeState: $composeState, + selectedRange: $selectedRange, + sendMessage: { ttl in + sendMessage(ttl: ttl) + resetLinkPreview() + }, + sendLiveMessage: chat.chatInfo.chatType != .local ? sendLiveMessage : nil, + updateLiveMessage: updateLiveMessage, + cancelLiveMessage: { + composeState.liveMessage = nil + chatModel.removeLiveDummy() + }, + showComposeActionButtons: !(chat.chatInfo.contact?.sendMsgToConnect ?? false), + voiceMessageAllowed: chat.chatInfo.featureEnabled(.voice), + disableSendButton: simplexLinkProhibited || fileProhibited || voiceProhibited, + showEnableVoiceMessagesAlert: chat.chatInfo.showEnableVoiceMessagesAlert, + startVoiceMessageRecording: { + Task { + await startVoiceMessageRecording() + } + }, + finishVoiceMessageRecording: finishVoiceMessageRecording, + allowVoiceMessagesToContact: allowVoiceMessagesToContact, + timedMessageAllowed: chat.chatInfo.featureEnabled(.timedMessages), + onMediaAdded: { media in if !media.isEmpty { chosenMedia = media }}, + keyboardVisible: $keyboardVisible, + keyboardHiddenDate: $keyboardHiddenDate, + sendButtonColor: chat.chatInfo.incognito + ? .indigo.opacity(colorScheme == .dark ? 1 : 0.7) + : theme.colors.primary + ) + .disabled(!chat.chatInfo.sendMsgEnabled) + + if let disabledText { + Text(disabledText) + .italic() + .foregroundColor(theme.colors.secondary) + .padding(.horizontal, 12) + } + } + } + + @ViewBuilder private func attachmentButton() -> some View { + let b = Button { + showChooseSource = true + } label: { + Image(systemName: "paperclip") + .resizable() + } + .disabled(composeState.attachmentDisabled || !chat.chatInfo.sendMsgEnabled) + .frame(width: 25, height: 25) + .padding(.bottom, 16) + .tint(theme.colors.primary) + if im.secondaryIMFilter == nil, + case let .group(g, _) = chat.chatInfo, + !g.fullGroupPreferences.files.on(for: g.membership) { + b.disabled(true).onTapGesture { + AlertManager.shared.showAlertMsg( + title: "Files and media prohibited!", + message: "Only group owners can enable files and media." + ) + } + } else { + b + } + } + + private func sendToConnectButton() -> some View { + Button { + Task { + if chat.chatInfo.contact?.nextSendGrpInv ?? false { + await sendMemberContactInvitation() + } else if chat.chatInfo.contact?.nextConnectPrepared ?? false { + await sendConnectPreparedContact() + } + } + } label: { + HStack { + Text("Connect") + .fontWeight(.medium) + Image(systemName: "person.fill.badge.plus") + } + } + .disabled(!composeState.sendEnabled) + } + + private func sendMemberContactInvitation() async { + do { + let mc = checkLinkPreview() + let contact = try await apiSendMemberContactInvitation(chat.chatInfo.apiId, mc) + await MainActor.run { + self.chatModel.updateContact(contact) + clearState() + } + } catch { + logger.error("ChatView.sendMemberContactInvitation error: \(error.localizedDescription)") + AlertManager.shared.showAlertMsg(title: "Error sending member contact invitation", message: "Error: \(responseError(error))") + } + } + + private func sendConnectPreparedContact() async { + do { + let mc = checkLinkPreview() + // TODO [short links] allow to choose incognito, different user profile (as "compose context") + let contact = try await apiConnectPreparedContact(contactId: chat.chatInfo.apiId, incognito: false, msg: mc) + await MainActor.run { + self.chatModel.updateContact(contact) + clearState() + } + } catch { + logger.error("ChatView.sendConnectPreparedContact error: \(error.localizedDescription)") + AlertManager.shared.showAlertMsg(title: "Error connecting with contact", message: "Error: \(responseError(error))") + } + } + + private func checkLinkPreview() -> MsgContent { + let msgText = composeState.message + switch (composeState.preview) { + case let .linkPreview(linkPreview: linkPreview): + if let parsedMsg = parseSimpleXMarkdown(msgText), + let url = getSimplexLink(parsedMsg).url, + let linkPreview = linkPreview, + url == linkPreview.uri { + return .link(text: msgText, preview: linkPreview) + } else { + return .text(msgText) + } + default: + return .text(msgText) + } + } + private func addMediaContent(_ content: UploadContent) async { if let img = await resizeImageToStrSize(content.uiImage, maxDataSize: 14000) { var newMedia: [(String, UploadContent?)] = [] @@ -768,8 +837,8 @@ struct ComposeView: View { .frame(maxWidth: .infinity, alignment: .leading) .background(.thinMaterial) } - - + + private func reportReasonView(_ reason: ReportReason) -> some View { let reportText = switch reason { case .spam: NSLocalizedString("Report spam: only group moderators will see it.", comment: "report reason") @@ -847,9 +916,7 @@ struct ComposeView: View { if liveMessage != nil { composeState = composeState.copy(liveMessage: nil) } await sending() } - if chat.chatInfo.contact?.nextSendGrpInv ?? false { - await sendMemberContactInvitation() - } else if case let .forwardingItems(chatItems, fromChatInfo) = composeState.contextItem { + if case let .forwardingItems(chatItems, fromChatInfo) = composeState.contextItem { // Composed text is send as a reply to the last forwarded item sent = await forwardItems(chatItems, fromChatInfo, ttl).last if !composeState.message.isEmpty { @@ -929,24 +996,11 @@ struct ComposeView: View { nil } } - + func sending() async { await MainActor.run { composeState.inProgress = true } } - func sendMemberContactInvitation() async { - do { - let mc = checkLinkPreview() - let contact = try await apiSendMemberContactInvitation(chat.chatInfo.apiId, mc) - await MainActor.run { - self.chatModel.updateContact(contact) - } - } catch { - logger.error("ChatView.sendMemberContactInvitation error: \(error.localizedDescription)") - AlertManager.shared.showAlertMsg(title: "Error sending member contact invitation", message: "Error: \(responseError(error))") - } - } - func updateMessage(_ ei: ChatItem, live: Bool) async -> ChatItem? { if let oldMsgContent = ei.content.msgContent { do { @@ -1010,7 +1064,7 @@ struct ComposeView: View { return nil } } - + func send(_ reportReason: ReportReason, chatItemId: Int64) async -> ChatItem? { if let chatItems = await apiReportMessage( groupId: chat.chatInfo.apiId, @@ -1025,7 +1079,7 @@ struct ComposeView: View { } return chatItems.first } - + return nil } @@ -1114,22 +1168,6 @@ struct ComposeView: View { return [] } } - - func checkLinkPreview() -> MsgContent { - switch (composeState.preview) { - case let .linkPreview(linkPreview: linkPreview): - if let parsedMsg = parseSimpleXMarkdown(msgText), - let url = getSimplexLink(parsedMsg).url, - let linkPreview = linkPreview, - url == linkPreview.uri { - return .link(text: msgText, preview: linkPreview) - } else { - return .text(msgText) - } - default: - return .text(msgText) - } - } } private func startVoiceMessageRecording() async { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift deleted file mode 100644 index 82090f312a..0000000000 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// ContextInvitingContactMemberView.swift -// SimpleX (iOS) -// -// Created by spaced4ndy on 18.09.2023. -// Copyright © 2023 SimpleX Chat. All rights reserved. -// - -import SwiftUI - -struct ContextInvitingContactMemberView: View { - @EnvironmentObject var theme: AppTheme - - var body: some View { - HStack { - Image(systemName: "message") - .foregroundColor(theme.colors.secondary) - Text("Send direct message to connect") - } - .padding(12) - .frame(minHeight: 54) - .frame(maxWidth: .infinity, alignment: .leading) - .background(.thinMaterial) - } -} - -struct ContextInvitingContactMemberView_Previews: PreviewProvider { - static var previews: some View { - ContextInvitingContactMemberView() - } -} diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift index e7b02c9aea..dec5e893a8 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift @@ -20,7 +20,7 @@ struct SendMessageView: View { var sendLiveMessage: (() async -> Void)? = nil var updateLiveMessage: (() async -> Void)? = nil var cancelLiveMessage: (() -> Void)? = nil - var nextSendGrpInv: Bool = false + var showComposeActionButtons: Bool = true var showVoiceMessageButton: Bool = true var voiceMessageAllowed: Bool = true var disableSendButton = false @@ -68,7 +68,7 @@ struct SendMessageView: View { selectedRange: $selectedRange, onImagesAdded: onMediaAdded ) - .padding(.trailing, 32) + .padding(.trailing, showComposeActionButtons ? 32 : 16) // 16 - for deleteTextButton .allowsTightening(false) .fixedSize(horizontal: false, vertical: true) } @@ -78,18 +78,20 @@ struct SendMessageView: View { deleteTextButton() } }) - .overlay(alignment: .bottomTrailing, content: { - if progressByTimeout { - ProgressView() - .scaleEffect(1.4) - .frame(width: 31, height: 31, alignment: .center) - .padding([.bottom, .trailing], 4) - } else { - composeActionButtons() - // required for intercepting clicks - .background(.white.opacity(0.000001)) - } - }) + .if(showComposeActionButtons) { v in + v.overlay(alignment: .bottomTrailing, content: { + if progressByTimeout { + ProgressView() + .scaleEffect(1.4) + .frame(width: 31, height: 31, alignment: .center) + .padding([.bottom, .trailing], 4) + } else { + composeActionButtons() + // required for intercepting clicks + .background(.white.opacity(0.000001)) + } + }) + } .padding(.vertical, 1) .background(theme.colors.background) .clipShape(composeShape) @@ -109,9 +111,7 @@ struct SendMessageView: View { @ViewBuilder private func composeActionButtons() -> some View { let vmrs = composeState.voiceMessageRecordingState - if nextSendGrpInv { - inviteMemberContactButton() - } else if case .reportedItem = composeState.contextItem { + if case .reportedItem = composeState.contextItem { sendMessageButton() } else if showVoiceMessageButton && composeState.message.isEmpty @@ -158,24 +158,6 @@ struct SendMessageView: View { .padding([.top, .trailing], 4) } - private func inviteMemberContactButton() -> some View { - Button { - sendMessage(nil) - } label: { - Image(systemName: "arrow.up.circle.fill") - .resizable() - .foregroundColor(sendButtonColor) - .frame(width: sendButtonSize, height: sendButtonSize) - .opacity(sendButtonOpacity) - } - .disabled( - !composeState.sendEnabled || - composeState.inProgress - ) - .frame(width: 31, height: 31) - .padding([.bottom, .trailing], 4) - } - private func sendMessageButton() -> some View { Button { sendMessage(nil) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index c62c25c071..5b0d21a6d8 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -345,6 +345,7 @@ struct GroupMemberInfoView: View { Button { planAndConnect( contactLink, + theme: theme, dismiss: true ) } label: { diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 58e0882e38..5d197a59c5 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -668,6 +668,7 @@ struct ChatListSearchBar: View { private func connect(_ link: String) { planAndConnect( link, + theme: theme, dismiss: false, cleanup: { searchText = "" diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 49f629d084..2f5eabb8c9 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -334,12 +334,10 @@ struct ChatPreviewView: View { if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active { chatPreviewInfoText("Tap to Connect") .foregroundColor(theme.colors.primary) - } else if !contact.sndReady && contact.activeConn != nil { - if contact.nextSendGrpInv { - chatPreviewInfoText("send direct message") - } else if contact.active { - chatPreviewInfoText("connecting…") - } + } else if contact.sendMsgToConnect { + chatPreviewInfoText("send to connect") + } else if !contact.sndReady && contact.activeConn != nil && contact.active { + chatPreviewInfoText("connecting…") } case let .group(groupInfo, _): switch (groupInfo.membership.memberStatus) { diff --git a/apps/ios/Shared/Views/Helpers/ShareSheet.swift b/apps/ios/Shared/Views/Helpers/ShareSheet.swift index 753d92b6d9..48da43c87d 100644 --- a/apps/ios/Shared/Views/Helpers/ShareSheet.swift +++ b/apps/ios/Shared/Views/Helpers/ShareSheet.swift @@ -89,3 +89,190 @@ func showSheet( let okAlertAction = UIAlertAction(title: NSLocalizedString("Ok", comment: "alert button"), style: .default) let cancelAlertAction = UIAlertAction(title: NSLocalizedString("Cancel", comment: "alert button"), style: .cancel) + +class OpenChatAlertViewController: UIViewController { + private let profileName: String + private let profileImage: UIView + private let cancelTitle: String + private let confirmTitle: String + private let onCancel: () -> Void + private let onConfirm: () -> Void + + init( + profileName: String, + profileImage: UIView, + cancelTitle: String = "Cancel", + confirmTitle: String = "Open", + onCancel: @escaping () -> Void, + onConfirm: @escaping () -> Void + ) { + self.profileName = profileName + self.profileImage = profileImage + self.cancelTitle = cancelTitle + self.confirmTitle = confirmTitle + self.onCancel = onCancel + self.onConfirm = onConfirm + super.init(nibName: nil, bundle: nil) + + modalPresentationStyle = .overFullScreen + modalTransitionStyle = .crossDissolve + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = UIColor.black.withAlphaComponent(0.3) + + // Container view + let containerView = UIView() + containerView.backgroundColor = .systemBackground + containerView.layer.cornerRadius = 12 + containerView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(containerView) + + // Profile image sizing + profileImage.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + profileImage.widthAnchor.constraint(equalToConstant: 60), + profileImage.heightAnchor.constraint(equalToConstant: 60) + ]) + + // Name label + let nameLabel = UILabel() + nameLabel.text = profileName + nameLabel.font = UIFont.systemFont(ofSize: 18, weight: .semibold) + nameLabel.textColor = .label + nameLabel.numberOfLines = 2 + nameLabel.translatesAutoresizingMaskIntoConstraints = false + + // Horizontal stack for image + name + let hStack = UIStackView(arrangedSubviews: [profileImage, nameLabel]) + hStack.axis = .horizontal + hStack.spacing = 12 + hStack.alignment = .center + hStack.translatesAutoresizingMaskIntoConstraints = false + + let topRowContainer = UIView() + topRowContainer.translatesAutoresizingMaskIntoConstraints = false + topRowContainer.addSubview(hStack) + + NSLayoutConstraint.activate([ + hStack.topAnchor.constraint(equalTo: topRowContainer.topAnchor), + hStack.bottomAnchor.constraint(equalTo: topRowContainer.bottomAnchor), + hStack.leadingAnchor.constraint(equalTo: topRowContainer.leadingAnchor, constant: 20), + hStack.trailingAnchor.constraint(equalTo: topRowContainer.trailingAnchor, constant: -20) + ]) + + // Buttons + let cancelButton = UIButton(type: .system) + cancelButton.setTitle(cancelTitle, for: .normal) + cancelButton.titleLabel?.font = UIFont.systemFont(ofSize: 15) + cancelButton.addTarget(self, action: #selector(cancelTapped), for: .touchUpInside) + + let confirmButton = UIButton(type: .system) + confirmButton.setTitle(confirmTitle, for: .normal) + confirmButton.titleLabel?.font = UIFont.systemFont(ofSize: 15, weight: .semibold) + confirmButton.addTarget(self, action: #selector(confirmTapped), for: .touchUpInside) + + // Button stack with equal width buttons + let buttonStack = UIStackView(arrangedSubviews: [cancelButton, confirmButton]) + buttonStack.axis = .horizontal + buttonStack.distribution = .fillEqually + buttonStack.spacing = 0 // no spacing, use divider instead + buttonStack.translatesAutoresizingMaskIntoConstraints = false + buttonStack.heightAnchor.constraint(greaterThanOrEqualToConstant: 50).isActive = true + + // Vertical stack containing hStack and buttonStack + let vStack = UIStackView(arrangedSubviews: [topRowContainer, buttonStack]) + vStack.axis = .vertical + vStack.spacing = 16 + vStack.alignment = .fill // important: buttons stretch full width + vStack.translatesAutoresizingMaskIntoConstraints = false + + containerView.addSubview(vStack) + + // Add horizontal divider above buttons + let horizontalDivider = UIView() + horizontalDivider.backgroundColor = UIColor(white: 0.85, alpha: 1) + horizontalDivider.translatesAutoresizingMaskIntoConstraints = false + containerView.addSubview(horizontalDivider) + + // Add vertical divider between buttons + let verticalDivider = UIView() + verticalDivider.backgroundColor = UIColor(white: 0.85, alpha: 1) + verticalDivider.translatesAutoresizingMaskIntoConstraints = false + buttonStack.addSubview(verticalDivider) + + // Constraints + + NSLayoutConstraint.activate([ + // Container view centering and fixed width + containerView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + containerView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + containerView.widthAnchor.constraint(equalToConstant: 280), + + // Vertical stack padding inside containerView + vStack.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 20), + vStack.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 0), + vStack.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 0), + vStack.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0), + + // Center hStack horizontally inside vStack's padded width + hStack.centerXAnchor.constraint(equalTo: vStack.centerXAnchor), + + // Horizontal divider above buttons + horizontalDivider.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + horizontalDivider.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + horizontalDivider.bottomAnchor.constraint(equalTo: buttonStack.topAnchor), + horizontalDivider.heightAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale), + + // Vertical divider between buttons + verticalDivider.widthAnchor.constraint(equalToConstant: 1 / UIScreen.main.scale), + verticalDivider.topAnchor.constraint(equalTo: buttonStack.topAnchor), + verticalDivider.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), + verticalDivider.centerXAnchor.constraint(equalTo: buttonStack.centerXAnchor) + ]) + } + + @objc private func cancelTapped() { + dismiss(animated: true) { + self.onCancel() + } + } + + @objc private func confirmTapped() { + dismiss(animated: true) { + self.onConfirm() + } + } +} + + +func showOpenChatAlert( + profileName: String, + profileImage: Content, + theme: AppTheme, + cancelTitle: String = "Cancel", + confirmTitle: String = "Open", + onCancel: @escaping () -> Void = {}, + onConfirm: @escaping () -> Void +) { + let themedView = profileImage.environmentObject(theme) + let hostingController = UIHostingController(rootView: themedView) + let hostedView = hostingController.view! + hostedView.backgroundColor = .clear + + if let topVC = getTopViewController() { + let alertVC = OpenChatAlertViewController( + profileName: profileName, + profileImage: hostedView, + cancelTitle: cancelTitle, + confirmTitle: confirmTitle, + onCancel: onCancel, + onConfirm: onConfirm + ) + topVC.present(alertVC, animated: true) + } +} diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift index 7a49c30b38..63b1e5b869 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -434,6 +434,7 @@ struct ContactsListSearchBar: View { private func connect(_ link: String) { planAndConnect( link, + theme: theme, dismiss: true, cleanup: { searchText = "" diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 45c4f42a19..c36ba14c76 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -654,6 +654,7 @@ private struct ConnectView: View { private func connect(_ link: String) { planAndConnect( link, + theme: theme, dismiss: true ) } @@ -1003,8 +1004,39 @@ private func showOwnGroupLinkConfirmConnectSheet( ) } +private func showPrepareContactAlert( + connectionLink: CreatedConnLink, + contactShortLinkData: ContactShortLinkData, + theme: AppTheme, + dismiss: Bool, + cleanup: (() -> Void)? +) { + showOpenChatAlert( + profileName: contactShortLinkData.profile.displayName, + profileImage: ProfileImage(imageStr: contactShortLinkData.profile.image, size: 60), + theme: theme, + cancelTitle: NSLocalizedString("Cancel", comment: "new chat action"), + confirmTitle: NSLocalizedString("Open chat", comment: "new chat action"), + onCancel: { cleanup?() }, + onConfirm: { + Task { + do { + let contact = try await apiPrepareContact(connLink: connectionLink, contactShortLinkData: contactShortLinkData) + await MainActor.run { + ChatModel.shared.addChat(Chat(chatInfo: .direct(contact: contact))) + openKnownContact(contact, dismiss: dismiss, showAlreadyExistsAlert: nil) + } + } catch let error { + logger.error("deleteGroupAlert apiDeleteChat error: \(error.localizedDescription)") + } + } + } + ) +} + func planAndConnect( _ shortOrFullLink: String, + theme: AppTheme, dismiss: Bool, cleanup: (() -> Void)? = nil, filterKnownContact: ((Contact) -> Void)? = nil, @@ -1016,16 +1048,29 @@ func planAndConnect( switch connectionPlan { case let .invitationLink(ilp): switch ilp { - case .ok: - logger.debug("planAndConnect, .invitationLink, .ok") - await MainActor.run { - showAskCurrentOrIncognitoProfileSheet( - title: NSLocalizedString("Connect via one-time link", comment: "new chat sheet title"), - connectionLink: connectionLink, - connectionPlan: connectionPlan, - dismiss: dismiss, - cleanup: cleanup - ) + case let .ok(contactSLinkData_): + if let contactSLinkData = contactSLinkData_ { + logger.debug("planAndConnect, .invitationLink, .ok, no short link data") + await MainActor.run { + showPrepareContactAlert( + connectionLink: connectionLink, + contactShortLinkData: contactSLinkData, + theme: theme, + dismiss: dismiss, + cleanup: cleanup + ) + } + } else { + logger.debug("planAndConnect, .invitationLink, .ok, short link data present") + await MainActor.run { + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("Connect via one-time link", comment: "new chat sheet title"), + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) + } } case .ownLink: logger.debug("planAndConnect, .invitationLink, .ownLink") @@ -1064,16 +1109,29 @@ func planAndConnect( } case let .contactAddress(cap): switch cap { - case .ok: - logger.debug("planAndConnect, .contactAddress, .ok") - await MainActor.run { - showAskCurrentOrIncognitoProfileSheet( - title: NSLocalizedString("Connect via contact address", comment: "new chat sheet title"), - connectionLink: connectionLink, - connectionPlan: connectionPlan, - dismiss: dismiss, - cleanup: cleanup - ) + case let .ok(contactSLinkData_): + if let contactSLinkData = contactSLinkData_ { + logger.debug("planAndConnect, .contactAddress, .ok, no short link data") + await MainActor.run { + showPrepareContactAlert( + connectionLink: connectionLink, + contactShortLinkData: contactSLinkData, + theme: theme, + dismiss: dismiss, + cleanup: cleanup + ) + } + } else { + logger.debug("planAndConnect, .contactAddress, .ok, short link data present") + await MainActor.run { + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("Connect via contact address", comment: "new chat sheet title"), + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) + } } case .ownLink: logger.debug("planAndConnect, .contactAddress, .ownLink") @@ -1129,15 +1187,21 @@ func planAndConnect( } case let .groupLink(glp): switch glp { - case .ok: - await MainActor.run { - showAskCurrentOrIncognitoProfileSheet( - title: NSLocalizedString("Join group", comment: "new chat sheet title"), - connectionLink: connectionLink, - connectionPlan: connectionPlan, - dismiss: dismiss, - cleanup: cleanup - ) + case let .ok(groupSLinkData_): + if let groupSLinkData = groupSLinkData_ { + logger.debug("planAndConnect, .groupLink, .ok, no short link data") + // TODO [short links] showPrepareGroupAlert -> apiPrepareGroup + } else { + logger.debug("planAndConnect, .groupLink, .ok, short link data present") + await MainActor.run { + showAskCurrentOrIncognitoProfileSheet( + title: NSLocalizedString("Join group", comment: "new chat sheet title"), + connectionLink: connectionLink, + connectionPlan: connectionPlan, + dismiss: dismiss, + cleanup: cleanup + ) + } } case let .ownLink(groupInfo): logger.debug("planAndConnect, .groupLink, .ownLink") diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index b5a217d8c0..a1e8f0078b 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -9507,8 +9507,8 @@ time to disappear кодът за сигурност е променен chat item text - - send direct message + + send to connect изпрати лично съобщение No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index fe0e02ccdf..a8baac75cf 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -9185,8 +9185,8 @@ time to disappear bezpečnostní kód změněn chat item text - - send direct message + + send to connect odeslat přímou zprávu No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index b2e404c141..479ab76df6 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -10068,8 +10068,8 @@ time to disappear Sicherheitscode wurde geändert chat item text - - send direct message + + send to connect Direktnachricht senden No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 5982f620b8..18d1dbddf6 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -10068,9 +10068,9 @@ time to disappear security code changed chat item text - - send direct message - send direct message + + send to connect + send to connect No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index 3c3ae9ff46..18c814dbfc 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -10031,8 +10031,8 @@ time to disappear código de seguridad cambiado chat item text - - send direct message + + send to connect Enviar mensaje directo No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index 7c93c5b0bb..edb52e0363 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -9157,8 +9157,8 @@ time to disappear turvakoodi on muuttunut chat item text - - send direct message + + send to connect No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index 80b3428cfe..559f72a663 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -9948,8 +9948,8 @@ time to disappear code de sécurité modifié chat item text - - send direct message + + send to connect envoyer un message direct No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index 5fd4c21027..e392e4d196 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -10068,8 +10068,8 @@ time to disappear a biztonsági kód módosult chat item text - - send direct message + + send to connect közvetlen üzenet küldése No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index d3c2a139cc..244b72ba36 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -10068,8 +10068,8 @@ time to disappear codice di sicurezza modificato chat item text - - send direct message + + send to connect invia messaggio diretto No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 2a7bfa8df1..4703d9db0b 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -9228,8 +9228,8 @@ time to disappear セキュリティコードが変更されました chat item text - - send direct message + + send to connect No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index d0b430cf02..cf1bd87084 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -10024,8 +10024,8 @@ time to disappear beveiligingscode gewijzigd chat item text - - send direct message + + send to connect stuur een direct bericht No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index 3255489efd..82cc6fac6a 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -9813,8 +9813,8 @@ time to disappear kod bezpieczeństwa zmieniony chat item text - - send direct message + + send to connect wyślij wiadomość bezpośrednią No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 2ec1130718..ff110641de 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -10067,8 +10067,8 @@ time to disappear код безопасности изменился chat item text - - send direct message + + send to connect отправьте сообщение No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index 528219b13a..19aaba9707 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -9123,8 +9123,8 @@ time to disappear เปลี่ยนรหัสความปลอดภัยแล้ว chat item text - - send direct message + + send to connect No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index d17a272016..2d982d0a8f 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -9828,8 +9828,8 @@ time to disappear güvenlik kodu değiştirildi chat item text - - send direct message + + send to connect doğrudan mesaj gönder No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index 687393cfab..b63d933a80 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -9890,8 +9890,8 @@ time to disappear змінено код безпеки chat item text - - send direct message + + send to connect надіслати пряме повідомлення No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index 06ce8d4950..31d916b725 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -9928,8 +9928,8 @@ time to disappear 安全密码已更改 chat item text - - send direct message + + send to connect 发送私信 No comment provided by engineer. diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 42e36a78c9..01783cc54d 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -145,7 +145,6 @@ 640417CE2B29B8C200CCB412 /* NewChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640417CC2B29B8C200CCB412 /* NewChatView.swift */; }; 640743612CD360E600158442 /* ChooseServerOperators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640743602CD360E600158442 /* ChooseServerOperators.swift */; }; 6407BA83295DA85D0082BA18 /* CIInvalidJSONView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */; }; - 6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */; }; 6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */; }; 642BA82D2CE50495005E9412 /* NewServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 642BA82C2CE50495005E9412 /* NewServerView.swift */; }; 6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */; }; @@ -179,8 +178,8 @@ 64C3B0212A0D359700E19930 /* CustomTimePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */; }; 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829982D54AEED006B9E89 /* libgmp.a */; }; 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C829992D54AEEE006B9E89 /* libffi.a */; }; - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F-ghc9.6.3.a */; }; - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F.a */; }; + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-DxARMdkkcOxGCpBWsdb8x4-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-DxARMdkkcOxGCpBWsdb8x4-ghc9.6.3.a */; }; + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-DxARMdkkcOxGCpBWsdb8x4.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-DxARMdkkcOxGCpBWsdb8x4.a */; }; 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */; }; 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; }; 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; @@ -508,7 +507,6 @@ 640417CC2B29B8C200CCB412 /* NewChatView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatView.swift; sourceTree = ""; }; 640743602CD360E600158442 /* ChooseServerOperators.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChooseServerOperators.swift; sourceTree = ""; }; 6407BA82295DA85D0082BA18 /* CIInvalidJSONView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIInvalidJSONView.swift; sourceTree = ""; }; - 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextInvitingContactMemberView.swift; sourceTree = ""; }; 6419EC572AB97507004A607A /* CIMemberCreatedContactView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIMemberCreatedContactView.swift; sourceTree = ""; }; 642BA82C2CE50495005E9412 /* NewServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewServerView.swift; sourceTree = ""; }; 6432857B2925443C00FBE5C8 /* GroupPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupPreferencesView.swift; sourceTree = ""; }; @@ -543,8 +541,8 @@ 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTimePicker.swift; sourceTree = ""; }; 64C829982D54AEED006B9E89 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; 64C829992D54AEEE006B9E89 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F-ghc9.6.3.a"; sourceTree = ""; }; - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F.a"; sourceTree = ""; }; + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-DxARMdkkcOxGCpBWsdb8x4-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.2-DxARMdkkcOxGCpBWsdb8x4-ghc9.6.3.a"; sourceTree = ""; }; + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-DxARMdkkcOxGCpBWsdb8x4.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.4.0.2-DxARMdkkcOxGCpBWsdb8x4.a"; sourceTree = ""; }; 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressView.swift; sourceTree = ""; }; 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = ""; }; @@ -702,8 +700,8 @@ 64C8299D2D54AEEE006B9E89 /* libgmp.a in Frameworks */, 64C8299E2D54AEEE006B9E89 /* libffi.a in Frameworks */, 64C829A12D54AEEE006B9E89 /* libgmpxx.a in Frameworks */, - 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F-ghc9.6.3.a in Frameworks */, - 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F.a in Frameworks */, + 64C8299F2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-DxARMdkkcOxGCpBWsdb8x4-ghc9.6.3.a in Frameworks */, + 64C829A02D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-DxARMdkkcOxGCpBWsdb8x4.a in Frameworks */, CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -788,8 +786,8 @@ 64C829992D54AEEE006B9E89 /* libffi.a */, 64C829982D54AEED006B9E89 /* libgmp.a */, 64C8299C2D54AEEE006B9E89 /* libgmpxx.a */, - 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F-ghc9.6.3.a */, - 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-K4qWCwk6PxbL8qHn42QC4F.a */, + 64C8299A2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-DxARMdkkcOxGCpBWsdb8x4-ghc9.6.3.a */, + 64C8299B2D54AEEE006B9E89 /* libHSsimplex-chat-6.4.0.2-DxARMdkkcOxGCpBWsdb8x4.a */, ); path = Libraries; sourceTree = ""; @@ -1085,7 +1083,6 @@ 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */, 644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */, D72A9087294BD7A70047C86D /* NativeTextEditor.swift */, - 6419EC552AB8BC8B004A607A /* ContextInvitingContactMemberView.swift */, 64A77A012DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift */, ); path = ComposeMessage; @@ -1466,7 +1463,6 @@ 5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */, 5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */, 8C74C3EC2C1B92A900039E77 /* Theme.swift in Sources */, - 6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */, 5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */, 8C7D949A2B88952700B7B9E1 /* MigrateToDevice.swift in Sources */, 5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */, diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 9a82c912dd..889469511a 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -1341,7 +1341,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { switch self { case let .direct(contact): // TODO [short links] this will have additional statuses for pending contact requests before they are accepted - if contact.nextSendGrpInv { return nil } + if contact.sendMsgToConnect { return nil } if !contact.active { return ("contact deleted", nil) } if !contact.sndReady { return ("contact not ready", nil) } if contact.activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false { return ("not synchronized", nil) } @@ -1708,19 +1708,22 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable { var createdAt: Date var updatedAt: Date var chatTs: Date? + public var connLinkToConnect: CreatedConnLink? var contactGroupMemberId: Int64? var contactGrpInvSent: Bool public var chatTags: [Int64] public var chatItemTTL: Int64? public var uiThemes: ThemeModeOverrides? public var chatDeleted: Bool - + public var id: ChatId { get { "@\(contactId)" } } public var apiId: Int64 { get { contactId } } public var ready: Bool { get { activeConn?.connStatus == .ready } } public var sndReady: Bool { get { ready || activeConn?.connStatus == .sndReady } } public var active: Bool { get { contactStatus == .active } } public var nextSendGrpInv: Bool { get { contactGroupMemberId != nil && !contactGrpInvSent } } + public var nextConnectPrepared: Bool { get { connLinkToConnect != nil && activeConn == nil } } + public var sendMsgToConnect: Bool { get { nextSendGrpInv || nextConnectPrepared } } public var displayName: String { localAlias == "" ? profile.displayName : localAlias } public var fullName: String { get { profile.fullName } } public var image: String? { get { profile.image } } @@ -2052,6 +2055,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable { var createdAt: Date var updatedAt: Date var chatTs: Date? + public var connLinkToConnect: CreatedConnLink? public var uiThemes: ThemeModeOverrides? public var membersRequireAttention: Int @@ -2165,6 +2169,15 @@ public enum MemberCriteria: String, Codable, Identifiable, Hashable { } } +public struct ContactShortLinkData: Codable, Hashable { + public var profile: Profile + public var welcomeMsg: String? +} + +public struct GroupShortLinkData: Codable, Hashable { + public var groupProfile: GroupProfile +} + public struct BusinessChatInfo: Decodable, Hashable { public var chatType: BusinessChatType public var businessId: String diff --git a/apps/ios/SimpleXChat/ChatUtils.swift b/apps/ios/SimpleXChat/ChatUtils.swift index 6f80629932..28972e4c72 100644 --- a/apps/ios/SimpleXChat/ChatUtils.swift +++ b/apps/ios/SimpleXChat/ChatUtils.swift @@ -82,7 +82,7 @@ public func foundChat(_ chat: ChatLike, _ searchStr: String) -> Bool { private func canForwardToChat(_ cInfo: ChatInfo) -> Bool { switch cInfo { - case let .direct(contact): cInfo.sendMsgEnabled && !contact.nextSendGrpInv + case let .direct(contact): cInfo.sendMsgEnabled && !contact.sendMsgToConnect case .group: cInfo.sendMsgEnabled case .local: cInfo.sendMsgEnabled case .contactRequest: false diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index 1f4ff88f78..2d5681fad3 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -3388,7 +3388,7 @@ chat item action */ "Send delivery receipts to" = "Изпращайте потвърждениe за доставка на"; /* No comment provided by engineer. */ -"send direct message" = "изпрати лично съобщение"; +"send to connect" = "изпрати лично съобщение"; /* No comment provided by engineer. */ "Send direct message to connect" = "Изпрати лично съобщение за свързване"; diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index a3a6ca8215..9cc6eb9047 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -2657,7 +2657,7 @@ chat item action */ "Send delivery receipts to" = "Potvrzení o doručení zasílat na"; /* No comment provided by engineer. */ -"send direct message" = "odeslat přímou zprávu"; +"send to connect" = "odeslat přímou zprávu"; /* No comment provided by engineer. */ "Send direct message to connect" = "Odeslat přímou zprávu pro připojení"; diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index 4be4ad96ba..46f29cd799 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -4597,7 +4597,7 @@ chat item action */ "Send delivery receipts to" = "Empfangsbestätigungen senden an"; /* No comment provided by engineer. */ -"send direct message" = "Direktnachricht senden"; +"send to connect" = "Direktnachricht senden"; /* No comment provided by engineer. */ "Send direct message to connect" = "Eine Direktnachricht zum Verbinden senden"; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index a8782c401f..caae0f495d 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -4492,7 +4492,7 @@ chat item action */ "Send delivery receipts to" = "Enviar confirmaciones de entrega a"; /* No comment provided by engineer. */ -"send direct message" = "Enviar mensaje directo"; +"send to connect" = "Enviar mensaje directo"; /* No comment provided by engineer. */ "Send direct message to connect" = "Envía un mensaje para conectar"; diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 9b570a5ae9..36b42c0edd 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -4303,7 +4303,7 @@ chat item action */ "Send delivery receipts to" = "Envoyer les accusés de réception à"; /* No comment provided by engineer. */ -"send direct message" = "envoyer un message direct"; +"send to connect" = "envoyer un message direct"; /* No comment provided by engineer. */ "Send direct message to connect" = "Envoyer un message direct pour vous connecter"; diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index 9a1da01665..14dc8f6e4a 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -4597,7 +4597,7 @@ chat item action */ "Send delivery receipts to" = "A kézbesítési jelentéseket a következő címre kell küldeni"; /* No comment provided by engineer. */ -"send direct message" = "közvetlen üzenet küldése"; +"send to connect" = "közvetlen üzenet küldése"; /* No comment provided by engineer. */ "Send direct message to connect" = "Közvetlen üzenet küldése a kapcsolódáshoz"; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index f36b35efc2..304b976885 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -4597,7 +4597,7 @@ chat item action */ "Send delivery receipts to" = "Invia ricevute di consegna a"; /* No comment provided by engineer. */ -"send direct message" = "invia messaggio diretto"; +"send to connect" = "invia messaggio diretto"; /* No comment provided by engineer. */ "Send direct message to connect" = "Invia messaggio diretto per connetterti"; diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index 5caea12ee2..5b646d4bfa 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -4489,7 +4489,7 @@ chat item action */ "Send delivery receipts to" = "Stuur ontvangstbewijzen naar"; /* No comment provided by engineer. */ -"send direct message" = "stuur een direct bericht"; +"send to connect" = "stuur een direct bericht"; /* No comment provided by engineer. */ "Send direct message to connect" = "Stuur een direct bericht om verbinding te maken"; diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index e3e860e329..5c4335bffe 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -4027,7 +4027,7 @@ chat item action */ "Send delivery receipts to" = "Wyślij potwierdzenia dostawy do"; /* No comment provided by engineer. */ -"send direct message" = "wyślij wiadomość bezpośrednią"; +"send to connect" = "wyślij wiadomość bezpośrednią"; /* No comment provided by engineer. */ "Send direct message to connect" = "Wyślij wiadomość bezpośrednią aby połączyć"; diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 70cd739531..4f5d6afa01 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -4597,7 +4597,7 @@ chat item action */ "Send delivery receipts to" = "Отправка отчётов о доставке"; /* No comment provided by engineer. */ -"send direct message" = "отправьте сообщение"; +"send to connect" = "отправьте сообщение"; /* No comment provided by engineer. */ "Send direct message to connect" = "Отправьте сообщение чтобы соединиться"; diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index 3d44c895ec..1b30c0ba21 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -4063,7 +4063,7 @@ chat item action */ "Send delivery receipts to" = "Görüldü bilgilerini şuraya gönder"; /* No comment provided by engineer. */ -"send direct message" = "doğrudan mesaj gönder"; +"send to connect" = "doğrudan mesaj gönder"; /* No comment provided by engineer. */ "Send direct message to connect" = "Bağlanmak için doğrudan mesaj gönder"; diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index 932c29d368..2dd85ded41 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -4141,7 +4141,7 @@ chat item action */ "Send delivery receipts to" = "Надсилання звітів про доставку"; /* No comment provided by engineer. */ -"send direct message" = "надіслати пряме повідомлення"; +"send to connect" = "надіслати пряме повідомлення"; /* No comment provided by engineer. */ "Send direct message to connect" = "Надішліть пряме повідомлення, щоб підключитися"; diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index 19d7c268d4..272ad5ebf8 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -4393,7 +4393,7 @@ chat item action */ "Send delivery receipts to" = "将送达回执发送给"; /* No comment provided by engineer. */ -"send direct message" = "发送私信"; +"send to connect" = "发送私信"; /* No comment provided by engineer. */ "Send direct message to connect" = "发送私信来连接"; 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 7af75e64e8..1804d7c644 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -421,7 +421,7 @@ Chats Settings connecting… - send direct message + send to connect you are invited to group join as %s rejected diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index da30795850..c28053a8ca 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -448,12 +448,12 @@ data ChatCommand | APISetConnectionIncognito Int64 IncognitoEnabled | APIChangeConnectionUser Int64 UserId -- new user id to switch connection to | APIConnectPlan UserId AConnectionLink - | APIPrepareContact UserId ContactShortLinkData ACreatedConnLink - | APIPrepareGroup UserId GroupShortLinkData ACreatedConnLink + | APIPrepareContact UserId ACreatedConnLink ContactShortLinkData + | APIPrepareGroup UserId ACreatedConnLink GroupShortLinkData | APIChangeContactUser ContactId UserId | APIChangeGroupUser GroupId UserId - | APIConnectPreparedContact {contactId :: ContactId, msgContent_ :: Maybe MsgContent} - | APIConnectPreparedGroup GroupId + | APIConnectPreparedContact {contactId :: ContactId, incognito :: IncognitoEnabled, msgContent_ :: Maybe MsgContent} + | APIConnectPreparedGroup GroupId IncognitoEnabled | APIConnect UserId IncognitoEnabled (Maybe ACreatedConnLink) (Maybe MsgContent) | Connect IncognitoEnabled (Maybe AConnectionLink) | APIConnectContactViaAddress UserId IncognitoEnabled ContactId @@ -683,8 +683,11 @@ data ChatResponse | CRConnectionIncognitoUpdated {user :: User, toConnection :: PendingContactConnection} | CRConnectionUserChanged {user :: User, fromConnection :: PendingContactConnection, toConnection :: PendingContactConnection, newUser :: User} | CRConnectionPlan {user :: User, connLink :: ACreatedConnLink, connectionPlan :: ConnectionPlan} + | CRNewPreparedContact {user :: User, contact :: Contact} + | CRNewPreparedGroup {user :: User, groupInfo :: GroupInfo} | CRSentConfirmation {user :: User, connection :: PendingContactConnection} | CRSentInvitation {user :: User, connection :: PendingContactConnection, customUserProfile :: Maybe Profile} + | CRStartedConnectionToContact {user :: User, contact :: Contact} | CRSentInvitationToContact {user :: User, contact :: Contact, customUserProfile :: Maybe Profile} | CRItemsReadForChat {user :: User, chatInfo :: AChatInfo} | CRContactDeleted {user :: User, contact :: Contact} diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 92ad34da26..ff154cb4cf 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -1267,6 +1267,7 @@ processChatCommand' vr = \case APICallStatus contactId receivedStatus -> withCurrentCall contactId $ \user ct call -> updateCallItemStatus user ct call receivedStatus Nothing $> Just call + -- TODO [short links] update address short link data APIUpdateProfile userId profile -> withUserId userId (`updateProfile` profile) APISetContactPrefs contactId prefs' -> withUser $ \user -> do ct <- withFastStore $ \db -> getContact db vr user contactId @@ -1666,36 +1667,42 @@ processChatCommand' vr = \case -- [incognito] generate profile for connection incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing subMode <- chatReadVar subscriptionMode - let userData = shortLinkUserData short + let userData = + if short + then Just $ encodeShortLinkData (ContactShortLinkData (userProfileToSend user incognitoProfile Nothing False) Nothing) + else Nothing (connId, ccLink) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation userData Nothing IKPQOn subMode ccLink' <- shortenCreatedLink ccLink -- TODO PQ pass minVersion from the current range - conn <- withFastStore' $ \db -> createDirectConnection db user connId ccLink' ConnNew incognitoProfile subMode initialChatVersion PQSupportOn + conn <- withFastStore' $ \db -> createDirectConnection db user connId ccLink' Nothing ConnNew incognitoProfile subMode initialChatVersion PQSupportOn pure $ CRInvitation user ccLink' conn AddContact short incognito -> withUser $ \User {userId} -> processChatCommand $ APIAddContact userId short incognito APISetConnectionIncognito connId incognito -> withUser $ \user@User {userId} -> do - conn'_ <- withFastStore $ \db -> do - conn@PendingContactConnection {pccConnStatus, customUserProfileId} <- getPendingContactConnection db userId connId - case (pccConnStatus, customUserProfileId, incognito) of - (ConnNew, Nothing, True) -> liftIO $ do - incognitoProfile <- generateRandomProfile + conn <- withFastStore $ \db -> getPendingContactConnection db userId connId + let PendingContactConnection {pccConnStatus, customUserProfileId} = conn + case (pccConnStatus, customUserProfileId, incognito) of + (ConnNew, Nothing, True) -> do + incognitoProfile <- liftIO generateRandomProfile + sLnk <- updatePCCShortLinkData conn (ContactShortLinkData (userProfileToSend user (Just incognitoProfile) Nothing False) Nothing) + conn' <- withFastStore' $ \db -> do pId <- createIncognitoProfile db user incognitoProfile - Just <$> updatePCCIncognito db user conn (Just pId) - (ConnNew, Just pId, False) -> liftIO $ do + updatePCCIncognito db user conn (Just pId) sLnk + pure $ CRConnectionIncognitoUpdated user conn' + (ConnNew, Just pId, False) -> do + sLnk <- updatePCCShortLinkData conn (ContactShortLinkData (userProfileToSend user Nothing Nothing False) Nothing) + conn' <- withFastStore' $ \db -> do deletePCCIncognitoProfile db user pId - Just <$> updatePCCIncognito db user conn Nothing - _ -> pure Nothing - case conn'_ of - Just conn' -> pure $ CRConnectionIncognitoUpdated user conn' - Nothing -> throwChatError CEConnectionIncognitoChangeProhibited + updatePCCIncognito db user conn Nothing sLnk + pure $ CRConnectionIncognitoUpdated user conn' + _ -> throwChatError CEConnectionIncognitoChangeProhibited APIChangeConnectionUser connId newUserId -> withUser $ \user@User {userId} -> do conn <- withFastStore $ \db -> getPendingContactConnection db userId connId let PendingContactConnection {pccConnStatus, connLinkInv} = conn case (pccConnStatus, connLinkInv) of (ConnNew, Just (CCLink cReqInv _)) -> do newUser <- privateGetUser newUserId - conn' <- ifM (canKeepLink cReqInv newUser) (updateConnRecord user conn newUser) (recreateConn user conn newUser) + conn' <- ifM (canKeepLink cReqInv newUser) (updateConn user conn newUser) (recreateConn user conn newUser) pure $ CRConnectionUserChanged user conn conn' newUser _ -> throwChatError CEConnectionUserChangeProhibited where @@ -1707,39 +1714,44 @@ processChatCommand' vr = \case map protoServer' . L.filter (\ServerCfg {enabled} -> enabled) <$> getKnownAgentServers SPSMP newUser pure $ smpServer `elem` newUserServers - updateConnRecord user@User {userId} conn@PendingContactConnection {customUserProfileId} newUser = do + updateConn user@User {userId} conn@PendingContactConnection {customUserProfileId} newUser = do withAgent $ \a -> changeConnectionUser a (aUserId user) (aConnId' conn) (aUserId newUser) + sLnk <- updatePCCShortLinkData conn (ContactShortLinkData (userProfileToSend newUser Nothing Nothing False) Nothing) withFastStore' $ \db -> do - conn' <- updatePCCUser db userId conn newUserId + conn' <- updatePCCUser db userId conn newUserId sLnk forM_ customUserProfileId $ \profileId -> deletePCCIncognitoProfile db user profileId pure conn' recreateConn user conn@PendingContactConnection {customUserProfileId, connLinkInv} newUser = do subMode <- chatReadVar subscriptionMode - let userData = shortLinkUserData $ isJust $ connShortLink =<< connLinkInv + let short = isJust $ connShortLink =<< connLinkInv + userData = + if short + then Just $ encodeShortLinkData (ContactShortLinkData (userProfileToSend user Nothing Nothing False) Nothing) + else Nothing (agConnId, ccLink) <- withAgent $ \a -> createConnection a (aUserId newUser) True SCMInvitation userData Nothing IKPQOn subMode ccLink' <- shortenCreatedLink ccLink conn' <- withFastStore' $ \db -> do deleteConnectionRecord db user connId forM_ customUserProfileId $ \profileId -> deletePCCIncognitoProfile db user profileId - createDirectConnection db newUser agConnId ccLink' ConnNew Nothing subMode initialChatVersion PQSupportOn + createDirectConnection db newUser agConnId ccLink' Nothing ConnNew Nothing subMode initialChatVersion PQSupportOn deleteAgentConnectionAsync (aConnId' conn) pure conn' APIConnectPlan userId cLink -> withUserId userId $ \user -> uncurry (CRConnectionPlan user) <$> connectPlan user cLink - -- TODO [short links] prepare entity - -- TODO - UI would call these APIs after Ok connection plans with short link data - -- TODO - Persist ACreatedConnLink to be used for connection later on user action: - -- TODO - `link` to contacts.inv_conn_req_to_connect, contacts.addr_conn_req_to_connect, groups.conn_req_to_connect - -- TODO - prepared "invitation" and "address" contacts have to be differentiated, - -- TODO for example to warn user before deleting "invitation" contact, hence two fields - -- TODO - Alternatively, entity can be prepared without user action during Ok plans - -- TODO to avoid extra user action, then these APIs can be avoided altogether - APIPrepareContact userId contactSLinkData link -> withUserId userId $ \user -> do - ok_ - APIPrepareGroup userId groupSLinkData link -> withUserId userId $ \user -> do - ok_ + APIPrepareContact userId link contactSLinkData -> withUserId userId $ \user -> do + let ContactShortLinkData {profile, welcomeMsg} = contactSLinkData + ct <- withStore $ \db -> createPreparedContact db user profile link + forM_ welcomeMsg $ \msg -> + createInternalChatItem user (CDDirectRcv ct) (CIRcvMsgContent $ MCText msg) Nothing + pure $ CRNewPreparedContact user ct + APIPrepareGroup userId link groupSLinkData -> withUserId userId $ \user -> do + let GroupShortLinkData {groupProfile} = groupSLinkData + -- TODO [short link] create host member for group connection on CONF, XGrpLinkInv (as in createGroupViaLink') + -- TODO - see other problems in createPreparedGroup: invited member id (user member), business chats + gInfo <- withStore $ \db -> createPreparedGroup db vr user groupProfile link + pure $ CRNewPreparedGroup user gInfo -- TODO [short links] change prepared entity user -- TODO - UI would call these APIs before APIConnectPrepared... APIs -- TODO - UI to transition to new user keeping chat opened @@ -1747,61 +1759,47 @@ processChatCommand' vr = \case ok_ APIChangeGroupUser groupId newUserId -> withUser $ \user -> do ok_ - -- TODO [short links] connect to prepared entity - -- TODO - UI would call these APIs from ChatView on user action after entity is prepared - -- TODO - APIs to call APIConnect - -- TODO - or new API for asynchronous connection? keep APIConnect for legacy links? - APIConnectPreparedContact contactId msgContent_ -> withUser $ \user -> do - -- TODO [short links] connect to prepared contact - -- TODO - for "invitation" contact: - -- TODO - optional message to be sent on successful "sender secure"? - -- TODO - call APIConnect, wait for synchronous (successful) response? - -- TODO - or persist message and queue it asynchronously? - -- TODO - rework agent to allow queueing messages for New connections? - -- TODO - for "address" contact: - -- TODO - optional message to be sent in contact request (pass to APIConnect) + -- Alternative to passing incognito to APIConnectPreparedContact, APIConnectPreparedGroup would be to + -- create new APIs to set incognito on entity - APISetContactIncognito, APISetGroupIncognito. + -- It would be more complex: + -- - would require to persist incognito profile on entity opposing to connection as currently, + -- - would require decomposing part of APIConnect. + -- As it's an edge case / not a big issue that it's not persisted like a change of user, + -- we're simply passing it to prepare here. + APIConnectPreparedContact contactId incognito msgContent_ -> withUser $ \user@User {userId} -> do + ct@Contact {connLinkToConnect} <- withFastStore $ \db -> getContact db vr user contactId + case connLinkToConnect of + Nothing -> throwCmdError "contact doesn't have link to connect" + Just link -> case link of + (ACCL SCMInvitation ccLink) -> + connectViaInvitation user incognito ccLink (Just contactId) >>= \case + CRSentConfirmation {} -> do + -- get updated contact with connection + ct' <- withFastStore $ \db -> getContact db vr user contactId + forM_ msgContent_ $ \mc -> do + let evt = XMsgNew $ MCSimple (extMsgContent mc Nothing) + (msg, _) <- sendDirectContactMessage user ct' evt + ci <- saveSndChatItem user (CDDirectSnd ct') msg (CISndMsgContent mc) + toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct') ci] + pure $ CRStartedConnectionToContact user ct' + cr -> pure cr + (ACCL SCMContact ccLink) -> + connectViaContact user incognito ccLink msgContent_ (Just $ CGMContactId contactId) >>= \case + CRSentInvitation {} -> do + -- get updated contact with connection + ct' <- withFastStore $ \db -> getContact db vr user contactId + forM_ msgContent_ $ \mc -> + createInternalChatItem user (CDDirectSnd ct') (CISndMsgContent mc) Nothing + pure $ CRStartedConnectionToContact user ct' + cr -> pure cr + -- TODO [short links] connect to prepared group + APIConnectPreparedGroup groupId incognito -> withUser $ \user -> do ok_ - APIConnectPreparedGroup groupId -> withUser $ \user -> do - ok_ - APIConnect userId incognito (Just (ACCL SCMInvitation (CCLink cReq@(CRInvitationUri crData e2e) sLnk_))) mc_ -> withUserId userId $ \user -> withInvitationLock "connect" (strEncode cReq) . procCmd $ do + APIConnect userId incognito (Just (ACCL SCMInvitation ccLink)) mc_ -> withUserId userId $ \user -> do when (isJust mc_) $ throwChatError CEConnReqMessageProhibited - subMode <- chatReadVar subscriptionMode - -- [incognito] generate profile to send - incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing - let profileToSend = userProfileToSend user incognitoProfile Nothing False - lift (withAgent' $ \a -> connRequestPQSupport a PQSupportOn cReq) >>= \case - Nothing -> throwChatError CEInvalidConnReq - -- TODO PQ the error above should be CEIncompatibleConnReqVersion, also the same API should be called in Plan - Just (agentV, pqSup') -> do - let chatV = agentToChatVersion agentV - dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend - -- TODO [short links] use short link data on connection: - -- TODO - new connection (Nothing) is only for legacy links - -- TODO - existing contact is new normal (allow existing connection to have contact or change approach) - withFastStore' (\db -> getConnectionEntityByConnReq db vr user cReqs) >>= \case - Nothing -> joinNewConn chatV dm - Just (RcvDirectMsgConnection conn@Connection {connId, connStatus, contactConnInitiated} Nothing) - | connStatus == ConnNew && contactConnInitiated -> joinNewConn chatV dm -- own connection link - | connStatus == ConnPrepared -> do - -- retrying join after error - pcc <- withFastStore $ \db -> getPendingContactConnection db userId connId - joinPreparedConn (aConnId conn) pcc dm - Just ent -> throwCmdError $ "connection exists: " <> show (connEntityInfo ent) - where - joinNewConn chatV dm = do - connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup' - let ccLink = CCLink cReq $ serverShortLink <$> sLnk_ - pcc <- withFastStore' $ \db -> createDirectConnection db user connId ccLink ConnPrepared (incognitoProfile $> profileToSend) subMode chatV pqSup' - joinPreparedConn connId pcc dm - joinPreparedConn connId pcc@PendingContactConnection {pccConnId} dm = do - void $ withAgent $ \a -> joinConnection a (aUserId user) connId True cReq dm pqSup' subMode - withFastStore' $ \db -> updateConnectionStatusFromTo db pccConnId ConnPrepared ConnJoined - pure $ CRSentConfirmation user pcc {pccConnStatus = ConnJoined} - cReqs = - ( CRInvitationUri crData {crScheme = SSSimplex} e2e, - CRInvitationUri crData {crScheme = simplexChat} e2e - ) - APIConnect userId incognito (Just (ACCL SCMContact ccLink)) mc_ -> withUserId userId $ \user -> connectViaContact user incognito ccLink mc_ + connectViaInvitation user incognito ccLink Nothing + APIConnect userId incognito (Just (ACCL SCMContact ccLink)) mc_ -> withUserId userId $ \user -> + connectViaContact user incognito ccLink mc_ Nothing APIConnect _ _ Nothing _ -> throwChatError CEInvalidConnReq Connect incognito (Just cLink@(ACL m cLink')) -> withUser $ \user -> do (ccLink, plan) <- connectPlan user cLink `catchChatError` \e -> case cLink' of CLFull cReq -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink (ILPOk Nothing)); _ -> throwError e @@ -1828,7 +1826,34 @@ processChatCommand' vr = \case processChatCommand $ APIListContacts userId APICreateMyAddress userId short -> withUserId userId $ \user -> procCmd $ do subMode <- chatReadVar subscriptionMode - let userData = shortLinkUserData short + -- TODO [short links] incognito interaction with contact address + -- TODO - problems: + -- TODO 1 now that user profile is advertised in short link, giving an option to + -- TODO share incognito profile on accept doesn't make sense. + -- TODO 2 even advertising a random incognito profile in short link is somewhat broken, + -- TODO as it would be the same for all connecting clients, but then changed on accept (in current implementation), + -- TODO so it would be clear it's incognito profile; it might as well be clearly a placeholder + -- TODO (e.g. "Hidden profile" in profile name), and to avoid distinguishing incognito and main profiles, + -- TODO it can be a setting that can be set for main profile too. + -- TODO 3 changing short link data from main to incognito profile also defeats the purpose of incognito profile, + -- TODO as by scanning the short link in the past, connecting clients may know main profile (even if it's + -- TODO currently set as incognito). + -- TODO - some possibilities: + -- TODO 1 always base choice on autoAccept -> acceptIncognito choice, don't give option on user accept action + -- TODO 2 in this case, replace short link user data on change of this setting? (doesn't solve problem 3) + -- TODO 3 give setting to include "Hidden profile" in short link, even if autoAccept is not set to incognito + -- TODO 4 share same random profile on each accept, this way connecting clients technically + -- TODO wouldn't be able to distinguish incognito profile (placeholder problem); + -- TODO 5 only give choice to accept with main or incognito profile if random profile or placeholder is shared + -- TODO in short link user data; if main profile is shared, don't give choice + -- TODO 6 don't allow to change from main profile in short link to "Hidden profile" (or incognito autoAccept), + -- TODO only allow to change vice versa, in one direction (solves problem 3) + -- TODO 7 remove incognito functionality from address altogether + -- TODO - it seems measures 3, 5, 6 are the most reasonable, or removing incognito functionality for addresses altogether + let userData = + if short + then Just $ encodeShortLinkData (ContactShortLinkData (userProfileToSend user Nothing Nothing False) Nothing) + else Nothing (connId, ccLink) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact userData Nothing IKPQOn subMode ccLink' <- shortenCreatedLink ccLink withFastStore $ \db -> createUserContactLink db user connId ccLink' subMode @@ -1853,11 +1878,14 @@ processChatCommand' vr = \case ShowMyAddress -> withUser' $ \User {userId} -> processChatCommand $ APIShowMyAddress userId APIAddMyAddressShortLink userId -> withUserId' userId $ \user -> do - (ucl@UserContactLink {connLinkContact = CCLink connFullLink sLnk_}, conn) <- + (ucl@UserContactLink {connLinkContact = CCLink connFullLink sLnk_, autoAccept}, conn) <- withFastStore $ \db -> (,) <$> getUserAddress db user <*> getUserAddressConnection db vr user when (isJust sLnk_) $ throwCmdError "address already has short link" - -- TODO [short links] set ContactShortLinkData - sLnk <- shortenShortLink' =<< withAgent (\a -> setConnShortLink a (aConnId conn) SCMContact "" Nothing) + -- TODO [short links] allow to add short link without data if autoAccept was set to incognito? + let shortLinkProfile = userProfileToSend user Nothing Nothing False + shortLinkMsg = autoAccept >>= autoReply >>= (Just . msgContentText) + userData = encodeShortLinkData (ContactShortLinkData shortLinkProfile shortLinkMsg) + sLnk <- shortenShortLink' =<< withAgent (\a -> setConnShortLink a (aConnId conn) SCMContact userData Nothing) case entityId conn of Just uclId -> do withFastStore' $ \db -> setUserContactLinkShortLink db uclId sLnk @@ -1875,6 +1903,7 @@ processChatCommand' vr = \case SetProfileAddress onOff -> withUser $ \User {userId} -> processChatCommand $ APISetProfileAddress userId onOff APIAddressAutoAccept userId autoAccept_ -> withUserId userId $ \user -> do + -- TODO [short links] update adress short link data if message changed forM_ autoAccept_ $ \AutoAccept {businessAddress, acceptIncognito} -> when (businessAddress && acceptIncognito) $ throwCmdError "requests to business address cannot be accepted incognito" contactLink <- withFastStore (\db -> updateUserAddressAutoAccept db user autoAccept_) @@ -2419,6 +2448,7 @@ processChatCommand' vr = \case processChatCommand $ APIListGroups userId (contactId' <$> ct_) search_ APIUpdateGroupProfile groupId p' -> withUser $ \user -> do g <- withFastStore $ \db -> getGroup db vr user groupId + -- TODO [short links] update group link short link data runUpdateGroupProfile user g p' UpdateGroupNames gName GroupProfile {displayName, fullName} -> updateGroupProfileByName gName $ \p -> p {displayName, fullName} @@ -2429,13 +2459,16 @@ processChatCommand' vr = \case ShowGroupDescription gName -> withUser $ \user -> CRGroupDescription user <$> withFastStore (\db -> getGroupInfoByName db vr user gName) APICreateGroupLink groupId mRole short -> withUser $ \user -> withGroupLock "createGroupLink" groupId $ do - gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId + gInfo@GroupInfo {groupProfile} <- withFastStore $ \db -> getGroupInfo db vr user groupId assertUserGroupRole gInfo GRAdmin when (mRole > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole groupLinkId <- GroupLinkId <$> drgRandomBytes 16 subMode <- chatReadVar subscriptionMode - let crClientData = encodeJSON $ CRDataGroup groupLinkId - userData = shortLinkUserData short + let userData = + if short + then Just $ encodeShortLinkData (GroupShortLinkData groupProfile) + else Nothing + crClientData = encodeJSON $ CRDataGroup groupLinkId (connId, ccLink) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact userData (Just crClientData) IKPQOff subMode ccLink' <- createdGroupLink <$> shortenCreatedLink ccLink withFastStore $ \db -> createGroupLink db user gInfo connId ccLink' groupLinkId mRole subMode @@ -2462,9 +2495,10 @@ processChatCommand' vr = \case conn <- getGroupLinkConnection db vr user gInfo pure (gInfo, gLink, conn) when (isJust sLnk_) $ throwCmdError "group link already has short link" - let crClientData = encodeJSON $ CRDataGroup gLinkId - -- TODO [short links] set GroupShortLinkData - sLnk <- shortenShortLink' =<< toShortGroupLink <$> withAgent (\a -> setConnShortLink a (aConnId conn) SCMContact "" (Just crClientData)) + let GroupInfo {groupProfile} = gInfo + userData = encodeShortLinkData (GroupShortLinkData groupProfile) + crClientData = encodeJSON $ CRDataGroup gLinkId + sLnk <- shortenShortLink' . toShortGroupLink =<< withAgent (\a -> setConnShortLink a (aConnId conn) SCMContact userData (Just crClientData)) withFastStore' $ \db -> setUserContactLinkShortLink db uclId sLnk let groupLink' = CCLink connFullLink (Just sLnk) pure $ CRGroupLink user gInfo groupLink' mRole @@ -2839,8 +2873,47 @@ processChatCommand' vr = \case CTGroup -> withFastStore $ \db -> getGroupChatItemIdByText' db user cId msg CTLocal -> withFastStore $ \db -> getLocalChatItemIdByText' db user cId msg _ -> throwCmdError "not supported" - connectViaContact :: User -> IncognitoEnabled -> CreatedLinkContact -> Maybe MsgContent -> CM ChatResponse - connectViaContact user@User {userId} incognito (CCLink cReq@(CRContactUri ConnReqUriData {crClientData}) sLnk) mc_ = withInvitationLock "connectViaContact" (strEncode cReq) $ do + connectViaInvitation :: User -> IncognitoEnabled -> CreatedLinkInvitation -> Maybe ContactId -> CM ChatResponse + connectViaInvitation user@User {userId} incognito (CCLink cReq@(CRInvitationUri crData e2e) sLnk_) contactId_ = + withInvitationLock "connect" (strEncode cReq) . procCmd $ do + subMode <- chatReadVar subscriptionMode + -- [incognito] generate profile to send + incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing + let profileToSend = userProfileToSend user incognitoProfile Nothing False + lift (withAgent' $ \a -> connRequestPQSupport a PQSupportOn cReq) >>= \case + Nothing -> throwChatError CEInvalidConnReq + -- TODO PQ the error above should be CEIncompatibleConnReqVersion, also the same API should be called in Plan + Just (agentV, pqSup') -> do + let chatV = agentToChatVersion agentV + dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend + withFastStore' (\db -> getConnectionEntityByConnReq db vr user cReqs) >>= \case + Nothing -> joinNewConn chatV dm + Just (RcvDirectMsgConnection conn@Connection {connId, connStatus, contactConnInitiated} _ct_) + | connStatus == ConnNew && contactConnInitiated -> joinNewConn chatV dm -- own connection link + | connStatus == ConnPrepared -> do + -- retrying join after error + pcc <- withFastStore $ \db -> getPendingContactConnection db userId connId + joinPreparedConn (aConnId conn) pcc dm + Just ent -> throwCmdError $ "connection is not RcvDirectMsgConnection: " <> show (connEntityInfo ent) + where + joinNewConn chatV dm = do + connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup' + let ccLink = CCLink cReq $ serverShortLink <$> sLnk_ + pcc <- withFastStore' $ \db -> createDirectConnection db user connId ccLink contactId_ ConnPrepared (incognitoProfile $> profileToSend) subMode chatV pqSup' + joinPreparedConn connId pcc dm + joinPreparedConn connId pcc@PendingContactConnection {pccConnId} dm = do + sqSecured <- withAgent $ \a -> joinConnection a (aUserId user) connId True cReq dm pqSup' subMode + let newStatus = if sqSecured then ConnSndReady else ConnJoined + withFastStore' $ \db -> updateConnectionStatusFromTo db pccConnId ConnPrepared newStatus + pure $ CRSentConfirmation user pcc {pccConnStatus = newStatus} + cReqs = + ( CRInvitationUri crData {crScheme = SSSimplex} e2e, + CRInvitationUri crData {crScheme = simplexChat} e2e + ) + -- TODO [short links] Maybe Int64 should be Maybe to differentiate between contact and group links; + -- TODO link connection to entity in createConnReqConnection + connectViaContact :: User -> IncognitoEnabled -> CreatedLinkContact -> Maybe MsgContent -> Maybe ContactOrGroupMemberId -> CM ChatResponse + connectViaContact user@User {userId} incognito (CCLink cReq@(CRContactUri ConnReqUriData {crClientData}) sLnk) mc_ comId_ = withInvitationLock "connectViaContact" (strEncode cReq) $ do let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq case groupLinkId of @@ -2872,7 +2945,7 @@ processChatCommand' vr = \case incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing subMode <- chatReadVar subscriptionMode let sLnk' = serverShortLink <$> sLnk - conn@PendingContactConnection {pccConnId} <- withFastStore' $ \db -> createConnReqConnection db userId connId cReqHash sLnk' xContactId incognitoProfile groupLinkId subMode chatV pqSup + conn@PendingContactConnection {pccConnId} <- withFastStore' $ \db -> createConnReqConnection db userId connId cReqHash sLnk' comId_ xContactId incognitoProfile groupLinkId subMode chatV pqSup joinContact user pccConnId connId cReq incognitoProfile xContactId mc_ inGroup pqSup chatV pure $ CRSentInvitation user conn incognitoProfile connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> CreatedLinkContact -> CM ChatResponse @@ -3211,11 +3284,12 @@ processChatCommand' vr = \case CLShort l -> do let l' = serverShortLink l withFastStore' (\db -> getConnectionEntityViaShortLink db vr user l') >>= \case - Just (cReq, ent) -> - (ACCL SCMInvitation (CCLink cReq (Just l')),) <$> (invitationEntityPlan ent `catchChatError` (pure . CPError)) + Just (cReq, ent) -> do + plan <- invitationEntityPlan Nothing ent `catchChatError` (pure . CPError) + pure (ACCL SCMInvitation (CCLink cReq (Just l')), plan) Nothing -> do (cReq, cData) <- getShortLinkConnReq user l' - let contactSLinkData_ = decodeJSON . safeDecodeUtf8 $ linkUserData cData + let contactSLinkData_ = decodeShortLinkData $ linkUserData cData invitationReqAndPlan cReq (Just l') contactSLinkData_ where invitationReqAndPlan cReq sLnk_ contactSLinkData_ = do @@ -3233,7 +3307,7 @@ processChatCommand' vr = \case Just (UserContactLink (CCLink cReq _) _) -> pure (ACCL SCMContact $ CCLink cReq (Just l'), CPContactAddress CAPOwnLink) Nothing -> do (cReq, cData) <- getShortLinkConnReq user l' - let contactSLinkData_ = decodeJSON . safeDecodeUtf8 $ linkUserData cData + let contactSLinkData_ = decodeShortLinkData $ linkUserData cData plan <- contactRequestPlan user cReq contactSLinkData_ pure (ACCL SCMContact $ CCLink cReq (Just l'), plan) CCTGroup -> @@ -3241,7 +3315,7 @@ processChatCommand' vr = \case Just (cReq, g) -> pure (ACCL SCMContact $ CCLink cReq (Just l'), CPGroupLink (GLPOwnLink g)) Nothing -> do (cReq, cData) <- getShortLinkConnReq user l' - let groupSLinkData_ = decodeJSON . safeDecodeUtf8 $ linkUserData cData + let groupSLinkData_ = decodeShortLinkData $ linkUserData cData plan <- groupJoinRequestPlan user cReq groupSLinkData_ pure (ACCL SCMContact $ CCLink cReq (Just l'), plan) CCTChannel -> throwCmdError "channel links are not supported in this version" @@ -3258,29 +3332,23 @@ processChatCommand' vr = \case invitationRequestPlan user cReq contactSLinkData_ = do withFastStore' (\db -> getConnectionEntityByConnReq db vr user $ invCReqSchemas cReq) >>= \case Nothing -> pure $ CPInvitationLink (ILPOk contactSLinkData_) - Just ent -> invitationEntityPlan ent + Just ent -> invitationEntityPlan contactSLinkData_ ent where invCReqSchemas :: ConnReqInvitation -> (ConnReqInvitation, ConnReqInvitation) invCReqSchemas (CRInvitationUri crData e2e) = ( CRInvitationUri crData {crScheme = SSSimplex} e2e, CRInvitationUri crData {crScheme = simplexChat} e2e ) - invitationEntityPlan :: ConnectionEntity -> CM ConnectionPlan - invitationEntityPlan = \case - RcvDirectMsgConnection Connection {connStatus = ConnPrepared} Nothing -> - -- TODO [short links] entity is already found - passing ContactShortLinkData doesn't make sense? - pure $ CPInvitationLink (ILPOk Nothing) - RcvDirectMsgConnection conn ct_ -> do - let Connection {connStatus, contactConnInitiated} = conn - if - | connStatus == ConnNew && contactConnInitiated -> - pure $ CPInvitationLink ILPOwnLink - -- TODO [short links] check status (now present contact may mean scanned, not only connecting) - | not (connReady conn) -> - pure $ CPInvitationLink (ILPConnecting ct_) - | otherwise -> case ct_ of - Just ct -> pure $ CPInvitationLink (ILPKnown ct) - Nothing -> throwChatError $ CEInternalError "ready RcvDirectMsgConnection connection should have associated contact" + invitationEntityPlan :: Maybe ContactShortLinkData -> ConnectionEntity -> CM ConnectionPlan + invitationEntityPlan contactSLinkData_ = \case + RcvDirectMsgConnection Connection {connStatus, contactConnInitiated} ct_ -> case ct_ of + Just ct + | contactActive ct -> pure $ CPInvitationLink (ILPKnown ct) + | otherwise -> pure $ CPInvitationLink (ILPOk contactSLinkData_) + Nothing + | connStatus == ConnNew && contactConnInitiated -> pure $ CPInvitationLink ILPOwnLink + | connStatus == ConnPrepared -> pure $ CPInvitationLink (ILPOk contactSLinkData_) + | otherwise -> pure $ CPInvitationLink (ILPConnecting Nothing) _ -> throwCmdError "found connection entity is not RcvDirectMsgConnection" contactOrGroupRequestPlan :: User -> ConnReqContact -> CM ConnectionPlan contactOrGroupRequestPlan user cReq@(CRContactUri crData) = do @@ -3357,8 +3425,19 @@ processChatCommand' vr = \case CSLInvitation _ srv lnkId linkKey -> CSLInvitation SLSServer srv lnkId linkKey CSLContact _ ct srv linkKey -> CSLContact SLSServer ct srv linkKey restoreShortLink' l = (`restoreShortLink` l) <$> asks (shortLinkPresetServers . config) - -- TODO [short links] pass encoded ContactShortLinkData or GroupShortLinkData - shortLinkUserData short = if short then Just "" else Nothing + encodeShortLinkData :: J.ToJSON a => a -> ByteString + encodeShortLinkData = encodeUtf8 . encodeJSON + decodeShortLinkData :: J.FromJSON a => ByteString -> Maybe a + decodeShortLinkData = decodeJSON . safeDecodeUtf8 + updatePCCShortLinkData :: J.ToJSON a => PendingContactConnection -> a -> CM (Maybe ShortLinkInvitation) + updatePCCShortLinkData conn@PendingContactConnection {connLinkInv} shortLinkData = do + let short = isJust $ connShortLink =<< connLinkInv + if short + then do + let userData = encodeShortLinkData shortLinkData + sLnk <- shortenShortLink' =<< withAgent (\a -> setConnShortLink a (aConnId' conn) SCMInvitation userData Nothing) + pure $ Just sLnk + else pure Nothing shortenShortLink' :: ConnShortLink m -> CM (ConnShortLink m) shortenShortLink' l = (`shortenShortLink` l) <$> asks (shortLinkPresetServers . config) shortenCreatedLink :: CreatedConnLink m -> CM (CreatedConnLink m) @@ -4321,12 +4400,12 @@ chatCommandP = "/_contacts " *> (APIListContacts <$> A.decimal), "/contacts" $> ListContacts, "/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> strP), - "/_prepare contact" *> (APIPrepareContact <$> A.decimal <* A.space <*> jsonP <* A.space <*> connLinkP), - "/_prepare group" *> (APIPrepareGroup <$> A.decimal <* A.space <*> jsonP <* A.space <*> connLinkP), + "/_prepare contact " *> (APIPrepareContact <$> A.decimal <* A.space <*> connLinkP <* A.space <*> jsonP), + "/_prepare group " *> (APIPrepareGroup <$> A.decimal <* A.space <*> connLinkP <* A.space <*> jsonP), "/_set contact user @" *> (APIChangeContactUser <$> A.decimal <* A.space <*> A.decimal), "/_set group user #" *> (APIChangeGroupUser <$> A.decimal <* A.space <*> A.decimal), - "/_connect contact @" *> (APIConnectPreparedContact <$> A.decimal <*> optional (A.space *> msgContentP)), - "/_connect group $" *> (APIConnectPreparedGroup <$> A.decimal), + "/_connect contact @" *> (APIConnectPreparedContact <$> A.decimal <*> incognitoOnOffP <*> optional (A.space *> msgContentP)), + "/_connect group #" *> (APIConnectPreparedGroup <$> A.decimal <*> incognitoOnOffP), "/_connect " *> (APIAddContact <$> A.decimal <*> shortOnOffP <*> incognitoOnOffP), "/_connect " *> (APIConnect <$> A.decimal <*> incognitoOnOffP <* A.space <*> connLinkP_ <*> optional (A.space *> msgContentP)), "/_set incognito :" *> (APISetConnectionIncognito <$> A.decimal <* A.space <*> onOffP), diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 0180117e1f..9d8816d5ac 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -667,6 +667,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- TODO add debugging output _ -> pure () where + -- TODO [short links] don't send auto-reply message if it should have been created by connecting client + -- TODO (based on version + whether address has short link data) sendAutoReply ct = \case Just AutoAccept {autoReply = Just mc} -> do (msg, _) <- sendDirectContactMessage user ct (XMsgNew $ MCSimple (extMsgContent mc Nothing)) diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index 836b6c07d0..15e53cd162 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -112,7 +112,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do [sql| SELECT c.contact_profile_id, c.local_display_name, c.via_group, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, - p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_req_to_connect, + p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.contact_group_member_id, c.contact_grp_inv_sent, c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl FROM contacts c JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id @@ -120,12 +120,13 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do |] (userId, contactId) toContact' :: Int64 -> Connection -> [ChatTagId] -> ContactRow' -> Contact - toContact' contactId conn chatTags ((profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. (connReqToConnect, contactGroupMemberId, BI contactGrpInvSent, uiThemes, BI chatDeleted, customData, chatItemTTL)) = + toContact' contactId conn chatTags ((profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. (connFullLink, connShortLink, contactGroupMemberId, BI contactGrpInvSent, uiThemes, BI chatDeleted, customData, chatItemTTL)) = let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn activeConn = Just conn - in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, connReqToConnect, contactGroupMemberId, contactGrpInvSent, chatTags, chatItemTTL, uiThemes, chatDeleted, customData} + connLinkToConnect = toACreatedConnLink_ connFullLink connShortLink + in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, connLinkToConnect, contactGroupMemberId, contactGrpInvSent, chatTags, chatItemTTL, uiThemes, chatDeleted, customData} getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember) getGroupAndMember_ groupMemberId c = do gm <- @@ -138,7 +139,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_req_to_connect, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupInfo {membership} diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 2f9dbaf708..4d5dc92de1 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -30,6 +30,7 @@ module Simplex.Chat.Store.Direct getProfileById, getConnReqContactXContactId, getContactByConnReqHash, + createPreparedContact, createDirectContact, deleteContactConnections, deleteContactFiles, @@ -100,7 +101,7 @@ import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.UITheme -import Simplex.Messaging.Agent.Protocol (ConnId, CreatedConnLink (..), InvitationId, UserId) +import Simplex.Messaging.Agent.Protocol (ACreatedConnLink, ConnId, CreatedConnLink (..), InvitationId, UserId) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow) import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -150,12 +151,11 @@ deletePendingContactConnection db userId connId = createAddressContactConnection :: DB.Connection -> VersionRangeChat -> User -> Contact -> ConnId -> ConnReqUriHash -> Maybe ShortLinkContact -> XContactId -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> ExceptT StoreError IO (Int64, Contact) createAddressContactConnection db vr user@User {userId} Contact {contactId} acId cReqHash sLnk xContactId incognitoProfile subMode chatV pqSup = do - PendingContactConnection {pccConnId} <- liftIO $ createConnReqConnection db userId acId cReqHash sLnk xContactId incognitoProfile Nothing subMode chatV pqSup - liftIO $ DB.execute db "UPDATE connections SET contact_id = ? WHERE connection_id = ?" (contactId, pccConnId) + PendingContactConnection {pccConnId} <- liftIO $ createConnReqConnection db userId acId cReqHash sLnk (Just $ CGMContactId contactId) xContactId incognitoProfile Nothing subMode chatV pqSup (pccConnId,) <$> getContact db vr user contactId -createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> Maybe ShortLinkContact -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection -createConnReqConnection db userId acId cReqHash sLnk xContactId incognitoProfile groupLinkId subMode chatV pqSup = do +createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> Maybe ShortLinkContact -> Maybe ContactOrGroupMemberId -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection +createConnReqConnection db userId acId cReqHash sLnk comId_ xContactId incognitoProfile groupLinkId subMode chatV pqSup = do createdAt <- getCurrentTime customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile let pccConnStatus = ConnJoined @@ -164,16 +164,23 @@ createConnReqConnection db userId acId cReqHash sLnk xContactId incognitoProfile [sql| INSERT INTO connections ( user_id, agent_conn_id, conn_status, conn_type, contact_conn_initiated, - via_contact_uri_hash, via_short_link_contact, xcontact_id, custom_user_profile_id, via_group_link, group_link_id, + via_contact_uri_hash, via_short_link_contact, contact_id, group_member_id, + xcontact_id, custom_user_profile_id, via_group_link, group_link_id, created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (userId, acId, pccConnStatus, ConnContact, BI True, cReqHash, sLnk, xContactId) - :. (customUserProfileId, BI (isJust groupLinkId), groupLinkId) + ( (userId, acId, pccConnStatus, ConnContact, BI True) + :. (cReqHash, sLnk, contactId_, groupMemberId_) + :. (xContactId, customUserProfileId, BI (isJust groupLinkId), groupLinkId) :. (createdAt, createdAt, BI (subMode == SMOnlyCreate), chatV, pqSup, pqSup) ) pccConnId <- insertedRowId db pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, groupLinkId, customUserProfileId, connLinkInv = Nothing, localAlias = "", createdAt, updatedAt = createdAt} + where + (contactId_, groupMemberId_) = case comId_ of + Just (CGMContactId ctId) -> (Just ctId, Nothing) + Just (CGMGroupMemberId gmId) -> (Nothing, Just gmId) + Nothing -> (Nothing, Nothing) getConnReqContactXContactId :: DB.Connection -> VersionRangeChat -> User -> ConnReqUriHash -> IO (Maybe Contact, Maybe XContactId) getConnReqContactXContactId db vr user@User {userId} cReqHash = do @@ -199,7 +206,7 @@ getContactByConnReqHash db vr user@User {userId} cReqHash = do SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_req_to_connect, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, @@ -215,8 +222,8 @@ getContactByConnReqHash db vr user@User {userId} cReqHash = do (userId, cReqHash, CSActive) mapM (addDirectChatTags db) ct_ -createDirectConnection :: DB.Connection -> User -> ConnId -> CreatedLinkInvitation -> ConnStatus -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection -createDirectConnection db User {userId} acId ccLink@(CCLink cReq shortLinkInv) pccConnStatus incognitoProfile subMode chatV pqSup = do +createDirectConnection :: DB.Connection -> User -> ConnId -> CreatedLinkInvitation -> Maybe ContactId -> ConnStatus -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection +createDirectConnection db User {userId} acId ccLink@(CCLink cReq shortLinkInv) contactId_ pccConnStatus incognitoProfile subMode chatV pqSup = do createdAt <- getCurrentTime customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile let contactConnInitiated = pccConnStatus == ConnNew @@ -224,11 +231,11 @@ createDirectConnection db User {userId} acId ccLink@(CCLink cReq shortLinkInv) p db [sql| INSERT INTO connections - (user_id, agent_conn_id, conn_req_inv, short_link_inv, conn_status, conn_type, contact_conn_initiated, custom_user_profile_id, + (user_id, agent_conn_id, conn_req_inv, short_link_inv, conn_status, conn_type, contact_id, contact_conn_initiated, custom_user_profile_id, created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (userId, acId, cReq, shortLinkInv, pccConnStatus, ConnContact, BI contactConnInitiated, customUserProfileId) + ( (userId, acId, cReq, shortLinkInv, pccConnStatus, ConnContact, contactId_, BI contactConnInitiated, customUserProfileId) :. (createdAt, createdAt, BI (subMode == SMOnlyCreate), chatV, pqSup, pqSup) ) pccConnId <- insertedRowId db @@ -239,10 +246,42 @@ createIncognitoProfile db User {userId} p = do createdAt <- getCurrentTime createIncognitoProfile_ db userId createdAt p +createPreparedContact :: DB.Connection -> User -> Profile -> ACreatedConnLink -> ExceptT StoreError IO Contact +createPreparedContact db user@User {userId} p@Profile {preferences} connLinkToConnect = do + currentTs <- liftIO getCurrentTime + (localDisplayName, contactId, profileId) <- createContact_ db userId p (Just connLinkToConnect) "" Nothing currentTs + let profile = toLocalProfile profileId p "" + userPreferences = emptyChatPrefs + mergedPreferences = contactUserPreferences user userPreferences preferences False + pure $ + Contact + { contactId, + localDisplayName, + profile, + activeConn = Nothing, + viaGroup = Nothing, + contactUsed = True, + contactStatus = CSActive, + chatSettings = defaultChatSettings, + userPreferences, + mergedPreferences, + createdAt = currentTs, + updatedAt = currentTs, + chatTs = Just currentTs, + connLinkToConnect = Just connLinkToConnect, + contactGroupMemberId = Nothing, + contactGrpInvSent = False, + chatTags = [], + chatItemTTL = Nothing, + uiThemes = Nothing, + chatDeleted = False, + customData = Nothing + } + createDirectContact :: DB.Connection -> User -> Connection -> Profile -> ExceptT StoreError IO Contact createDirectContact db user@User {userId} conn@Connection {connId, localAlias} p@Profile {preferences} = do currentTs <- liftIO getCurrentTime - (localDisplayName, contactId, profileId) <- createContact_ db userId p localAlias Nothing currentTs + (localDisplayName, contactId, profileId) <- createContact_ db userId p Nothing localAlias Nothing currentTs liftIO $ DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, currentTs, connId) let profile = toLocalProfile profileId p localAlias userPreferences = emptyChatPrefs @@ -262,7 +301,7 @@ createDirectContact db user@User {userId} conn@Connection {connId, localAlias} p createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, - connReqToConnect = Nothing, + connLinkToConnect = Nothing, contactGroupMemberId = Nothing, contactGrpInvSent = False, chatTags = [], @@ -429,31 +468,39 @@ updateContactConnectionAlias db userId conn localAlias = do (localAlias, updatedAt, userId, pccConnId conn) pure (conn :: PendingContactConnection) {localAlias, updatedAt} -updatePCCIncognito :: DB.Connection -> User -> PendingContactConnection -> Maybe ProfileId -> IO PendingContactConnection -updatePCCIncognito db User {userId} conn customUserProfileId = do +updatePCCIncognito :: DB.Connection -> User -> PendingContactConnection -> Maybe ProfileId -> Maybe ShortLinkInvitation -> IO PendingContactConnection +updatePCCIncognito db User {userId} conn@PendingContactConnection {connLinkInv} customUserProfileId sLnk = do updatedAt <- getCurrentTime DB.execute db [sql| UPDATE connections - SET custom_user_profile_id = ?, updated_at = ? + SET custom_user_profile_id = ?, short_link_inv = ?, updated_at = ? WHERE user_id = ? AND connection_id = ? |] - (customUserProfileId, updatedAt, userId, pccConnId conn) - pure (conn :: PendingContactConnection) {customUserProfileId, updatedAt} + (customUserProfileId, sLnk, updatedAt, userId, pccConnId conn) + pure (conn :: PendingContactConnection) {customUserProfileId, connLinkInv = connLinkInv', updatedAt} + where + connLinkInv' = case connLinkInv of + Just (CCLink cReq _) -> Just (CCLink cReq sLnk) + Nothing -> Nothing -updatePCCUser :: DB.Connection -> UserId -> PendingContactConnection -> UserId -> IO PendingContactConnection -updatePCCUser db userId conn newUserId = do +updatePCCUser :: DB.Connection -> UserId -> PendingContactConnection -> UserId -> Maybe ShortLinkInvitation -> IO PendingContactConnection +updatePCCUser db userId conn@PendingContactConnection {connLinkInv} newUserId sLnk = do updatedAt <- getCurrentTime DB.execute db [sql| UPDATE connections - SET user_id = ?, custom_user_profile_id = NULL, updated_at = ? + SET user_id = ?, short_link_inv = ?, custom_user_profile_id = NULL, updated_at = ? WHERE user_id = ? AND connection_id = ? |] - (newUserId, updatedAt, userId, pccConnId conn) - pure (conn :: PendingContactConnection) {customUserProfileId = Nothing, updatedAt} + (newUserId, sLnk, updatedAt, userId, pccConnId conn) + pure (conn :: PendingContactConnection) {customUserProfileId = Nothing, connLinkInv = connLinkInv', updatedAt} + where + connLinkInv' = case connLinkInv of + Just (CCLink cReq _) -> Just (CCLink cReq sLnk) + Nothing -> Nothing deletePCCIncognitoProfile :: DB.Connection -> User -> ProfileId -> IO () deletePCCIncognitoProfile db User {userId} profileId = @@ -652,7 +699,7 @@ createOrUpdateContactRequest db vr user@User {userId, userContactId} userContact SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_req_to_connect, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, @@ -829,7 +876,7 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences} createdAt, updatedAt = createdAt, chatTs = Just createdAt, - connReqToConnect = Nothing, + connLinkToConnect = Nothing, contactGroupMemberId = Nothing, contactGrpInvSent = False, chatTags = [], @@ -869,7 +916,7 @@ getContact_ db vr user@User {userId} contactId deleted = do SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_req_to_connect, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index d6b180da28..38ab9fa53e 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -32,6 +32,7 @@ module Simplex.Chat.Store.Groups createNewGroup, createGroupInvitation, deleteContactCardKeepConn, + createPreparedGroup, createGroupInvitedViaLink, createGroupRejectedViaLink, setViaGroupLinkHash, @@ -163,7 +164,7 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme -import Simplex.Messaging.Agent.Protocol (ConnId, CreatedConnLink (..), UserId) +import Simplex.Messaging.Agent.Protocol (ACreatedConnLink, ConnId, CreatedConnLink (..), UserId) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, fromOnlyBI, maybeFirstRow) import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -283,7 +284,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId vr = do -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_req_to_connect, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupInfo {membership} @@ -365,7 +366,7 @@ createNewGroup db vr gVar user@User {userId} groupProfile incognitoProfile = Exc updatedAt = currentTs, chatTs = Just currentTs, userMemberProfileSentAt = Just currentTs, - connReqToConnect = Nothing, + connLinkToConnect = Nothing, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, @@ -437,7 +438,7 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ updatedAt = currentTs, chatTs = Just currentTs, userMemberProfileSentAt = Just currentTs, - connReqToConnect = Nothing, + connLinkToConnect = Nothing, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, @@ -535,6 +536,24 @@ deleteContactCardKeepConn db connId Contact {contactId, profile = LocalProfile { DB.execute db "DELETE FROM contacts WHERE contact_id = ?" (Only contactId) DB.execute db "DELETE FROM contact_profiles WHERE contact_profile_id = ?" (Only profileId) +createPreparedGroup :: DB.Connection -> VersionRangeChat -> User -> GroupProfile -> ACreatedConnLink -> ExceptT StoreError IO GroupInfo +createPreparedGroup db vr user@User {userId} groupProfile connLinkToConnect = do + currentTs <- liftIO getCurrentTime + -- TODO [short links] support preparing business chats + let business = Nothing + groupId <- createGroup_ db userId groupProfile (Just connLinkToConnect) business currentTs + -- TODO [short links] create "unknown" host member here? set invitedByGroupMemberId later? + -- TODO - same for invitedMember + -- TODO - for membershipStatus - new status GSMemNew? + -- TODO - customUserProfileId - pass on APIConnectPreparedGroup, update member; or separate apis for switching before joining? + let invitedByGroupMemberId = Nothing + invitedMember = MemberIdRole (MemberId "unknown") GRMember + membershipStatus = GSMemAccepted + customUserProfileId = Nothing + void $ createContactMemberInv_ db user groupId invitedByGroupMemberId user invitedMember GCUserMember membershipStatus IBUnknown customUserProfileId currentTs vr + -- TODO [short links] review: setViaGroupLinkHash + getGroupInfo db vr user groupId + createGroupInvitedViaLink :: DB.Connection -> VersionRangeChat -> User -> Connection -> GroupLinkInvitation -> ExceptT StoreError IO (GroupInfo, GroupMember) createGroupInvitedViaLink db vr user conn GroupLinkInvitation {fromMember, fromMemberName, invitedMember, groupProfile, accepted, business} = do let fromMemberProfile = profileFromName fromMemberName @@ -559,7 +578,7 @@ createGroupViaLink' business membershipStatus = do currentTs <- liftIO getCurrentTime - groupId <- insertGroup_ currentTs + groupId <- createGroup_ db userId groupProfile Nothing business currentTs hostMemberId <- insertHost_ currentTs groupId liftIO $ DB.execute db "UPDATE connections SET conn_type = ?, group_member_id = ?, updated_at = ? WHERE connection_id = ?" (ConnMember, hostMemberId, currentTs, connId) -- using IBUnknown since host is created without contact @@ -567,25 +586,6 @@ createGroupViaLink' liftIO $ setViaGroupLinkHash db groupId connId (,) <$> getGroupInfo db vr user groupId <*> getGroupMemberById db vr user hostMemberId where - insertGroup_ currentTs = ExceptT $ do - let GroupProfile {displayName, fullName, description, image, groupPreferences, memberAdmission} = groupProfile - withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do - liftIO $ do - DB.execute - db - "INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, member_admission, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" - (displayName, fullName, description, image, userId, groupPreferences, memberAdmission, currentTs, currentTs) - profileId <- insertedRowId db - DB.execute - db - [sql| - INSERT INTO groups - (group_profile_id, local_display_name, user_id, enable_ntfs, - created_at, updated_at, chat_ts, user_member_profile_sent_at, business_chat, business_member_id, customer_member_id) - VALUES (?,?,?,?,?,?,?,?,?,?,?) - |] - ((profileId, localDisplayName, userId, BI True, currentTs, currentTs, currentTs, currentTs) :. businessChatInfoRow business) - insertedRowId db insertHost_ currentTs groupId = do (localDisplayName, profileId) <- createNewMemberProfile_ db user fromMemberProfile currentTs let MemberIdRole {memberId, memberRole} = fromMember @@ -603,6 +603,28 @@ createGroupViaLink' ) insertedRowId db +createGroup_ :: DB.Connection -> UserId -> GroupProfile -> Maybe ACreatedConnLink -> Maybe BusinessChatInfo -> UTCTime -> ExceptT StoreError IO GroupId +createGroup_ db userId groupProfile connLinkToConnect business currentTs = ExceptT $ do + let GroupProfile {displayName, fullName, description, image, groupPreferences, memberAdmission} = groupProfile + withLocalDisplayName db userId displayName $ \localDisplayName -> runExceptT $ do + liftIO $ do + DB.execute + db + "INSERT INTO group_profiles (display_name, full_name, description, image, user_id, preferences, member_admission, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" + (displayName, fullName, description, image, userId, groupPreferences, memberAdmission, currentTs, currentTs) + profileId <- insertedRowId db + DB.execute + db + [sql| + INSERT INTO groups + (group_profile_id, local_display_name, user_id, enable_ntfs, + created_at, updated_at, chat_ts, user_member_profile_sent_at, conn_full_link_to_connect, conn_short_link_to_connect, + business_chat, business_member_id, customer_member_id) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + |] + ((profileId, localDisplayName, userId, BI True, currentTs, currentTs, currentTs, currentTs) :. connLinkToConnectRow connLinkToConnect :. businessChatInfoRow business) + insertedRowId db + setViaGroupLinkHash :: DB.Connection -> GroupId -> Int64 -> IO () setViaGroupLinkHash db groupId connId = DB.execute @@ -778,7 +800,7 @@ getUserGroupDetails db vr User {userId, userContactId} _contactId_ search_ = do SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_req_to_connect, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, @@ -1638,7 +1660,7 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} = do -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_req_to_connect, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupInfo {membership} @@ -2314,7 +2336,7 @@ createMemberContact quotaErrCounter = 0 } mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn - pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, connReqToConnect = Nothing, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing} + pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, connLinkToConnect = Nothing, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing} getMemberContact :: DB.Connection -> VersionRangeChat -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation) getMemberContact db vr user contactId = do @@ -2351,7 +2373,7 @@ createMemberContactInvited contactId <- createContactUpdateMember currentTs userPreferences ctConn <- createMemberContactConn_ db user connIds gInfo mConn contactId subMode let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn - mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, connReqToConnect = Nothing, contactGroupMemberId = Nothing, contactGrpInvSent = False, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing} + mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, connLinkToConnect = Nothing, contactGroupMemberId = Nothing, contactGrpInvSent = False, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing} m' = m {memberContactId = Just contactId} pure (mCt', m') where diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/M20250526_short_links.hs b/src/Simplex/Chat/Store/SQLite/Migrations/M20250526_short_links.hs index 686e6b7bd6..75f24bfae3 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/M20250526_short_links.hs +++ b/src/Simplex/Chat/Store/SQLite/Migrations/M20250526_short_links.hs @@ -11,13 +11,17 @@ import Database.SQLite.Simple.QQ (sql) m20250526_short_links :: Query m20250526_short_links = [sql| -ALTER TABLE contacts ADD COLUMN conn_req_to_connect BLOB; -ALTER TABLE groups ADD COLUMN conn_req_to_connect BLOB; +ALTER TABLE contacts ADD COLUMN conn_full_link_to_connect BLOB; +ALTER TABLE contacts ADD COLUMN conn_short_link_to_connect BLOB; +ALTER TABLE groups ADD COLUMN conn_full_link_to_connect BLOB; +ALTER TABLE groups ADD COLUMN conn_short_link_to_connect BLOB; |] down_m20250526_short_links :: Query down_m20250526_short_links = [sql| -ALTER TABLE contacts DROP COLUMN conn_req_to_connect; -ALTER TABLE groups DROP COLUMN conn_req_to_connect; +ALTER TABLE contacts DROP COLUMN conn_full_link_to_connect; +ALTER TABLE contacts DROP COLUMN conn_short_link_to_connect; +ALTER TABLE groups DROP COLUMN conn_full_link_to_connect; +ALTER TABLE groups DROP COLUMN conn_short_link_to_connect; |] diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt index d5c2554afd..ce760cfffb 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_query_plans.txt @@ -33,14 +33,6 @@ Query: Plan: -Query: - INSERT INTO groups - (group_profile_id, local_display_name, user_id, enable_ntfs, - created_at, updated_at, chat_ts, user_member_profile_sent_at, business_chat, business_member_id, customer_member_id) - VALUES (?,?,?,?,?,?,?,?,?,?,?) - -Plan: - Query: INSERT INTO groups (group_profile_id, local_display_name, user_id, enable_ntfs, @@ -54,7 +46,7 @@ Query: -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_req_to_connect, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupInfo {membership} @@ -180,7 +172,7 @@ Query: SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_req_to_connect, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, @@ -331,7 +323,7 @@ Plan: Query: SELECT c.contact_profile_id, c.local_display_name, c.via_group, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, - p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_req_to_connect, + p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.contact_group_member_id, c.contact_grp_inv_sent, c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl FROM contacts c JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id @@ -701,6 +693,15 @@ Query: Plan: +Query: + INSERT INTO groups + (group_profile_id, local_display_name, user_id, enable_ntfs, + created_at, updated_at, chat_ts, user_member_profile_sent_at, conn_full_link_to_connect, conn_short_link_to_connect, + business_chat, business_member_id, customer_member_id) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + +Plan: + Query: INSERT INTO groups (local_display_name, user_id, group_profile_id, enable_ntfs, @@ -828,7 +829,7 @@ Query: SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_req_to_connect, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, @@ -852,7 +853,7 @@ Query: -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_req_to_connect, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupInfo {membership} @@ -901,7 +902,7 @@ Query: SELECT g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_req_to_connect, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, mu.group_member_id, g.group_id, mu.member_id, mu.peer_chat_min_version, mu.peer_chat_max_version, mu.member_role, mu.member_category, mu.member_status, mu.show_messages, mu.member_restriction, @@ -1398,7 +1399,7 @@ Query: SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_req_to_connect, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, @@ -4034,9 +4035,9 @@ Plan: Query: INSERT INTO connections - (user_id, agent_conn_id, conn_req_inv, short_link_inv, conn_status, conn_type, contact_conn_initiated, custom_user_profile_id, + (user_id, agent_conn_id, conn_req_inv, short_link_inv, conn_status, conn_type, contact_id, contact_conn_initiated, custom_user_profile_id, created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption) - VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: @@ -4052,9 +4053,10 @@ Plan: Query: INSERT INTO connections ( user_id, agent_conn_id, conn_status, conn_type, contact_conn_initiated, - via_contact_uri_hash, via_short_link_contact, xcontact_id, custom_user_profile_id, via_group_link, group_link_id, + via_contact_uri_hash, via_short_link_contact, contact_id, group_member_id, + xcontact_id, custom_user_profile_id, via_group_link, group_link_id, created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption - ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) Plan: @@ -4259,7 +4261,7 @@ SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE connections - SET custom_user_profile_id = ?, updated_at = ? + SET custom_user_profile_id = ?, short_link_inv = ?, updated_at = ? WHERE user_id = ? AND connection_id = ? Plan: @@ -4307,7 +4309,7 @@ SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) Query: UPDATE connections - SET user_id = ?, custom_user_profile_id = NULL, updated_at = ? + SET user_id = ?, short_link_inv = ?, custom_user_profile_id = NULL, updated_at = ? WHERE user_id = ? AND connection_id = ? Plan: @@ -4578,7 +4580,7 @@ Query: -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_req_to_connect, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupMember - membership @@ -4603,7 +4605,7 @@ Query: -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_req_to_connect, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupMember - membership @@ -5500,7 +5502,7 @@ Query: INSERT INTO contacts (contact_profile_id, local_display_name, user_id, is Plan: SEARCH users USING COVERING INDEX sqlite_autoindex_users_1 (contact_id=?) -Query: INSERT INTO contacts (contact_profile_id, local_display_name, user_id, via_group, created_at, updated_at, chat_ts, contact_used) VALUES (?,?,?,?,?,?,?,?) +Query: INSERT INTO contacts (contact_profile_id, local_display_name, user_id, via_group, created_at, updated_at, chat_ts, contact_used, conn_full_link_to_connect, conn_short_link_to_connect) VALUES (?,?,?,?,?,?,?,?,?,?) Plan: SEARCH users USING COVERING INDEX sqlite_autoindex_users_1 (contact_id=?) @@ -5863,10 +5865,6 @@ Query: UPDATE connections SET conn_type = ?, group_member_id = ?, updated_at = ? Plan: SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) -Query: UPDATE connections SET contact_id = ? WHERE connection_id = ? -Plan: -SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) - Query: UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ? Plan: SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) diff --git a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql index 9f2914a319..dfea7f6909 100644 --- a/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Store/SQLite/Migrations/chat_schema.sql @@ -79,7 +79,8 @@ CREATE TABLE contacts( ui_themes TEXT, chat_deleted INTEGER NOT NULL DEFAULT 0, chat_item_ttl INTEGER, - conn_req_to_connect BLOB, + conn_full_link_to_connect BLOB, + conn_short_link_to_connect BLOB, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -137,7 +138,8 @@ CREATE TABLE groups( chat_item_ttl INTEGER, local_alias TEXT DEFAULT '', members_require_attention INTEGER NOT NULL DEFAULT 0, - conn_req_to_connect BLOB, -- received + conn_full_link_to_connect BLOB, + conn_short_link_to_connect BLOB, -- received FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 5791dbf0d3..f777e94f98 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -2,6 +2,7 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} @@ -36,7 +37,7 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme -import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri, ConnId, ConnShortLink, ConnectionMode (..), CreatedConnLink (..), UserId) +import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri (..), AConnShortLink (..), ACreatedConnLink (..), ConnId, ConnShortLink, ConnectionMode (..), CreatedConnLink (..), SConnectionMode (..), UserId) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow) import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) import qualified Simplex.Messaging.Agent.Store.DB as DB @@ -382,10 +383,10 @@ setCommandConnId db User {userId} cmdId connId = do createContact :: DB.Connection -> User -> Profile -> ExceptT StoreError IO () createContact db User {userId} profile = do currentTs <- liftIO getCurrentTime - void $ createContact_ db userId profile "" Nothing currentTs + void $ createContact_ db userId profile Nothing "" Nothing currentTs -createContact_ :: DB.Connection -> UserId -> Profile -> LocalAlias -> Maybe Int64 -> UTCTime -> ExceptT StoreError IO (Text, ContactId, ProfileId) -createContact_ db userId Profile {displayName, fullName, image, contactLink, preferences} localAlias viaGroup currentTs = +createContact_ :: DB.Connection -> UserId -> Profile -> Maybe ACreatedConnLink -> LocalAlias -> Maybe Int64 -> UTCTime -> ExceptT StoreError IO (Text, ContactId, ProfileId) +createContact_ db userId Profile {displayName, fullName, image, contactLink, preferences} connLinkToConnect localAlias viaGroup currentTs = ExceptT . withLocalDisplayName db userId displayName $ \ldn -> do DB.execute db @@ -394,11 +395,18 @@ createContact_ db userId Profile {displayName, fullName, image, contactLink, pre profileId <- insertedRowId db DB.execute db - "INSERT INTO contacts (contact_profile_id, local_display_name, user_id, via_group, created_at, updated_at, chat_ts, contact_used) VALUES (?,?,?,?,?,?,?,?)" - (profileId, ldn, userId, viaGroup, currentTs, currentTs, currentTs, BI True) + "INSERT INTO contacts (contact_profile_id, local_display_name, user_id, via_group, created_at, updated_at, chat_ts, contact_used, conn_full_link_to_connect, conn_short_link_to_connect) VALUES (?,?,?,?,?,?,?,?,?,?)" + ((profileId, ldn, userId, viaGroup, currentTs, currentTs, currentTs, BI True) :. connLinkToConnectRow connLinkToConnect) contactId <- insertedRowId db pure $ Right (ldn, contactId, profileId) +type ConnLinkToConnectRow = (Maybe AConnectionRequestUri, Maybe AConnShortLink) + +connLinkToConnectRow :: Maybe ACreatedConnLink -> ConnLinkToConnectRow +connLinkToConnectRow = \case + Just (ACCL m (CCLink fullLink shortLink)) -> (Just (ACR m fullLink), ACSL m <$> shortLink) + Nothing -> (Nothing, Nothing) + deleteUnusedIncognitoProfileById_ :: DB.Connection -> User -> ProfileId -> IO () deleteUnusedIncognitoProfileById_ db User {userId} profileId = DB.execute @@ -417,18 +425,28 @@ deleteUnusedIncognitoProfileById_ db User {userId} profileId = |] (userId, profileId, userId, profileId, userId, profileId) -type ContactRow' = (ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. (Maybe AConnectionRequestUri, Maybe GroupMemberId, BoolInt, Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64) +type ContactRow' = (ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. (Maybe AConnectionRequestUri, Maybe AConnShortLink, Maybe GroupMemberId, BoolInt, Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64) type ContactRow = Only ContactId :. ContactRow' toContact :: VersionRangeChat -> User -> [ChatTagId] -> ContactRow :. MaybeConnectionRow -> Contact -toContact vr user chatTags ((Only contactId :. (profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. (connReqToConnect, contactGroupMemberId, BI contactGrpInvSent, uiThemes, BI chatDeleted, customData, chatItemTTL)) :. connRow) = +toContact vr user chatTags ((Only contactId :. (profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. (connFullLink, connShortLink, contactGroupMemberId, BI contactGrpInvSent, uiThemes, BI chatDeleted, customData, chatItemTTL)) :. connRow) = let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} activeConn = toMaybeConnection vr connRow chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} incognito = maybe False connIncognito activeConn mergedPreferences = contactUserPreferences user userPreferences preferences incognito - in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, connReqToConnect, contactGroupMemberId, contactGrpInvSent, chatTags, chatItemTTL, uiThemes, chatDeleted, customData} + connLinkToConnect = toACreatedConnLink_ connFullLink connShortLink + in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, connLinkToConnect, contactGroupMemberId, contactGrpInvSent, chatTags, chatItemTTL, uiThemes, chatDeleted, customData} + +toACreatedConnLink_ :: Maybe AConnectionRequestUri -> Maybe AConnShortLink -> Maybe ACreatedConnLink +toACreatedConnLink_ connFullLink connShortLink = case (connFullLink, connShortLink) of + (Nothing, _) -> Nothing + (Just (ACR m cr), Nothing) -> Just $ ACCL m (CCLink cr Nothing) + (Just (ACR m cr), Just (ACSL m' l)) -> case (m, m') of + (SCMInvitation, SCMInvitation) -> Just $ ACCL SCMInvitation (CCLink cr (Just l)) + (SCMContact, SCMContact) -> Just $ ACCL SCMContact (CCLink cr (Just l)) + _ -> Nothing getProfileById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO LocalProfile getProfileById db userId profileId = @@ -579,18 +597,21 @@ safeDeleteLDN db User {userId} localDisplayName = do type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe MemberId) -type GroupInfoRow = (Int64, GroupName, GroupName, Text, Text, Maybe Text, Maybe ImageData, Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime, Maybe ConnReqContact) :. BusinessChatInfoRow :. (Maybe UIThemeEntityOverrides, Maybe CustomData, Maybe Int64, Int) :. GroupMemberRow +type GroupInfoRow = (Int64, GroupName, GroupName, Text, Text, Maybe Text, Maybe ImageData, Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences, Maybe GroupMemberAdmission) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime, Maybe ConnReqContact, Maybe ShortLinkContact) :. BusinessChatInfoRow :. (Maybe UIThemeEntityOverrides, Maybe CustomData, Maybe Int64, Int) :. GroupMemberRow type GroupMemberRow = (Int64, Int64, MemberId, VersionChat, VersionChat, GroupMemberRole, GroupMemberCategory, GroupMemberStatus, BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, ContactName, Maybe ContactId, ProfileId, ProfileId, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, Maybe Preferences) :. (UTCTime, UTCTime) :. (Maybe UTCTime, Int64, Int64, Int64, Maybe UTCTime) toGroupInfo :: VersionRangeChat -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo -toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, localAlias, description, image, enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt, connReqToConnect) :. businessRow :. (uiThemes, customData, chatItemTTL, membersRequireAttention) :. userMemberRow) = +toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, localAlias, description, image, enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt, connFullLink, connShortLink) :. businessRow :. (uiThemes, customData, chatItemTTL, membersRequireAttention) :. userMemberRow) = let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} fullGroupPreferences = mergeGroupPreferences groupPreferences groupProfile = GroupProfile {displayName, fullName, description, image, groupPreferences, memberAdmission} businessChat = toBusinessChatInfo businessRow - in GroupInfo {groupId, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, connReqToConnect, chatTags, chatItemTTL, uiThemes, customData, membersRequireAttention} + connLinkToConnect = case (connFullLink, connShortLink) of + (Nothing, _) -> Nothing + (Just fullLink, shortLink_) -> Just $ CCLink fullLink shortLink_ + in GroupInfo {groupId, localDisplayName, groupProfile, localAlias, businessChat, fullGroupPreferences, membership, chatSettings, createdAt, updatedAt, chatTs, userMemberProfileSentAt, connLinkToConnect, chatTags, chatItemTTL, uiThemes, customData, membersRequireAttention} toGroupMember :: Int64 -> GroupMemberRow -> GroupMember toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, BI showMessages, memberRestriction_) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, preferences) :. (createdAt, updatedAt) :. (supportChatTs_, supportChatUnread, supportChatMemberAttention, supportChatMentions, supportChatLastMsgFromMemberTs)) = @@ -623,7 +644,7 @@ groupInfoQuery = -- GroupInfo g.group_id, g.local_display_name, gp.display_name, gp.full_name, g.local_alias, gp.description, gp.image, g.enable_ntfs, g.send_rcpts, g.favorite, gp.preferences, gp.member_admission, - g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_req_to_connect, + g.created_at, g.updated_at, g.chat_ts, g.user_member_profile_sent_at, g.conn_full_link_to_connect, g.conn_short_link_to_connect, g.business_chat, g.business_member_id, g.customer_member_id, g.ui_themes, g.custom_data, g.chat_item_ttl, g.members_require_attention, -- GroupMember - membership diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index e40ecf3a85..8db7e27a39 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -51,7 +51,7 @@ import Simplex.Chat.Types.UITheme import Simplex.Chat.Types.Util import Simplex.FileTransfer.Description (FileDigest) import Simplex.FileTransfer.Types (RcvFileId, SndFileId) -import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri, ACorrId, AEventTag (..), AEvtTag (..), ConnId, ConnShortLink, ConnectionLink, ConnectionMode (..), ConnectionRequestUri, CreatedConnLink, InvitationId, SAEntity (..), UserId) +import Simplex.Messaging.Agent.Protocol (ACorrId, ACreatedConnLink, AEventTag (..), AEvtTag (..), ConnId, ConnShortLink, ConnectionLink, ConnectionMode (..), ConnectionRequestUri, CreatedConnLink, InvitationId, SAEntity (..), UserId) import Simplex.Messaging.Agent.Store.DB (Binary (..), blobFieldDecoder, fromTextField_) import Simplex.Messaging.Crypto.File (CryptoFileArgs (..)) import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff) @@ -188,7 +188,7 @@ data Contact = Contact createdAt :: UTCTime, updatedAt :: UTCTime, chatTs :: Maybe UTCTime, - connReqToConnect :: Maybe AConnectionRequestUri, + connLinkToConnect :: Maybe ACreatedConnLink, contactGroupMemberId :: Maybe GroupMemberId, contactGrpInvSent :: Bool, chatTags :: [ChatTagId], @@ -419,7 +419,7 @@ data GroupInfo = GroupInfo updatedAt :: UTCTime, chatTs :: Maybe UTCTime, userMemberProfileSentAt :: Maybe UTCTime, - connReqToConnect :: Maybe ConnReqContact, + connLinkToConnect :: Maybe CreatedLinkContact, chatTags :: [ChatTagId], chatItemTTL :: Maybe Int64, uiThemes :: Maybe UIThemeEntityOverrides, @@ -456,6 +456,8 @@ data GroupSummary = GroupSummary data ContactOrGroup = CGContact Contact | CGGroup GroupInfo [GroupMember] +data ContactOrGroupMemberId = CGMContactId ContactId | CGMGroupMemberId GroupMemberId + contactAndGroupIds :: ContactOrGroup -> (Maybe ContactId, Maybe GroupId) contactAndGroupIds = \case CGContact Contact {contactId} -> (Just contactId, Nothing) @@ -654,7 +656,7 @@ deriving newtype instance FromField ImageData -- TODO [short links] StrEncoding instances? data ContactShortLinkData = ContactShortLinkData { profile :: Profile, - welcomeMessage :: Maybe Text + welcomeMsg :: Maybe Text } deriving (Show) @@ -1481,6 +1483,8 @@ type CreatedLinkContact = CreatedConnLink 'CMContact type ConnLinkContact = ConnectionLink 'CMContact +type ShortLinkInvitation = ConnShortLink 'CMInvitation + type ShortLinkContact = ConnShortLink 'CMContact data Connection = Connection diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index eadea6008d..4989f1ca12 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -189,9 +189,12 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte CRInvitation u ccLink _ -> ttyUser u $ viewConnReqInvitation ccLink CRConnectionIncognitoUpdated u c -> ttyUser u $ viewConnectionIncognitoUpdated c CRConnectionUserChanged u c c' nu -> ttyUser u $ viewConnectionUserChanged u c nu c' - CRConnectionPlan u _ connectionPlan -> ttyUser u $ viewConnectionPlan cfg connectionPlan + CRConnectionPlan u connLink connectionPlan -> ttyUser u $ viewConnectionPlan cfg connLink connectionPlan + CRNewPreparedContact u c -> ttyUser u [ttyContact' c <> ": contact is prepared"] + CRNewPreparedGroup u g -> ttyUser u [ttyGroup' g <> ": group is prepared"] CRSentConfirmation u _ -> ttyUser u ["confirmation sent!"] CRSentInvitation u _ customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView + CRStartedConnectionToContact u c -> ttyUser u [ttyContact' c <> ": connection started"] CRSentInvitationToContact u _c customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView CRItemsReadForChat u _chatId -> ttyUser u ["items read for chat"] CRContactDeleted u c -> ttyUser u [ttyContact' c <> ": contact is deleted"] @@ -988,7 +991,13 @@ viewConnReqInvitation (CCLink cReq shortLink) = "", "and ask them to connect: " <> highlight' "/c " ] - <> ["The invitation link for old clients: " <> plain cReqStr | isJust shortLink] + <> + if isJust shortLink + then + [ "The invitation link for old clients:", + plain cReqStr + ] + else [] where cReqStr = strEncode $ simplexChatInvitation cReq @@ -1815,10 +1824,10 @@ viewConnectionUserChanged User {localDisplayName = n} PendingContactConnection { where cReqStr = strEncode $ simplexChatInvitation cReq -viewConnectionPlan :: ChatConfig -> ConnectionPlan -> [StyledString] -viewConnectionPlan ChatConfig {logLevel, testView} = \case +viewConnectionPlan :: ChatConfig -> ACreatedConnLink -> ConnectionPlan -> [StyledString] +viewConnectionPlan ChatConfig {logLevel, testView} connLink = \case CPInvitationLink ilp -> case ilp of - ILPOk _contactSLinkData -> [invLink "ok to connect"] + ILPOk contactSLinkData -> [invLink "ok to connect"] <> [viewJSON contactSLinkData | testView] ILPOwnLink -> [invLink "own link"] ILPConnecting Nothing -> [invLink "connecting"] ILPConnecting (Just ct) -> [invLink ("connecting to contact " <> ttyContact' ct)] @@ -1829,7 +1838,7 @@ viewConnectionPlan ChatConfig {logLevel, testView} = \case where invLink = ("invitation link: " <>) CPContactAddress cap -> case cap of - CAPOk _contactSLinkData -> [ctAddr "ok to connect"] + CAPOk contactSLinkData -> [ctAddr "ok to connect"] <> [viewJSON contactSLinkData | testView] CAPOwnLink -> [ctAddr "own address"] CAPConnectingConfirmReconnect -> [ctAddr "connecting, allowed to reconnect"] CAPConnectingProhibit ct -> [ctAddr ("connecting to contact " <> ttyContact' ct)] @@ -1841,7 +1850,7 @@ viewConnectionPlan ChatConfig {logLevel, testView} = \case where ctAddr = ("contact address: " <>) CPGroupLink glp -> case glp of - GLPOk _groupSLinkData -> [grpLink "ok to connect"] + GLPOk groupSLinkData -> [grpLink "ok to connect"] <> [viewJSON groupSLinkData | testView] GLPOwnLink g -> [grpLink "own link for group " <> ttyGroup' g] GLPConnectingConfirmReconnect -> [grpLink "connecting, allowed to reconnect"] GLPConnectingProhibit Nothing -> [grpLink "connecting"] diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 21ee1e9218..678238ed48 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -248,6 +248,7 @@ testRetryConnecting ps = testChatCfgOpts2 cfg' opts' aliceProfile bobProfile tes alice <## "server disconnected localhost ()" bob ##> ("/_connect plan 1 " <> inv) bob <## "invitation link: ok to connect" + _sLinkData <- getTermLine bob bob ##> ("/_connect 1 " <> inv) bob <##. "smp agent error: BROKER" withSmpServer' serverCfg' $ do @@ -255,6 +256,7 @@ testRetryConnecting ps = testChatCfgOpts2 cfg' opts' aliceProfile bobProfile tes threadDelay 250000 bob ##> ("/_connect plan 1 " <> inv) bob <## "invitation link: ok to connect" + _sLinkData <- getTermLine bob bob ##> ("/_connect 1 " <> inv) bob <## "confirmation sent!" concurrently_ @@ -299,6 +301,7 @@ testRetryConnectingClientTimeout ps = do withNewTestChatCfgOpts ps cfgZeroTimeout opts' "bob" bobProfile $ \bob -> do bob ##> ("/_connect plan 1 " <> inv) bob <## "invitation link: ok to connect" + _sLinkData <- getTermLine bob bob ##> ("/_connect 1 " <> inv) bob <## "smp agent error: BROKER {brokerAddress = \"smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7003\", brokerErr = TIMEOUT}" @@ -312,6 +315,7 @@ testRetryConnectingClientTimeout ps = do withTestChatCfgOpts ps cfg' opts' "bob" $ \bob -> do bob ##> ("/_connect plan 1 " <> inv) bob <## "invitation link: ok to connect" + _sLinkData <- getTermLine bob bob ##> ("/_connect 1 " <> inv) bob <## "confirmation sent!" @@ -478,6 +482,7 @@ testPlanInvitationLinkOk = inv <- getInvitation alice bob ##> ("/_connect plan 1 " <> inv) bob <## "invitation link: ok to connect" + _sLinkData <- getTermLine bob bob ##> ("/c " <> inv) bob <## "confirmation sent!" @@ -487,6 +492,7 @@ testPlanInvitationLinkOk = bob ##> ("/_connect plan 1 " <> inv) bob <## "invitation link: ok to connect" -- conn_req_inv is forgotten after connection + _sLinkData <- getTermLine bob alice <##> bob testPlanInvitationLinkOwn :: HasCallStack => TestParams -> IO () @@ -510,6 +516,7 @@ testPlanInvitationLinkOwn ps = alice ##> ("/_connect plan 1 " <> inv) alice <## "invitation link: ok to connect" -- conn_req_inv is forgotten after connection + _sLinkData <- getTermLine alice threadDelay 100000 alice @@@ [("@alice_1", lastChatFeature), ("@alice_2", lastChatFeature)] alice `send` "@alice_2 hi" diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 1cf6d0d0d6..324c6589ed 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -2425,10 +2425,12 @@ testPlanGroupLinkLeaveRejoin = bob ##> ("/_connect plan 1 " <> gLink) bob <## "group link: ok to connect" + _sLinkData <- getTermLine bob let gLinkSchema2 = linkAnotherSchema gLink bob ##> ("/_connect plan 1 " <> gLinkSchema2) bob <## "group link: ok to connect" + _sLinkData <- getTermLine bob bob ##> ("/c " <> gLink) bob <## "connection request sent!" @@ -3357,6 +3359,7 @@ testPlanGroupLinkKnown = bob ##> ("/_connect plan 1 " <> gLink) bob <## "group link: ok to connect" + _sLinkData <- getTermLine bob bob ##> ("/c " <> gLink) bob <## "connection request sent!" diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index fe933d5b98..0fdae6fc02 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -102,10 +102,13 @@ chatProfileTests = do it "SimpleX links" testGroupPrefsSimplexLinksForRole it "set user, contact and group UI theme" testSetUITheme describe "short links" $ do - it "should connect via one-time inviation" testShortLinkInvitation - it "should plan and connect via one-time inviation" testPlanShortLinkInvitation + it "should connect via one-time invitation" testShortLinkInvitation + it "should plan and connect via one-time invitation" testPlanShortLinkInvitation it "should connect via contact address" testShortLinkContactAddress it "should join group" testShortLinkJoinGroup + describe "connection via prepared entity" $ do + it "prepare contact using invitation short link data and connect" testShortLinkInvitationPrepareContact + it "prepare contact using address short link data and connect" testShortLinkAddressPrepareContact testUpdateProfile :: HasCallStack => TestParams -> IO () testUpdateProfile = @@ -285,6 +288,7 @@ testRetryAcceptingViaContactLink ps = testChatCfgOpts2 cfg' opts' aliceProfile b alice <## "server disconnected localhost ()" bob ##> ("/_connect plan 1 " <> cLink) bob <## "contact address: ok to connect" + _sLinkData <- getTermLine bob bob ##> ("/_connect 1 " <> cLink) bob <##. "smp agent error: BROKER" withSmpServer' serverCfg' $ do @@ -292,6 +296,7 @@ testRetryAcceptingViaContactLink ps = testChatCfgOpts2 cfg' opts' aliceProfile b threadDelay 250000 bob ##> ("/_connect plan 1 " <> cLink) bob <## "contact address: ok to connect" + _sLinkData <- getTermLine bob bob ##> ("/_connect 1 " <> cLink) alice <#? bob alice <## "server disconnected localhost ()" @@ -701,6 +706,7 @@ testBusinessAddress = testChat3 businessProfile aliceProfile {fullName = "Alice biz <## "auto_accept on, business" bob ##> ("/_connect plan 1 " <> cLink) bob <## "contact address: ok to connect" + _sLinkData <- getTermLine bob bob ##> ("/c " <> cLink) bob <## "connection request sent!" bob ##> ("/_connect plan 1 " <> cLink) @@ -886,6 +892,7 @@ testPlanAddressOkKnown = bob ##> ("/_connect plan 1 " <> cLink) bob <## "contact address: ok to connect" + _sLinkData <- getTermLine bob bob ##> ("/c " <> cLink) alice <#? bob @@ -1068,10 +1075,12 @@ testPlanAddressContactDeletedReconnected = bob ##> ("/_connect plan 1 " <> cLink) bob <## "contact address: ok to connect" + _sLinkData <- getTermLine bob let cLinkSchema2 = linkAnotherSchema cLink bob ##> ("/_connect plan 1 " <> cLinkSchema2) bob <## "contact address: ok to connect" + _sLinkData <- getTermLine bob bob ##> ("/c " <> cLink) bob <## "connection request sent!" @@ -2593,7 +2602,7 @@ testShortLinkInvitation :: HasCallStack => TestParams -> IO () testShortLinkInvitation = testChat2 aliceProfile bobProfile $ \alice bob -> do alice ##> "/c short" - inv <- getShortInvitation alice + (inv, _) <- getShortInvitation alice bob ##> ("/c " <> inv) bob <## "confirmation sent!" concurrently_ @@ -2608,13 +2617,14 @@ testPlanShortLinkInvitation :: HasCallStack => TestParams -> IO () testPlanShortLinkInvitation = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do alice ##> "/c short" - inv <- getShortInvitation alice + (inv, _) <- getShortInvitation alice alice ##> ("/_connect plan 1 " <> inv) alice <## "invitation link: own link" alice ##> ("/_connect plan 1 " <> slSimplexScheme inv) alice <## "invitation link: own link" bob ##> ("/_connect plan 1 " <> inv) bob <## "invitation link: ok to connect" + _sLinkData <- getTermLine bob -- nobody else can connect cath ##> ("/_connect plan 1 " <> inv) cath <##. "error: connection authorization failed" @@ -2623,9 +2633,11 @@ testPlanShortLinkInvitation = -- bob can retry "plan" bob ##> ("/_connect plan 1 " <> inv) bob <## "invitation link: ok to connect" + _sLinkData <- getTermLine bob -- with simplex: scheme too bob ##> ("/_connect plan 1 " <> slSimplexScheme inv) bob <## "invitation link: ok to connect" + _sLinkData <- getTermLine bob bob ##> ("/c " <> inv) bob <## "confirmation sent!" concurrently_ @@ -2669,6 +2681,7 @@ testShortLinkContactAddress = sName <- showName cc cc ##> ("/_connect plan 1 " <> cLink) cc <## "contact address: ok to connect" + _sLinkData <- getTermLine cc cc ##> ("/c " <> cLink) alice <#? cc alice ##> ("/ac " <> name) @@ -2738,6 +2751,7 @@ testShortLinkJoinGroup = sName <- showName cc cc ##> ("/_connect plan 1 " <> link) cc <## "group link: ok to connect" + _sLinkData <- getTermLine cc cc ##> ("/c " <> link) cc <## "connection request sent!" alice <## (sName <> ": accepting request to join group #team...") @@ -2747,3 +2761,52 @@ testShortLinkJoinGroup = cc <## "#team: joining the group..." cc <## "#team: you joined the group" ] + +testShortLinkInvitationPrepareContact :: HasCallStack => TestParams -> IO () +testShortLinkInvitationPrepareContact = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + alice ##> "/_connect 1 short=on" + (shortLink, fullLink) <- getShortInvitation alice + bob ##> ("/_connect plan 1 " <> shortLink) + bob <## "invitation link: ok to connect" + contactSLinkData <- getTermLine bob + bob ##> ("/_prepare contact 1 " <> fullLink <> " " <> shortLink <> " " <> contactSLinkData) + bob <## "alice: contact is prepared" + bob ##> "/_connect contact @2 text hello" + bob + <### [ "alice: connection started", + WithTime "@alice hello" + ] + alice <# "bob> hello" + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + alice <##> bob + +testShortLinkAddressPrepareContact :: HasCallStack => TestParams -> IO () +testShortLinkAddressPrepareContact = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + alice ##> "/ad short" + (shortLink, fullLink) <- getShortContactLink alice True + bob ##> ("/_connect plan 1 " <> shortLink) + bob <## "contact address: ok to connect" + contactSLinkData <- getTermLine bob + bob ##> ("/_prepare contact 1 " <> fullLink <> " " <> shortLink <> " " <> contactSLinkData) + bob <## "alice: contact is prepared" + bob ##> "/_connect contact @2 text hello" + bob + <### [ "alice: connection started", + WithTime "@alice hello" + ] + -- TODO [short links] for alice create message from contact request + alice <## "bob (Bob) wants to connect to you!" + alice <## "to accept: /ac bob" + alice <## "to reject: /rc bob (the sender will NOT be notified)" + alice ##> "/ac bob" + alice <## "bob (Bob): accepting contact request, you can send messages to contact" + concurrently_ + (bob <## "alice (Alice): contact is connected") + (alice <## "bob (Bob): contact is connected") + alice <##> bob diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index 38c8c308c8..bb42c265e8 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -505,20 +505,27 @@ dropPartialReceipt_ msg = case splitAt 2 msg of _ -> Nothing getInvitation :: HasCallStack => TestCC -> IO String -getInvitation = getInvitation_ False +getInvitation cc = do + (inv, _) <- getInvitation_ False cc + pure inv -getShortInvitation :: HasCallStack => TestCC -> IO String +getShortInvitation :: HasCallStack => TestCC -> IO (String, String) getShortInvitation = getInvitation_ True -getInvitation_ :: HasCallStack => Bool -> TestCC -> IO String +getInvitation_ :: HasCallStack => Bool -> TestCC -> IO (String, String) getInvitation_ short cc = do cc <## "pass this invitation link to your contact (via another channel):" cc <## "" inv <- getTermLine cc cc <## "" cc <## "and ask them to connect: /c " - when short $ cc <##. "The invitation link for old clients: https://simplex.chat/invitation#" - pure inv + fullLink <- + if short + then do + cc <##. "The invitation link for old clients:" + getTermLine cc + else pure "" + pure (inv, fullLink) getShortContactLink :: HasCallStack => TestCC -> Bool -> IO (String, String) getShortContactLink cc created = do