core, ui: short connection links with stored data (#5824)

* core, ui: optionally use short links (#5799)

* core: optionally use short links

* update test

* update simplexmq, short group links

* fix query

* fix parser for _connect

* ios: use short links

* shorten links to remove fingerprint and onion hosts from known servers

* fix parser

* tests

* nix

* update query plans

* update simplexmq, simplex: schema for short links

* simplexmq

* update ios

* fix short links in ios

* android: use short links

* fix short group links, test short link connection plans

* core: fix connection plan to recognize own short links

* update simplexmq

* space

* all tests

* relative symlinks in simplexmq to fix windows build

* core: improve connection plan for short links (#5825)

* core: improve connection plan for short links

* improve connection plans

* update UI

* update simplexmq

* ios: add preset server domains to entitlements, add short link paths to .well-known/apple-app-site-association

* update simplexmq

* fix group short link in iOS, fix simplex:/ scheme saved to database or used for connection plans

* update simplexmq

* ios: delay opening URI from outside until the app is started

* update simplexmq
This commit is contained in:
Evgeny 2025-04-14 21:25:32 +01:00 committed by GitHub
parent 38c2529d8b
commit 45e395d35a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
71 changed files with 1676 additions and 819 deletions

View file

@ -443,12 +443,12 @@ struct ContentView: View {
} }
func connectViaUrl() { func connectViaUrl() {
dismissAllSheets() {
let m = ChatModel.shared let m = ChatModel.shared
if let url = m.appOpenUrl { if let url = m.appOpenUrl {
m.appOpenUrl = nil m.appOpenUrl = nil
dismissAllSheets() {
var path = url.path var path = url.path
if (path == "/contact" || path == "/invitation") { if (path == "/contact" || path == "/invitation" || path == "/a" || path == "/c" || path == "/g" || path == "/i") {
path.removeFirst() path.removeFirst()
let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)") let link = url.absoluteString.replacingOccurrences(of: "///\(path)", with: "/\(path)")
planAndConnect( planAndConnect(

View file

@ -839,13 +839,14 @@ func apiVerifyGroupMember(_ groupId: Int64, _ groupMemberId: Int64, connectionCo
return nil return nil
} }
func apiAddContact(incognito: Bool) async -> ((String, PendingContactConnection)?, Alert?) { func apiAddContact(incognito: Bool) async -> ((CreatedConnLink, PendingContactConnection)?, Alert?) {
guard let userId = ChatModel.shared.currentUser?.userId else { guard let userId = ChatModel.shared.currentUser?.userId else {
logger.error("apiAddContact: no current user") logger.error("apiAddContact: no current user")
return (nil, nil) return (nil, nil)
} }
let r = await chatSendCmd(.apiAddContact(userId: userId, incognito: incognito), bgTask: false) let short = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS)
if case let .invitation(_, connReqInvitation, connection) = r { return ((connReqInvitation, connection), nil) } let r = await chatSendCmd(.apiAddContact(userId: userId, short: short, incognito: incognito), bgTask: false)
if case let .invitation(_, connLinkInv, connection) = r { return ((connLinkInv, connection), nil) }
let alert = connectionErrorAlert(r) let alert = connectionErrorAlert(r)
return (nil, alert) return (nil, alert)
} }
@ -856,23 +857,26 @@ func apiSetConnectionIncognito(connId: Int64, incognito: Bool) async throws -> P
throw r throw r
} }
func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> PendingContactConnection? { func apiChangeConnectionUser(connId: Int64, userId: Int64) async throws -> PendingContactConnection {
let r = await chatSendCmd(.apiChangeConnectionUser(connId: connId, userId: userId)) let r = await chatSendCmd(.apiChangeConnectionUser(connId: connId, userId: userId))
if case let .connectionUserChanged(_, _, toConnection, _) = r {return toConnection} if case let .connectionUserChanged(_, _, toConnection, _) = r {return toConnection}
throw r throw r
} }
func apiConnectPlan(connReq: String) async throws -> ConnectionPlan { func apiConnectPlan(connLink: String) async -> ((CreatedConnLink, ConnectionPlan)?, Alert?) {
let userId = try currentUserId("apiConnectPlan") guard let userId = ChatModel.shared.currentUser?.userId else {
let r = await chatSendCmd(.apiConnectPlan(userId: userId, connReq: connReq)) logger.error("apiConnectPlan: no current user")
if case let .connectionPlan(_, connectionPlan) = r { return connectionPlan } return (nil, nil)
logger.error("apiConnectPlan error: \(responseError(r))") }
throw r let r = await chatSendCmd(.apiConnectPlan(userId: userId, connLink: connLink))
if case let .connectionPlan(_, connLink, connPlan) = r { return ((connLink, connPlan), nil) }
let alert = apiConnectResponseAlert(r) ?? connectionErrorAlert(r)
return (nil, alert)
} }
func apiConnect(incognito: Bool, connReq: String) async -> (ConnReqType, PendingContactConnection)? { func apiConnect(incognito: Bool, connLink: CreatedConnLink) async -> (ConnReqType, PendingContactConnection)? {
let (r, alert) = await apiConnect_(incognito: incognito, connReq: connReq) let (r, alert) = await apiConnect_(incognito: incognito, connLink: connLink)
if let alert = alert { if let alert = alert {
AlertManager.shared.showAlert(alert) AlertManager.shared.showAlert(alert)
return nil return nil
@ -881,12 +885,12 @@ func apiConnect(incognito: Bool, connReq: String) async -> (ConnReqType, Pending
} }
} }
func apiConnect_(incognito: Bool, connReq: String) async -> ((ConnReqType, PendingContactConnection)?, Alert?) { func apiConnect_(incognito: Bool, connLink: CreatedConnLink) async -> ((ConnReqType, PendingContactConnection)?, Alert?) {
guard let userId = ChatModel.shared.currentUser?.userId else { guard let userId = ChatModel.shared.currentUser?.userId else {
logger.error("apiConnect: no current user") logger.error("apiConnect: no current user")
return (nil, nil) return (nil, nil)
} }
let r = await chatSendCmd(.apiConnect(userId: userId, incognito: incognito, connReq: connReq)) let r = await chatSendCmd(.apiConnect(userId: userId, incognito: incognito, connLink: connLink))
let m = ChatModel.shared let m = ChatModel.shared
switch r { switch r {
case let .sentConfirmation(_, connection): case let .sentConfirmation(_, connection):
@ -899,20 +903,31 @@ func apiConnect_(incognito: Bool, connReq: String) async -> ((ConnReqType, Pendi
} }
let alert = contactAlreadyExistsAlert(contact) let alert = contactAlreadyExistsAlert(contact)
return (nil, alert) return (nil, alert)
default: ()
}
let alert = apiConnectResponseAlert(r) ?? connectionErrorAlert(r)
return (nil, alert)
}
private func apiConnectResponseAlert(_ r: ChatResponse) -> Alert? {
switch r {
case .chatCmdError(_, .error(.invalidConnReq)): case .chatCmdError(_, .error(.invalidConnReq)):
let alert = mkAlert( mkAlert(
title: "Invalid connection link", title: "Invalid connection link",
message: "Please check that you used the correct link or ask your contact to send you another one." message: "Please check that you used the correct link or ask your contact to send you another one."
) )
return (nil, alert) case .chatCmdError(_, .error(.unsupportedConnReq)):
mkAlert(
title: "Unsupported connection link",
message: "This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link."
)
case .chatCmdError(_, .errorAgent(.SMP(_, .AUTH))): case .chatCmdError(_, .errorAgent(.SMP(_, .AUTH))):
let alert = mkAlert( mkAlert(
title: "Connection error (AUTH)", title: "Connection error (AUTH)",
message: "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." message: "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection."
) )
return (nil, alert)
case let .chatCmdError(_, .errorAgent(.SMP(_, .BLOCKED(info)))): case let .chatCmdError(_, .errorAgent(.SMP(_, .BLOCKED(info)))):
let alert = Alert( Alert(
title: Text("Connection blocked"), title: Text("Connection blocked"),
message: Text("Connection is blocked by server operator:\n\(info.reason.text)"), message: Text("Connection is blocked by server operator:\n\(info.reason.text)"),
primaryButton: .default(Text("Ok")), primaryButton: .default(Text("Ok")),
@ -922,25 +937,22 @@ func apiConnect_(incognito: Bool, connReq: String) async -> ((ConnReqType, Pendi
} }
} }
) )
return (nil, alert)
case .chatCmdError(_, .errorAgent(.SMP(_, .QUOTA))): case .chatCmdError(_, .errorAgent(.SMP(_, .QUOTA))):
let alert = mkAlert( mkAlert(
title: "Undelivered messages", title: "Undelivered messages",
message: "The connection reached the limit of undelivered messages, your contact may be offline." message: "The connection reached the limit of undelivered messages, your contact may be offline."
) )
return (nil, alert)
case let .chatCmdError(_, .errorAgent(.INTERNAL(internalErr))): case let .chatCmdError(_, .errorAgent(.INTERNAL(internalErr))):
if internalErr == "SEUniqueID" { if internalErr == "SEUniqueID" {
let alert = mkAlert( mkAlert(
title: "Already connected?", title: "Already connected?",
message: "It seems like you are already connected via this link. If it is not the case, there was an error (\(responseError(r)))." message: "It seems like you are already connected via this link. If it is not the case, there was an error (\(responseError(r)))."
) )
return (nil, alert) } else {
nil
} }
default: () default: nil
} }
let alert = connectionErrorAlert(r)
return (nil, alert)
} }
func contactAlreadyExistsAlert(_ contact: Contact) -> Alert { func contactAlreadyExistsAlert(_ contact: Contact) -> Alert {
@ -1130,10 +1142,10 @@ func apiSetChatUIThemes(chatId: ChatId, themes: ThemeModeOverrides?) async -> Bo
} }
func apiCreateUserAddress() async throws -> String { func apiCreateUserAddress(short: Bool) async throws -> CreatedConnLink {
let userId = try currentUserId("apiCreateUserAddress") let userId = try currentUserId("apiCreateUserAddress")
let r = await chatSendCmd(.apiCreateMyAddress(userId: userId)) let r = await chatSendCmd(.apiCreateMyAddress(userId: userId, short: short))
if case let .userContactLinkCreated(_, connReq) = r { return connReq } if case let .userContactLinkCreated(_, connLink) = r { return connLink }
throw r throw r
} }
@ -1642,15 +1654,16 @@ func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws
throw r throw r
} }
func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> (String, GroupMemberRole) { func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> (CreatedConnLink, GroupMemberRole) {
let r = await chatSendCmd(.apiCreateGroupLink(groupId: groupId, memberRole: memberRole)) let short = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS)
if case let .groupLinkCreated(_, _, connReq, memberRole) = r { return (connReq, memberRole) } let r = await chatSendCmd(.apiCreateGroupLink(groupId: groupId, memberRole: memberRole, short: short))
if case let .groupLinkCreated(_, _, connLink, memberRole) = r { return (connLink, memberRole) }
throw r throw r
} }
func apiGroupLinkMemberRole(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> (String, GroupMemberRole) { func apiGroupLinkMemberRole(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> (CreatedConnLink, GroupMemberRole) {
let r = await chatSendCmd(.apiGroupLinkMemberRole(groupId: groupId, memberRole: memberRole)) let r = await chatSendCmd(.apiGroupLinkMemberRole(groupId: groupId, memberRole: memberRole))
if case let .groupLink(_, _, connReq, memberRole) = r { return (connReq, memberRole) } if case let .groupLink(_, _, connLink, memberRole) = r { return (connLink, memberRole) }
throw r throw r
} }
@ -1660,11 +1673,11 @@ func apiDeleteGroupLink(_ groupId: Int64) async throws {
throw r throw r
} }
func apiGetGroupLink(_ groupId: Int64) throws -> (String, GroupMemberRole)? { func apiGetGroupLink(_ groupId: Int64) throws -> (CreatedConnLink, GroupMemberRole)? {
let r = chatSendCmdSync(.apiGetGroupLink(groupId: groupId)) let r = chatSendCmdSync(.apiGetGroupLink(groupId: groupId))
switch r { switch r {
case let .groupLink(_, _, connReq, memberRole): case let .groupLink(_, _, connLink, memberRole):
return (connReq, memberRole) return (connLink, memberRole)
case .chatCmdError(_, chatError: .errorStore(storeError: .groupLinkNotFound)): case .chatCmdError(_, chatError: .errorStore(storeError: .groupLinkNotFound)):
return nil return nil
default: throw r default: throw r

View file

@ -19,6 +19,7 @@ struct SimpleXApp: App {
@Environment(\.scenePhase) var scenePhase @Environment(\.scenePhase) var scenePhase
@State private var enteredBackgroundAuthenticated: TimeInterval? = nil @State private var enteredBackgroundAuthenticated: TimeInterval? = nil
@State private var appOpenUrlLater: URL?
init() { init() {
DispatchQueue.global(qos: .background).sync { DispatchQueue.global(qos: .background).sync {
@ -42,7 +43,11 @@ struct SimpleXApp: App {
.environmentObject(AppTheme.shared) .environmentObject(AppTheme.shared)
.onOpenURL { url in .onOpenURL { url in
logger.debug("ContentView.onOpenURL: \(url)") logger.debug("ContentView.onOpenURL: \(url)")
if AppChatState.shared.value == .active {
chatModel.appOpenUrl = url chatModel.appOpenUrl = url
} else {
appOpenUrlLater = url
}
} }
.onAppear() { .onAppear() {
// Present screen for continue migration if it wasn't finished yet // Present screen for continue migration if it wasn't finished yet
@ -93,9 +98,18 @@ struct SimpleXApp: App {
if !chatModel.showCallView && !CallController.shared.hasActiveCalls() { if !chatModel.showCallView && !CallController.shared.hasActiveCalls() {
await updateCallInvitations() await updateCallInvitations()
} }
if let url = appOpenUrlLater {
await MainActor.run {
appOpenUrlLater = nil
chatModel.appOpenUrl = url
} }
} }
} }
} else if let url = appOpenUrlLater {
appOpenUrlLater = nil
chatModel.appOpenUrl = url
}
}
} }
} }
default: default:

View file

@ -7,7 +7,7 @@
// //
import SwiftUI import SwiftUI
import SimpleXChat @preconcurrency import SimpleXChat
func infoRow(_ title: LocalizedStringKey, _ value: String) -> some View { func infoRow(_ title: LocalizedStringKey, _ value: String) -> some View {
HStack { HStack {

View file

@ -45,7 +45,7 @@ struct ChatView: View {
@State private var selectedMember: GMember? = nil @State private var selectedMember: GMember? = nil
// opening GroupLinkView on link button (incognito) // opening GroupLinkView on link button (incognito)
@State private var showGroupLinkSheet: Bool = false @State private var showGroupLinkSheet: Bool = false
@State private var groupLink: String? @State private var groupLink: CreatedConnLink?
@State private var groupLinkMemberRole: GroupMemberRole = .member @State private var groupLinkMemberRole: GroupMemberRole = .member
@State private var forwardedChatItems: [ChatItem] = [] @State private var forwardedChatItems: [ChatItem] = []
@State private var selectedChatItems: Set<Int64>? = nil @State private var selectedChatItems: Set<Int64>? = nil

View file

@ -21,7 +21,7 @@ struct GroupChatInfoView: View {
@State var localAlias: String @State var localAlias: String
@FocusState private var aliasTextFieldFocused: Bool @FocusState private var aliasTextFieldFocused: Bool
@State private var alert: GroupChatInfoViewAlert? = nil @State private var alert: GroupChatInfoViewAlert? = nil
@State private var groupLink: String? @State private var groupLink: CreatedConnLink?
@State private var groupLinkMemberRole: GroupMemberRole = .member @State private var groupLinkMemberRole: GroupMemberRole = .member
@State private var groupLinkNavLinkActive: Bool = false @State private var groupLinkNavLinkActive: Bool = false
@State private var addMembersNavLinkActive: Bool = false @State private var addMembersNavLinkActive: Bool = false

View file

@ -10,12 +10,14 @@ import SwiftUI
import SimpleXChat import SimpleXChat
struct GroupLinkView: View { struct GroupLinkView: View {
@EnvironmentObject var theme: AppTheme
var groupId: Int64 var groupId: Int64
@Binding var groupLink: String? @Binding var groupLink: CreatedConnLink?
@Binding var groupLinkMemberRole: GroupMemberRole @Binding var groupLinkMemberRole: GroupMemberRole
var showTitle: Bool = false var showTitle: Bool = false
var creatingGroup: Bool = false var creatingGroup: Bool = false
var linkCreatedCb: (() -> Void)? = nil var linkCreatedCb: (() -> Void)? = nil
@State private var showShortLink = true
@State private var creatingLink = false @State private var creatingLink = false
@State private var alert: GroupLinkAlert? @State private var alert: GroupLinkAlert?
@State private var shouldCreate = true @State private var shouldCreate = true
@ -69,10 +71,10 @@ struct GroupLinkView: View {
} }
} }
.frame(height: 36) .frame(height: 36)
SimpleXLinkQRCode(uri: groupLink) SimpleXCreatedLinkQRCode(link: groupLink, short: $showShortLink)
.id("simplex-qrcode-view-for-\(groupLink)") .id("simplex-qrcode-view-for-\(groupLink.simplexChatUri(short: showShortLink))")
Button { Button {
showShareSheet(items: [simplexChatLink(groupLink)]) showShareSheet(items: [groupLink.simplexChatUri(short: showShortLink)])
} label: { } label: {
Label("Share link", systemImage: "square.and.arrow.up") Label("Share link", systemImage: "square.and.arrow.up")
} }
@ -93,6 +95,10 @@ struct GroupLinkView: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
} }
} }
} header: {
if let groupLink, groupLink.connShortLink != nil {
ToggleShortLinkHeader(text: Text(""), link: groupLink, short: $showShortLink)
}
} }
.alert(item: $alert) { alert in .alert(item: $alert) { alert in
switch alert { switch alert {
@ -158,8 +164,8 @@ struct GroupLinkView: View {
struct GroupLinkView_Previews: PreviewProvider { struct GroupLinkView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
@State var groupLink: String? = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D" @State var groupLink: CreatedConnLink? = CreatedConnLink(connFullLink: "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", connShortLink: nil)
@State var noGroupLink: String? = nil @State var noGroupLink: CreatedConnLink? = nil
return Group { return Group {
GroupLinkView(groupId: 1, groupLink: $groupLink, groupLinkMemberRole: Binding.constant(.member)) GroupLinkView(groupId: 1, groupLink: $groupLink, groupLinkMemberRole: Binding.constant(.member))

View file

@ -14,6 +14,7 @@ struct ContactConnectionInfo: View {
@EnvironmentObject var theme: AppTheme @EnvironmentObject var theme: AppTheme
@Environment(\.dismiss) var dismiss: DismissAction @Environment(\.dismiss) var dismiss: DismissAction
@State var contactConnection: PendingContactConnection @State var contactConnection: PendingContactConnection
@State private var showShortLink: Bool = true
@State private var alert: CCInfoAlert? @State private var alert: CCInfoAlert?
@State private var localAlias = "" @State private var localAlias = ""
@State private var showIncognitoSheet = false @State private var showIncognitoSheet = false
@ -61,14 +62,19 @@ struct ContactConnectionInfo: View {
} }
if contactConnection.initiated, if contactConnection.initiated,
let connReqInv = contactConnection.connReqInv { let connLinkInv = contactConnection.connLinkInv {
SimpleXLinkQRCode(uri: simplexChatLink(connReqInv)) SimpleXCreatedLinkQRCode(link: connLinkInv, short: $showShortLink)
.id("simplex-invitation-qrcode-\(connLinkInv.simplexChatUri(short: showShortLink))")
incognitoEnabled() incognitoEnabled()
shareLinkButton(connReqInv, theme.colors.secondary) shareLinkButton(connLinkInv, short: showShortLink)
oneTimeLinkLearnMoreButton(theme.colors.secondary) oneTimeLinkLearnMoreButton()
} else { } else {
incognitoEnabled() incognitoEnabled()
oneTimeLinkLearnMoreButton(theme.colors.secondary) oneTimeLinkLearnMoreButton()
}
} header: {
if let connLinkInv = contactConnection.connLinkInv, connLinkInv.connShortLink != nil {
ToggleShortLinkHeader(text: Text(""), link: connLinkInv, short: $showShortLink)
} }
} footer: { } footer: {
sharedProfileInfo(contactConnection.incognito) sharedProfileInfo(contactConnection.incognito)
@ -167,26 +173,22 @@ struct ContactConnectionInfo: View {
} }
} }
private func shareLinkButton(_ connReqInvitation: String, _ secondaryColor: Color) -> some View { private func shareLinkButton(_ connLinkInvitation: CreatedConnLink, short: Bool) -> some View {
Button { Button {
showShareSheet(items: [simplexChatLink(connReqInvitation)]) showShareSheet(items: [connLinkInvitation.simplexChatUri(short: short)])
} label: { } label: {
settingsRow("square.and.arrow.up", color: secondaryColor) { Label("Share 1-time link", systemImage: "square.and.arrow.up")
Text("Share 1-time link")
}
} }
} }
private func oneTimeLinkLearnMoreButton(_ secondaryColor: Color) -> some View { private func oneTimeLinkLearnMoreButton() -> some View {
NavigationLink { NavigationLink {
AddContactLearnMore(showTitle: false) AddContactLearnMore(showTitle: false)
.navigationTitle("One-time invitation link") .navigationTitle("One-time invitation link")
.modifier(ThemedBackground()) .modifier(ThemedBackground())
.navigationBarTitleDisplayMode(.large) .navigationBarTitleDisplayMode(.large)
} label: { } label: {
settingsRow("info.circle", color: secondaryColor) { Label("Learn more", systemImage: "info.circle")
Text("Learn more")
}
} }
} }

View file

@ -23,7 +23,7 @@ struct AddGroupView: View {
@State private var showTakePhoto = false @State private var showTakePhoto = false
@State private var chosenImage: UIImage? = nil @State private var chosenImage: UIImage? = nil
@State private var showInvalidNameAlert = false @State private var showInvalidNameAlert = false
@State private var groupLink: String? @State private var groupLink: CreatedConnLink?
@State private var groupLinkMemberRole: GroupMemberRole = .member @State private var groupLinkMemberRole: GroupMemberRole = .member
var body: some View { var body: some View {

View file

@ -81,7 +81,8 @@ struct NewChatView: View {
@State var selection: NewChatOption @State var selection: NewChatOption
@State var showQRCodeScanner = false @State var showQRCodeScanner = false
@State private var invitationUsed: Bool = false @State private var invitationUsed: Bool = false
@State private var connReqInvitation: String = "" @State private var connLinkInvitation: CreatedConnLink = CreatedConnLink(connFullLink: "", connShortLink: nil)
@State private var showShortLink = true
@State private var creatingConnReq = false @State private var creatingConnReq = false
@State var choosingProfile = false @State var choosingProfile = false
@State private var pastedLink: String = "" @State private var pastedLink: String = ""
@ -174,11 +175,12 @@ struct NewChatView: View {
private func prepareAndInviteView() -> some View { private func prepareAndInviteView() -> some View {
ZStack { // ZStack is needed for views to not make transitions between each other ZStack { // ZStack is needed for views to not make transitions between each other
if connReqInvitation != "" { if connLinkInvitation.connFullLink != "" {
InviteView( InviteView(
invitationUsed: $invitationUsed, invitationUsed: $invitationUsed,
contactConnection: $contactConnection, contactConnection: $contactConnection,
connReqInvitation: $connReqInvitation, connLinkInvitation: $connLinkInvitation,
showShortLink: $showShortLink,
choosingProfile: $choosingProfile choosingProfile: $choosingProfile
) )
} else if creatingConnReq { } else if creatingConnReq {
@ -190,16 +192,16 @@ struct NewChatView: View {
} }
private func createInvitation() { private func createInvitation() {
if connReqInvitation == "" && contactConnection == nil && !creatingConnReq { if connLinkInvitation.connFullLink == "" && contactConnection == nil && !creatingConnReq {
creatingConnReq = true creatingConnReq = true
Task { Task {
_ = try? await Task.sleep(nanoseconds: 250_000000) _ = try? await Task.sleep(nanoseconds: 250_000000)
let (r, apiAlert) = await apiAddContact(incognito: incognitoGroupDefault.get()) let (r, apiAlert) = await apiAddContact(incognito: incognitoGroupDefault.get())
if let (connReq, pcc) = r { if let (connLink, pcc) = r {
await MainActor.run { await MainActor.run {
m.updateContactConnection(pcc) m.updateContactConnection(pcc)
m.showingInvitation = ShowingInvitation(pcc: pcc, connChatUsed: false) m.showingInvitation = ShowingInvitation(pcc: pcc, connChatUsed: false)
connReqInvitation = connReq connLinkInvitation = connLink
contactConnection = pcc contactConnection = pcc
} }
} else { } else {
@ -243,7 +245,8 @@ private struct InviteView: View {
@EnvironmentObject var theme: AppTheme @EnvironmentObject var theme: AppTheme
@Binding var invitationUsed: Bool @Binding var invitationUsed: Bool
@Binding var contactConnection: PendingContactConnection? @Binding var contactConnection: PendingContactConnection?
@Binding var connReqInvitation: String @Binding var connLinkInvitation: CreatedConnLink
@Binding var showShortLink: Bool
@Binding var choosingProfile: Bool @Binding var choosingProfile: Bool
@AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false
@ -261,7 +264,7 @@ private struct InviteView: View {
NavigationLink { NavigationLink {
ActiveProfilePicker( ActiveProfilePicker(
contactConnection: $contactConnection, contactConnection: $contactConnection,
connReqInvitation: $connReqInvitation, connLinkInvitation: $connLinkInvitation,
incognitoEnabled: $incognitoDefault, incognitoEnabled: $incognitoDefault,
choosingProfile: $choosingProfile, choosingProfile: $choosingProfile,
selectedProfile: selectedProfile selectedProfile: selectedProfile
@ -296,7 +299,7 @@ private struct InviteView: View {
private func shareLinkView() -> some View { private func shareLinkView() -> some View {
HStack { HStack {
let link = simplexChatLink(connReqInvitation) let link = connLinkInvitation.simplexChatUri(short: showShortLink)
linkTextView(link) linkTextView(link)
Button { Button {
showShareSheet(items: [link]) showShareSheet(items: [link])
@ -310,9 +313,9 @@ private struct InviteView: View {
} }
private func qrCodeView() -> some View { private func qrCodeView() -> some View {
Section(header: Text("Or show this code").foregroundColor(theme.colors.secondary)) { Section {
SimpleXLinkQRCode(uri: connReqInvitation, onShare: setInvitationUsed) SimpleXCreatedLinkQRCode(link: connLinkInvitation, short: $showShortLink, onShare: setInvitationUsed)
.id("simplex-qrcode-view-for-\(connReqInvitation)") .id("simplex-qrcode-view-for-\(connLinkInvitation.simplexChatUri(short: showShortLink))")
.padding() .padding()
.background( .background(
RoundedRectangle(cornerRadius: 12, style: .continuous) RoundedRectangle(cornerRadius: 12, style: .continuous)
@ -322,6 +325,8 @@ private struct InviteView: View {
.listRowBackground(Color.clear) .listRowBackground(Color.clear)
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
} header: {
ToggleShortLinkHeader(text: Text("Or show this code"), link: connLinkInvitation, short: $showShortLink)
} }
} }
@ -343,7 +348,7 @@ private struct ActiveProfilePicker: View {
@EnvironmentObject var chatModel: ChatModel @EnvironmentObject var chatModel: ChatModel
@EnvironmentObject var theme: AppTheme @EnvironmentObject var theme: AppTheme
@Binding var contactConnection: PendingContactConnection? @Binding var contactConnection: PendingContactConnection?
@Binding var connReqInvitation: String @Binding var connLinkInvitation: CreatedConnLink
@Binding var incognitoEnabled: Bool @Binding var incognitoEnabled: Bool
@Binding var choosingProfile: Bool @Binding var choosingProfile: Bool
@State private var alert: SomeAlert? @State private var alert: SomeAlert?
@ -415,12 +420,11 @@ private struct ActiveProfilePicker: View {
} }
Task { Task {
do { do {
if let contactConn = contactConnection, if let contactConn = contactConnection {
let conn = try await apiChangeConnectionUser(connId: contactConn.pccConnId, userId: profile.userId) { let conn = try await apiChangeConnectionUser(connId: contactConn.pccConnId, userId: profile.userId)
await MainActor.run { await MainActor.run {
contactConnection = conn contactConnection = conn
connReqInvitation = conn.connReqInv ?? "" connLinkInvitation = conn.connLinkInv ?? CreatedConnLink(connFullLink: "", connShortLink: nil)
incognitoEnabled = false incognitoEnabled = false
chatModel.updateContactConnection(conn) chatModel.updateContactConnection(conn)
} }
@ -836,23 +840,25 @@ func sharedProfileInfo(_ incognito: Bool) -> Text {
} }
enum PlanAndConnectAlert: Identifiable { enum PlanAndConnectAlert: Identifiable {
case ownInvitationLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool) case ownInvitationLinkConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool)
case invitationLinkConnecting(connectionLink: String) case invitationLinkConnecting(connectionLink: CreatedConnLink)
case ownContactAddressConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool) case ownContactAddressConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool)
case contactAddressConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool) case contactAddressConnectingConfirmReconnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool)
case groupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool) case groupLinkConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool)
case groupLinkConnectingConfirmReconnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool) case groupLinkConnectingConfirmReconnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool)
case groupLinkConnecting(connectionLink: String, groupInfo: GroupInfo?) case groupLinkConnecting(connectionLink: CreatedConnLink, groupInfo: GroupInfo?)
case error(shortOrFullLink: String, alert: Alert)
var id: String { var id: String {
switch self { switch self {
case let .ownInvitationLinkConfirmConnect(connectionLink, _, _): return "ownInvitationLinkConfirmConnect \(connectionLink)" case let .ownInvitationLinkConfirmConnect(connectionLink, _, _): return "ownInvitationLinkConfirmConnect \(connectionLink.connFullLink)"
case let .invitationLinkConnecting(connectionLink): return "invitationLinkConnecting \(connectionLink)" case let .invitationLinkConnecting(connectionLink): return "invitationLinkConnecting \(connectionLink.connFullLink)"
case let .ownContactAddressConfirmConnect(connectionLink, _, _): return "ownContactAddressConfirmConnect \(connectionLink)" case let .ownContactAddressConfirmConnect(connectionLink, _, _): return "ownContactAddressConfirmConnect \(connectionLink.connFullLink)"
case let .contactAddressConnectingConfirmReconnect(connectionLink, _, _): return "contactAddressConnectingConfirmReconnect \(connectionLink)" case let .contactAddressConnectingConfirmReconnect(connectionLink, _, _): return "contactAddressConnectingConfirmReconnect \(connectionLink.connFullLink)"
case let .groupLinkConfirmConnect(connectionLink, _, _): return "groupLinkConfirmConnect \(connectionLink)" case let .groupLinkConfirmConnect(connectionLink, _, _): return "groupLinkConfirmConnect \(connectionLink.connFullLink)"
case let .groupLinkConnectingConfirmReconnect(connectionLink, _, _): return "groupLinkConnectingConfirmReconnect \(connectionLink)" case let .groupLinkConnectingConfirmReconnect(connectionLink, _, _): return "groupLinkConnectingConfirmReconnect \(connectionLink.connFullLink)"
case let .groupLinkConnecting(connectionLink, _): return "groupLinkConnecting \(connectionLink)" case let .groupLinkConnecting(connectionLink, _): return "groupLinkConnecting \(connectionLink.connFullLink)"
case let .error(shortOrFullLink, alert): return "error \(shortOrFullLink)"
} }
} }
} }
@ -935,21 +941,22 @@ func planAndConnectAlert(_ alert: PlanAndConnectAlert, dismiss: Bool, cleanup: (
dismissButton: .default(Text("OK")) { cleanup?() } dismissButton: .default(Text("OK")) { cleanup?() }
) )
} }
case let .error(_, alert): return alert
} }
} }
enum PlanAndConnectActionSheet: Identifiable { enum PlanAndConnectActionSheet: Identifiable {
case askCurrentOrIncognitoProfile(connectionLink: String, connectionPlan: ConnectionPlan?, title: LocalizedStringKey) case askCurrentOrIncognitoProfile(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan?, title: LocalizedStringKey)
case askCurrentOrIncognitoProfileDestructive(connectionLink: String, connectionPlan: ConnectionPlan, title: LocalizedStringKey) case askCurrentOrIncognitoProfileDestructive(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, title: LocalizedStringKey)
case askCurrentOrIncognitoProfileConnectContactViaAddress(contact: Contact) case askCurrentOrIncognitoProfileConnectContactViaAddress(contact: Contact)
case ownGroupLinkConfirmConnect(connectionLink: String, connectionPlan: ConnectionPlan, incognito: Bool?, groupInfo: GroupInfo) case ownGroupLinkConfirmConnect(connectionLink: CreatedConnLink, connectionPlan: ConnectionPlan, incognito: Bool?, groupInfo: GroupInfo)
var id: String { var id: String {
switch self { switch self {
case let .askCurrentOrIncognitoProfile(connectionLink, _, _): return "askCurrentOrIncognitoProfile \(connectionLink)" case let .askCurrentOrIncognitoProfile(connectionLink, _, _): return "askCurrentOrIncognitoProfile \(connectionLink.connFullLink)"
case let .askCurrentOrIncognitoProfileDestructive(connectionLink, _, _): return "askCurrentOrIncognitoProfileDestructive \(connectionLink)" case let .askCurrentOrIncognitoProfileDestructive(connectionLink, _, _): return "askCurrentOrIncognitoProfileDestructive \(connectionLink.connFullLink)"
case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact): return "askCurrentOrIncognitoProfileConnectContactViaAddress \(contact.contactId)" case let .askCurrentOrIncognitoProfileConnectContactViaAddress(contact): return "askCurrentOrIncognitoProfileConnectContactViaAddress \(contact.contactId)"
case let .ownGroupLinkConfirmConnect(connectionLink, _, _, _): return "ownGroupLinkConfirmConnect \(connectionLink)" case let .ownGroupLinkConfirmConnect(connectionLink, _, _, _): return "ownGroupLinkConfirmConnect \(connectionLink.connFullLink)"
} }
} }
} }
@ -1008,7 +1015,7 @@ func planAndConnectActionSheet(_ sheet: PlanAndConnectActionSheet, dismiss: Bool
} }
func planAndConnect( func planAndConnect(
_ connectionLink: String, _ shortOrFullLink: String,
showAlert: @escaping (PlanAndConnectAlert) -> Void, showAlert: @escaping (PlanAndConnectAlert) -> Void,
showActionSheet: @escaping (PlanAndConnectActionSheet) -> Void, showActionSheet: @escaping (PlanAndConnectActionSheet) -> Void,
dismiss: Bool, dismiss: Bool,
@ -1018,8 +1025,8 @@ func planAndConnect(
filterKnownGroup: ((GroupInfo) -> Void)? = nil filterKnownGroup: ((GroupInfo) -> Void)? = nil
) { ) {
Task { Task {
do { let (result, alert) = await apiConnectPlan(connLink: shortOrFullLink)
let connectionPlan = try await apiConnectPlan(connReq: connectionLink) if let (connectionLink, connectionPlan) = result {
switch connectionPlan { switch connectionPlan {
case let .invitationLink(ilp): case let .invitationLink(ilp):
switch ilp { switch ilp {
@ -1028,17 +1035,22 @@ func planAndConnect(
if let incognito = incognito { if let incognito = incognito {
connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup)
} else { } else {
await MainActor.run {
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via one-time link")) showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via one-time link"))
} }
}
case .ownLink: case .ownLink:
logger.debug("planAndConnect, .invitationLink, .ownLink, incognito=\(incognito?.description ?? "nil")") logger.debug("planAndConnect, .invitationLink, .ownLink, incognito=\(incognito?.description ?? "nil")")
await MainActor.run {
if let incognito = incognito { if let incognito = incognito {
showAlert(.ownInvitationLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) showAlert(.ownInvitationLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
} else { } else {
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own one-time link!")) showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own one-time link!"))
} }
}
case let .connecting(contact_): case let .connecting(contact_):
logger.debug("planAndConnect, .invitationLink, .connecting, incognito=\(incognito?.description ?? "nil")") logger.debug("planAndConnect, .invitationLink, .connecting, incognito=\(incognito?.description ?? "nil")")
await MainActor.run {
if let contact = contact_ { if let contact = contact_ {
if let f = filterKnownContact { if let f = filterKnownContact {
f(contact) f(contact)
@ -1048,14 +1060,17 @@ func planAndConnect(
} else { } else {
showAlert(.invitationLinkConnecting(connectionLink: connectionLink)) showAlert(.invitationLinkConnecting(connectionLink: connectionLink))
} }
}
case let .known(contact): case let .known(contact):
logger.debug("planAndConnect, .invitationLink, .known, incognito=\(incognito?.description ?? "nil")") logger.debug("planAndConnect, .invitationLink, .known, incognito=\(incognito?.description ?? "nil")")
await MainActor.run {
if let f = filterKnownContact { if let f = filterKnownContact {
f(contact) f(contact)
} else { } else {
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) } openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) }
} }
} }
}
case let .contactAddress(cap): case let .contactAddress(cap):
switch cap { switch cap {
case .ok: case .ok:
@ -1063,70 +1078,91 @@ func planAndConnect(
if let incognito = incognito { if let incognito = incognito {
connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup) connectViaLink(connectionLink, connectionPlan: connectionPlan, dismiss: dismiss, incognito: incognito, cleanup: cleanup)
} else { } else {
await MainActor.run {
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via contact address")) showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect via contact address"))
} }
}
case .ownLink: case .ownLink:
logger.debug("planAndConnect, .contactAddress, .ownLink, incognito=\(incognito?.description ?? "nil")") logger.debug("planAndConnect, .contactAddress, .ownLink, incognito=\(incognito?.description ?? "nil")")
await MainActor.run {
if let incognito = incognito { if let incognito = incognito {
showAlert(.ownContactAddressConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) showAlert(.ownContactAddressConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
} else { } else {
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own SimpleX address!")) showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Connect to yourself?\nThis is your own SimpleX address!"))
} }
}
case .connectingConfirmReconnect: case .connectingConfirmReconnect:
logger.debug("planAndConnect, .contactAddress, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")") logger.debug("planAndConnect, .contactAddress, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")")
await MainActor.run {
if let incognito = incognito { if let incognito = incognito {
showAlert(.contactAddressConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) showAlert(.contactAddressConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
} else { } else {
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You have already requested connection!\nRepeat connection request?")) showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You have already requested connection!\nRepeat connection request?"))
} }
}
case let .connectingProhibit(contact): case let .connectingProhibit(contact):
logger.debug("planAndConnect, .contactAddress, .connectingProhibit, incognito=\(incognito?.description ?? "nil")") logger.debug("planAndConnect, .contactAddress, .connectingProhibit, incognito=\(incognito?.description ?? "nil")")
await MainActor.run {
if let f = filterKnownContact { if let f = filterKnownContact {
f(contact) f(contact)
} else { } else {
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) } openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyConnectingAlert(contact)) }
} }
}
case let .known(contact): case let .known(contact):
logger.debug("planAndConnect, .contactAddress, .known, incognito=\(incognito?.description ?? "nil")") logger.debug("planAndConnect, .contactAddress, .known, incognito=\(incognito?.description ?? "nil")")
await MainActor.run {
if let f = filterKnownContact { if let f = filterKnownContact {
f(contact) f(contact)
} else { } else {
openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) } openKnownContact(contact, dismiss: dismiss) { AlertManager.shared.showAlert(contactAlreadyExistsAlert(contact)) }
} }
}
case let .contactViaAddress(contact): case let .contactViaAddress(contact):
logger.debug("planAndConnect, .contactAddress, .contactViaAddress, incognito=\(incognito?.description ?? "nil")") logger.debug("planAndConnect, .contactAddress, .contactViaAddress, incognito=\(incognito?.description ?? "nil")")
if let incognito = incognito { if let incognito = incognito {
connectContactViaAddress_(contact, dismiss: dismiss, incognito: incognito, cleanup: cleanup) connectContactViaAddress_(contact, dismiss: dismiss, incognito: incognito, cleanup: cleanup)
} else { } else {
await MainActor.run {
showActionSheet(.askCurrentOrIncognitoProfileConnectContactViaAddress(contact: contact)) showActionSheet(.askCurrentOrIncognitoProfileConnectContactViaAddress(contact: contact))
} }
} }
}
case let .groupLink(glp): case let .groupLink(glp):
switch glp { switch glp {
case .ok: case .ok:
await MainActor.run {
if let incognito = incognito { if let incognito = incognito {
showAlert(.groupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) showAlert(.groupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
} else { } else {
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Join group")) showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "Join group"))
} }
}
case let .ownLink(groupInfo): case let .ownLink(groupInfo):
logger.debug("planAndConnect, .groupLink, .ownLink, incognito=\(incognito?.description ?? "nil")") logger.debug("planAndConnect, .groupLink, .ownLink, incognito=\(incognito?.description ?? "nil")")
await MainActor.run {
if let f = filterKnownGroup { if let f = filterKnownGroup {
f(groupInfo) f(groupInfo)
} }
showActionSheet(.ownGroupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito, groupInfo: groupInfo)) showActionSheet(.ownGroupLinkConfirmConnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito, groupInfo: groupInfo))
}
case .connectingConfirmReconnect: case .connectingConfirmReconnect:
logger.debug("planAndConnect, .groupLink, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")") logger.debug("planAndConnect, .groupLink, .connectingConfirmReconnect, incognito=\(incognito?.description ?? "nil")")
await MainActor.run {
if let incognito = incognito { if let incognito = incognito {
showAlert(.groupLinkConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito)) showAlert(.groupLinkConnectingConfirmReconnect(connectionLink: connectionLink, connectionPlan: connectionPlan, incognito: incognito))
} else { } else {
showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You are already joining the group!\nRepeat join request?")) showActionSheet(.askCurrentOrIncognitoProfileDestructive(connectionLink: connectionLink, connectionPlan: connectionPlan, title: "You are already joining the group!\nRepeat join request?"))
} }
}
case let .connectingProhibit(groupInfo_): case let .connectingProhibit(groupInfo_):
logger.debug("planAndConnect, .groupLink, .connectingProhibit, incognito=\(incognito?.description ?? "nil")") logger.debug("planAndConnect, .groupLink, .connectingProhibit, incognito=\(incognito?.description ?? "nil")")
await MainActor.run {
showAlert(.groupLinkConnecting(connectionLink: connectionLink, groupInfo: groupInfo_)) showAlert(.groupLinkConnecting(connectionLink: connectionLink, groupInfo: groupInfo_))
}
case let .known(groupInfo): case let .known(groupInfo):
logger.debug("planAndConnect, .groupLink, .known, incognito=\(incognito?.description ?? "nil")") logger.debug("planAndConnect, .groupLink, .known, incognito=\(incognito?.description ?? "nil")")
await MainActor.run {
if let f = filterKnownGroup { if let f = filterKnownGroup {
f(groupInfo) f(groupInfo)
} else { } else {
@ -1134,14 +1170,19 @@ func planAndConnect(
} }
} }
} }
} catch { case let .error(chatError):
logger.debug("planAndConnect, plan error") logger.debug("planAndConnect, .error \(chatErrorString(chatError))")
if let incognito = incognito { if let incognito = incognito {
connectViaLink(connectionLink, connectionPlan: nil, dismiss: dismiss, incognito: incognito, cleanup: cleanup) connectViaLink(connectionLink, connectionPlan: nil, dismiss: dismiss, incognito: incognito, cleanup: cleanup)
} else { } else {
showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: nil, title: "Connect via link")) showActionSheet(.askCurrentOrIncognitoProfile(connectionLink: connectionLink, connectionPlan: nil, title: "Connect via link"))
} }
} }
} else if let alert {
await MainActor.run {
showAlert(.error(shortOrFullLink: shortOrFullLink, alert: alert))
}
}
} }
} }
@ -1161,22 +1202,22 @@ private func connectContactViaAddress_(_ contact: Contact, dismiss: Bool, incogn
} }
private func connectViaLink( private func connectViaLink(
_ connectionLink: String, _ connectionLink: CreatedConnLink,
connectionPlan: ConnectionPlan?, connectionPlan: ConnectionPlan?,
dismiss: Bool, dismiss: Bool,
incognito: Bool, incognito: Bool,
cleanup: (() -> Void)? cleanup: (() -> Void)?
) { ) {
Task { Task {
if let (connReqType, pcc) = await apiConnect(incognito: incognito, connReq: connectionLink) { if let (connReqType, pcc) = await apiConnect(incognito: incognito, connLink: connectionLink) {
await MainActor.run { await MainActor.run {
ChatModel.shared.updateContactConnection(pcc) ChatModel.shared.updateContactConnection(pcc)
} }
let crt: ConnReqType let crt: ConnReqType
if let plan = connectionPlan { crt = if let plan = connectionPlan {
crt = planToConnReqType(plan) planToConnReqType(plan) ?? connReqType
} else { } else {
crt = connReqType connReqType
} }
DispatchQueue.main.async { DispatchQueue.main.async {
if dismiss { if dismiss {
@ -1199,10 +1240,8 @@ private func connectViaLink(
} }
func openKnownContact(_ contact: Contact, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) { func openKnownContact(_ contact: Contact, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
Task {
let m = ChatModel.shared let m = ChatModel.shared
if let c = m.getContactChat(contact.contactId) { if let c = m.getContactChat(contact.contactId) {
DispatchQueue.main.async {
if dismiss { if dismiss {
dismissAllSheets(animated: true) { dismissAllSheets(animated: true) {
ItemsModel.shared.loadOpenChat(c.id) { ItemsModel.shared.loadOpenChat(c.id) {
@ -1216,14 +1255,10 @@ func openKnownContact(_ contact: Contact, dismiss: Bool, showAlreadyExistsAlert:
} }
} }
} }
}
}
func openKnownGroup(_ groupInfo: GroupInfo, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) { func openKnownGroup(_ groupInfo: GroupInfo, dismiss: Bool, showAlreadyExistsAlert: (() -> Void)?) {
Task {
let m = ChatModel.shared let m = ChatModel.shared
if let g = m.getGroupChat(groupInfo.groupId) { if let g = m.getGroupChat(groupInfo.groupId) {
DispatchQueue.main.async {
if dismiss { if dismiss {
dismissAllSheets(animated: true) { dismissAllSheets(animated: true) {
ItemsModel.shared.loadOpenChat(g.id) { ItemsModel.shared.loadOpenChat(g.id) {
@ -1237,8 +1272,6 @@ func openKnownGroup(_ groupInfo: GroupInfo, dismiss: Bool, showAlreadyExistsAler
} }
} }
} }
}
}
func contactAlreadyConnectingAlert(_ contact: Contact) -> Alert { func contactAlreadyConnectingAlert(_ contact: Contact) -> Alert {
mkAlert( mkAlert(
@ -1273,11 +1306,12 @@ enum ConnReqType: Equatable {
} }
} }
private func planToConnReqType(_ connectionPlan: ConnectionPlan) -> ConnReqType { private func planToConnReqType(_ connectionPlan: ConnectionPlan) -> ConnReqType? {
switch connectionPlan { switch connectionPlan {
case .invitationLink: return .invitation case .invitationLink: .invitation
case .contactAddress: return .contact case .contactAddress: .contact
case .groupLink: return .groupLink case .groupLink: .groupLink
case .error: nil
} }
} }

View file

@ -8,6 +8,7 @@
import SwiftUI import SwiftUI
import CoreImage.CIFilterBuiltins import CoreImage.CIFilterBuiltins
import SimpleXChat
struct MutableQRCode: View { struct MutableQRCode: View {
@Binding var uri: String @Binding var uri: String
@ -20,6 +21,16 @@ struct MutableQRCode: View {
} }
} }
struct SimpleXCreatedLinkQRCode: View {
let link: CreatedConnLink
@Binding var short: Bool
var onShare: (() -> Void)? = nil
var body: some View {
QRCode(uri: link.simplexChatUri(short: short), onShare: onShare)
}
}
struct SimpleXLinkQRCode: View { struct SimpleXLinkQRCode: View {
let uri: String let uri: String
var withLogo: Bool = true var withLogo: Bool = true
@ -31,12 +42,6 @@ struct SimpleXLinkQRCode: View {
} }
} }
func simplexChatLink(_ uri: String) -> String {
uri.starts(with: "simplex:/")
? uri.replacingOccurrences(of: "simplex:/", with: "https://simplex.chat/")
: uri
}
struct QRCode: View { struct QRCode: View {
let uri: String let uri: String
var withLogo: Bool = true var withLogo: Bool = true

View file

@ -31,7 +31,7 @@ struct CreateSimpleXAddress: View {
Spacer() Spacer()
if let userAddress = m.userAddress { if let userAddress = m.userAddress {
SimpleXLinkQRCode(uri: userAddress.connReqContact) SimpleXCreatedLinkQRCode(link: userAddress.connLinkContact, short: Binding.constant(false))
.frame(maxHeight: g.size.width) .frame(maxHeight: g.size.width)
shareQRCodeButton(userAddress) shareQRCodeButton(userAddress)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
@ -77,9 +77,9 @@ struct CreateSimpleXAddress: View {
progressIndicator = true progressIndicator = true
Task { Task {
do { do {
let connReqContact = try await apiCreateUserAddress() let connLinkContact = try await apiCreateUserAddress(short: false)
DispatchQueue.main.async { DispatchQueue.main.async {
m.userAddress = UserContactLink(connReqContact: connReqContact) m.userAddress = UserContactLink(connLinkContact: connLinkContact)
} }
await MainActor.run { progressIndicator = false } await MainActor.run { progressIndicator = false }
} catch let error { } catch let error {
@ -121,7 +121,7 @@ struct CreateSimpleXAddress: View {
private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View { private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View {
Button { Button {
showShareSheet(items: [simplexChatLink(userAddress.connReqContact)]) showShareSheet(items: [simplexChatLink(userAddress.connLinkContact.simplexChatUri(short: false))])
} label: { } label: {
Label("Share", systemImage: "square.and.arrow.up") Label("Share", systemImage: "square.and.arrow.up")
} }
@ -189,7 +189,7 @@ struct SendAddressMailView: View {
let messageBody = String(format: NSLocalizedString(""" let messageBody = String(format: NSLocalizedString("""
<p>Hi!</p> <p>Hi!</p>
<p><a href="%@">Connect to me via SimpleX Chat</a></p> <p><a href="%@">Connect to me via SimpleX Chat</a></p>
""", comment: "email text"), simplexChatLink(userAddress.connReqContact)) """, comment: "email text"), simplexChatLink(userAddress.connLinkContact.simplexChatUri(short: false)))
MailView( MailView(
isShowing: self.$showMailView, isShowing: self.$showMailView,
result: $mailViewResult, result: $mailViewResult,

View file

@ -20,6 +20,8 @@ struct PrivacySettings: View {
@AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true @AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true
@AppStorage(GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS, store: groupDefaults) private var askToApproveRelays = true @AppStorage(GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS, store: groupDefaults) private var askToApproveRelays = true
@State private var simplexLinkMode = privacySimplexLinkModeDefault.get() @State private var simplexLinkMode = privacySimplexLinkModeDefault.get()
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@AppStorage(DEFAULT_PRIVACY_SHORT_LINKS) private var shortSimplexLinks = false
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false @AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
@State private var currentLAMode = privacyLocalAuthModeDefault.get() @State private var currentLAMode = privacyLocalAuthModeDefault.get()
@ -111,6 +113,11 @@ struct PrivacySettings: View {
.onChange(of: simplexLinkMode) { mode in .onChange(of: simplexLinkMode) { mode in
privacySimplexLinkModeDefault.set(mode) privacySimplexLinkModeDefault.set(mode)
} }
if developerTools {
settingsRow("link.badge.plus", color: theme.colors.secondary) {
Toggle("Use short links (BETA)", isOn: $shortSimplexLinks)
}
}
} header: { } header: {
Text("Chats") Text("Chats")
.foregroundColor(theme.colors.secondary) .foregroundColor(theme.colors.secondary)

View file

@ -33,6 +33,7 @@ let DEFAULT_PRIVACY_CHAT_LIST_OPEN_LINKS = "privacyChatListOpenLinks"
let DEFAULT_PRIVACY_SIMPLEX_LINK_MODE = "privacySimplexLinkMode" let DEFAULT_PRIVACY_SIMPLEX_LINK_MODE = "privacySimplexLinkMode"
let DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS = "privacyShowChatPreviews" let DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS = "privacyShowChatPreviews"
let DEFAULT_PRIVACY_SAVE_LAST_DRAFT = "privacySaveLastDraft" let DEFAULT_PRIVACY_SAVE_LAST_DRAFT = "privacySaveLastDraft"
let DEFAULT_PRIVACY_SHORT_LINKS = "privacyShortLinks"
let DEFAULT_PRIVACY_PROTECT_SCREEN = "privacyProtectScreen" let DEFAULT_PRIVACY_PROTECT_SCREEN = "privacyProtectScreen"
let DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET = "privacyDeliveryReceiptsSet" let DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET = "privacyDeliveryReceiptsSet"
let DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS = "privacyMediaBlurRadius" let DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS = "privacyMediaBlurRadius"
@ -99,6 +100,7 @@ let appDefaults: [String: Any] = [
DEFAULT_PRIVACY_SIMPLEX_LINK_MODE: SimpleXLinkMode.description.rawValue, DEFAULT_PRIVACY_SIMPLEX_LINK_MODE: SimpleXLinkMode.description.rawValue,
DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS: true, DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS: true,
DEFAULT_PRIVACY_SAVE_LAST_DRAFT: true, DEFAULT_PRIVACY_SAVE_LAST_DRAFT: true,
DEFAULT_PRIVACY_SHORT_LINKS: false,
DEFAULT_PRIVACY_PROTECT_SCREEN: false, DEFAULT_PRIVACY_PROTECT_SCREEN: false,
DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET: false, DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET: false,
DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS: 0, DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS: 0,

View file

@ -8,7 +8,7 @@
import SwiftUI import SwiftUI
import MessageUI import MessageUI
import SimpleXChat @preconcurrency import SimpleXChat
struct UserAddressView: View { struct UserAddressView: View {
@Environment(\.dismiss) var dismiss: DismissAction @Environment(\.dismiss) var dismiss: DismissAction
@ -16,6 +16,7 @@ struct UserAddressView: View {
@EnvironmentObject var theme: AppTheme @EnvironmentObject var theme: AppTheme
@State var shareViaProfile = false @State var shareViaProfile = false
@State var autoCreate = false @State var autoCreate = false
@State private var showShortLink = true
@State private var aas = AutoAcceptState() @State private var aas = AutoAcceptState()
@State private var savedAAS = AutoAcceptState() @State private var savedAAS = AutoAcceptState()
@State private var showMailView = false @State private var showMailView = false
@ -135,8 +136,8 @@ struct UserAddressView: View {
@ViewBuilder private func existingAddressView(_ userAddress: UserContactLink) -> some View { @ViewBuilder private func existingAddressView(_ userAddress: UserContactLink) -> some View {
Section { Section {
SimpleXLinkQRCode(uri: userAddress.connReqContact) SimpleXCreatedLinkQRCode(link: userAddress.connLinkContact, short: $showShortLink)
.id("simplex-contact-address-qrcode-\(userAddress.connReqContact)") .id("simplex-contact-address-qrcode-\(userAddress.connLinkContact.simplexChatUri(short: showShortLink))")
shareQRCodeButton(userAddress) shareQRCodeButton(userAddress)
// if MFMailComposeViewController.canSendMail() { // if MFMailComposeViewController.canSendMail() {
// shareViaEmailButton(userAddress) // shareViaEmailButton(userAddress)
@ -153,8 +154,7 @@ struct UserAddressView: View {
} }
addressSettingsButton(userAddress) addressSettingsButton(userAddress)
} header: { } header: {
Text("For social media") ToggleShortLinkHeader(text: Text("For social media"), link: userAddress.connLinkContact, short: $showShortLink)
.foregroundColor(theme.colors.secondary)
} footer: { } footer: {
if aas.business { if aas.business {
Text("Add your team members to the conversations.") Text("Add your team members to the conversations.")
@ -193,9 +193,10 @@ struct UserAddressView: View {
progressIndicator = true progressIndicator = true
Task { Task {
do { do {
let connReqContact = try await apiCreateUserAddress() let short = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS)
let connLinkContact = try await apiCreateUserAddress(short: short)
DispatchQueue.main.async { DispatchQueue.main.async {
chatModel.userAddress = UserContactLink(connReqContact: connReqContact) chatModel.userAddress = UserContactLink(connLinkContact: connLinkContact)
alert = .shareOnCreate alert = .shareOnCreate
progressIndicator = false progressIndicator = false
} }
@ -231,7 +232,7 @@ struct UserAddressView: View {
private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View { private func shareQRCodeButton(_ userAddress: UserContactLink) -> some View {
Button { Button {
showShareSheet(items: [simplexChatLink(userAddress.connReqContact)]) showShareSheet(items: [simplexChatLink(userAddress.connLinkContact.simplexChatUri(short: showShortLink))])
} label: { } label: {
settingsRow("square.and.arrow.up", color: theme.colors.secondary) { settingsRow("square.and.arrow.up", color: theme.colors.secondary) {
Text("Share address") Text("Share address")
@ -294,6 +295,28 @@ struct UserAddressView: View {
} }
} }
struct ToggleShortLinkHeader: View {
@EnvironmentObject var theme: AppTheme
let text: Text
var link: CreatedConnLink
@Binding var short: Bool
var body: some View {
if link.connShortLink == nil {
text.foregroundColor(theme.colors.secondary)
} else {
HStack {
text.foregroundColor(theme.colors.secondary)
Spacer()
Text(short ? "Full link" : "Short link")
.textCase(.none)
.foregroundColor(theme.colors.primary)
.onTapGesture { short.toggle() }
}
}
}
}
private struct AutoAcceptState: Equatable { private struct AutoAcceptState: Equatable {
var enable = false var enable = false
var incognito = false var incognito = false
@ -542,7 +565,7 @@ private func saveAAS(_ aas: Binding<AutoAcceptState>, _ savedAAS: Binding<AutoAc
struct UserAddressView_Previews: PreviewProvider { struct UserAddressView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let chatModel = ChatModel() let chatModel = ChatModel()
chatModel.userAddress = UserContactLink(connReqContact: "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D") chatModel.userAddress = UserContactLink(connLinkContact: CreatedConnLink(connFullLink: "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", connShortLink: nil))
return Group { return Group {

View file

@ -9,6 +9,10 @@
<string>applinks:simplex.chat</string> <string>applinks:simplex.chat</string>
<string>applinks:www.simplex.chat</string> <string>applinks:www.simplex.chat</string>
<string>applinks:simplex.chat?mode=developer</string> <string>applinks:simplex.chat?mode=developer</string>
<string>applinks:*.simplex.im</string>
<string>applinks:*.simplex.im?mode=developer</string>
<string>applinks:*.simplexonflux.com</string>
<string>applinks:*.simplexonflux.com?mode=developer</string>
</array> </array>
<key>com.apple.security.application-groups</key> <key>com.apple.security.application-groups</key>
<array> <array>

View file

@ -324,7 +324,7 @@ public func responseError(_ err: Error) -> String {
} }
} }
func chatErrorString(_ err: ChatError) -> String { public func chatErrorString(_ err: ChatError) -> String {
if case let .invalidJSON(json) = err { return json } if case let .invalidJSON(json) = err { return json }
return String(describing: err) return String(describing: err)
} }

View file

@ -77,7 +77,7 @@ public enum ChatCommand {
case apiLeaveGroup(groupId: Int64) case apiLeaveGroup(groupId: Int64)
case apiListMembers(groupId: Int64) case apiListMembers(groupId: Int64)
case apiUpdateGroupProfile(groupId: Int64, groupProfile: GroupProfile) case apiUpdateGroupProfile(groupId: Int64, groupProfile: GroupProfile)
case apiCreateGroupLink(groupId: Int64, memberRole: GroupMemberRole) case apiCreateGroupLink(groupId: Int64, memberRole: GroupMemberRole, short: Bool)
case apiGroupLinkMemberRole(groupId: Int64, memberRole: GroupMemberRole) case apiGroupLinkMemberRole(groupId: Int64, memberRole: GroupMemberRole)
case apiDeleteGroupLink(groupId: Int64) case apiDeleteGroupLink(groupId: Int64)
case apiGetGroupLink(groupId: Int64) case apiGetGroupLink(groupId: Int64)
@ -116,11 +116,11 @@ public enum ChatCommand {
case apiGetGroupMemberCode(groupId: Int64, groupMemberId: Int64) case apiGetGroupMemberCode(groupId: Int64, groupMemberId: Int64)
case apiVerifyContact(contactId: Int64, connectionCode: String?) case apiVerifyContact(contactId: Int64, connectionCode: String?)
case apiVerifyGroupMember(groupId: Int64, groupMemberId: Int64, connectionCode: String?) case apiVerifyGroupMember(groupId: Int64, groupMemberId: Int64, connectionCode: String?)
case apiAddContact(userId: Int64, incognito: Bool) case apiAddContact(userId: Int64, short: Bool, incognito: Bool)
case apiSetConnectionIncognito(connId: Int64, incognito: Bool) case apiSetConnectionIncognito(connId: Int64, incognito: Bool)
case apiChangeConnectionUser(connId: Int64, userId: Int64) case apiChangeConnectionUser(connId: Int64, userId: Int64)
case apiConnectPlan(userId: Int64, connReq: String) case apiConnectPlan(userId: Int64, connLink: String)
case apiConnect(userId: Int64, incognito: Bool, connReq: String) case apiConnect(userId: Int64, incognito: Bool, connLink: CreatedConnLink)
case apiConnectContactViaAddress(userId: Int64, incognito: Bool, contactId: Int64) case apiConnectContactViaAddress(userId: Int64, incognito: Bool, contactId: Int64)
case apiDeleteChat(type: ChatType, id: Int64, chatDeleteMode: ChatDeleteMode) case apiDeleteChat(type: ChatType, id: Int64, chatDeleteMode: ChatDeleteMode)
case apiClearChat(type: ChatType, id: Int64) case apiClearChat(type: ChatType, id: Int64)
@ -132,7 +132,7 @@ public enum ChatCommand {
case apiSetConnectionAlias(connId: Int64, localAlias: String) case apiSetConnectionAlias(connId: Int64, localAlias: String)
case apiSetUserUIThemes(userId: Int64, themes: ThemeModeOverrides?) case apiSetUserUIThemes(userId: Int64, themes: ThemeModeOverrides?)
case apiSetChatUIThemes(chatId: String, themes: ThemeModeOverrides?) case apiSetChatUIThemes(chatId: String, themes: ThemeModeOverrides?)
case apiCreateMyAddress(userId: Int64) case apiCreateMyAddress(userId: Int64, short: Bool)
case apiDeleteMyAddress(userId: Int64) case apiDeleteMyAddress(userId: Int64)
case apiShowMyAddress(userId: Int64) case apiShowMyAddress(userId: Int64)
case apiSetProfileAddress(userId: Int64, on: Bool) case apiSetProfileAddress(userId: Int64, on: Bool)
@ -256,7 +256,7 @@ public enum ChatCommand {
case let .apiLeaveGroup(groupId): return "/_leave #\(groupId)" case let .apiLeaveGroup(groupId): return "/_leave #\(groupId)"
case let .apiListMembers(groupId): return "/_members #\(groupId)" case let .apiListMembers(groupId): return "/_members #\(groupId)"
case let .apiUpdateGroupProfile(groupId, groupProfile): return "/_group_profile #\(groupId) \(encodeJSON(groupProfile))" case let .apiUpdateGroupProfile(groupId, groupProfile): return "/_group_profile #\(groupId) \(encodeJSON(groupProfile))"
case let .apiCreateGroupLink(groupId, memberRole): return "/_create link #\(groupId) \(memberRole)" case let .apiCreateGroupLink(groupId, memberRole, short): return "/_create link #\(groupId) \(memberRole) short=\(onOff(short))"
case let .apiGroupLinkMemberRole(groupId, memberRole): return "/_set link role #\(groupId) \(memberRole)" case let .apiGroupLinkMemberRole(groupId, memberRole): return "/_set link role #\(groupId) \(memberRole)"
case let .apiDeleteGroupLink(groupId): return "/_delete link #\(groupId)" case let .apiDeleteGroupLink(groupId): return "/_delete link #\(groupId)"
case let .apiGetGroupLink(groupId): return "/_get link #\(groupId)" case let .apiGetGroupLink(groupId): return "/_get link #\(groupId)"
@ -305,11 +305,11 @@ public enum ChatCommand {
case let .apiVerifyContact(contactId, .none): return "/_verify code @\(contactId)" case let .apiVerifyContact(contactId, .none): return "/_verify code @\(contactId)"
case let .apiVerifyGroupMember(groupId, groupMemberId, .some(connectionCode)): return "/_verify code #\(groupId) \(groupMemberId) \(connectionCode)" case let .apiVerifyGroupMember(groupId, groupMemberId, .some(connectionCode)): return "/_verify code #\(groupId) \(groupMemberId) \(connectionCode)"
case let .apiVerifyGroupMember(groupId, groupMemberId, .none): return "/_verify code #\(groupId) \(groupMemberId)" case let .apiVerifyGroupMember(groupId, groupMemberId, .none): return "/_verify code #\(groupId) \(groupMemberId)"
case let .apiAddContact(userId, incognito): return "/_connect \(userId) incognito=\(onOff(incognito))" case let .apiAddContact(userId, short, incognito): return "/_connect \(userId) short=\(onOff(short)) incognito=\(onOff(incognito))"
case let .apiSetConnectionIncognito(connId, incognito): return "/_set incognito :\(connId) \(onOff(incognito))" case let .apiSetConnectionIncognito(connId, incognito): return "/_set incognito :\(connId) \(onOff(incognito))"
case let .apiChangeConnectionUser(connId, userId): return "/_set conn user :\(connId) \(userId)" case let .apiChangeConnectionUser(connId, userId): return "/_set conn user :\(connId) \(userId)"
case let .apiConnectPlan(userId, connReq): return "/_connect plan \(userId) \(connReq)" case let .apiConnectPlan(userId, connLink): return "/_connect plan \(userId) \(connLink)"
case let .apiConnect(userId, incognito, connReq): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connReq)" 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 .apiConnectContactViaAddress(userId, incognito, contactId): return "/_connect contact \(userId) incognito=\(onOff(incognito)) \(contactId)"
case let .apiDeleteChat(type, id, chatDeleteMode): return "/_delete \(ref(type, id)) \(chatDeleteMode.cmdString)" case let .apiDeleteChat(type, id, chatDeleteMode): return "/_delete \(ref(type, id)) \(chatDeleteMode.cmdString)"
case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))" case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))"
@ -321,7 +321,7 @@ public enum ChatCommand {
case let .apiSetConnectionAlias(connId, localAlias): return "/_set alias :\(connId) \(localAlias.trimmingCharacters(in: .whitespaces))" case let .apiSetConnectionAlias(connId, localAlias): return "/_set alias :\(connId) \(localAlias.trimmingCharacters(in: .whitespaces))"
case let .apiSetUserUIThemes(userId, themes): return "/_set theme user \(userId) \(themes != nil ? encodeJSON(themes) : "")" case let .apiSetUserUIThemes(userId, themes): return "/_set theme user \(userId) \(themes != nil ? encodeJSON(themes) : "")"
case let .apiSetChatUIThemes(chatId, themes): return "/_set theme \(chatId) \(themes != nil ? encodeJSON(themes) : "")" case let .apiSetChatUIThemes(chatId, themes): return "/_set theme \(chatId) \(themes != nil ? encodeJSON(themes) : "")"
case let .apiCreateMyAddress(userId): return "/_address \(userId)" case let .apiCreateMyAddress(userId, short): return "/_address \(userId) short=\(onOff(short))"
case let .apiDeleteMyAddress(userId): return "/_delete_address \(userId)" case let .apiDeleteMyAddress(userId): return "/_delete_address \(userId)"
case let .apiShowMyAddress(userId): return "/_show_address \(userId)" case let .apiShowMyAddress(userId): return "/_show_address \(userId)"
case let .apiSetProfileAddress(userId, on): return "/_profile_address \(userId) \(onOff(on))" case let .apiSetProfileAddress(userId, on): return "/_profile_address \(userId) \(onOff(on))"
@ -629,10 +629,10 @@ public enum ChatResponse: Decodable, Error {
case groupMemberCode(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionCode: String) case groupMemberCode(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionCode: String)
case connectionVerified(user: UserRef, verified: Bool, expectedCode: String) case connectionVerified(user: UserRef, verified: Bool, expectedCode: String)
case tagsUpdated(user: UserRef, userTags: [ChatTag], chatTags: [Int64]) case tagsUpdated(user: UserRef, userTags: [ChatTag], chatTags: [Int64])
case invitation(user: UserRef, connReqInvitation: String, connection: PendingContactConnection) case invitation(user: UserRef, connLinkInvitation: CreatedConnLink, connection: PendingContactConnection)
case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection) case connectionIncognitoUpdated(user: UserRef, toConnection: PendingContactConnection)
case connectionUserChanged(user: UserRef, fromConnection: PendingContactConnection, toConnection: PendingContactConnection, newUser: UserRef) case connectionUserChanged(user: UserRef, fromConnection: PendingContactConnection, toConnection: PendingContactConnection, newUser: UserRef)
case connectionPlan(user: UserRef, connectionPlan: ConnectionPlan) case connectionPlan(user: UserRef, connLink: CreatedConnLink, connectionPlan: ConnectionPlan)
case sentConfirmation(user: UserRef, connection: PendingContactConnection) case sentConfirmation(user: UserRef, connection: PendingContactConnection)
case sentInvitation(user: UserRef, connection: PendingContactConnection) case sentInvitation(user: UserRef, connection: PendingContactConnection)
case sentInvitationToContact(user: UserRef, contact: Contact, customUserProfile: Profile?) case sentInvitationToContact(user: UserRef, contact: Contact, customUserProfile: Profile?)
@ -649,7 +649,7 @@ public enum ChatResponse: Decodable, Error {
case contactPrefsUpdated(user: User, fromContact: Contact, toContact: Contact) case contactPrefsUpdated(user: User, fromContact: Contact, toContact: Contact)
case userContactLink(user: User, contactLink: UserContactLink) case userContactLink(user: User, contactLink: UserContactLink)
case userContactLinkUpdated(user: User, contactLink: UserContactLink) case userContactLinkUpdated(user: User, contactLink: UserContactLink)
case userContactLinkCreated(user: User, connReqContact: String) case userContactLinkCreated(user: User, connLinkContact: CreatedConnLink)
case userContactLinkDeleted(user: User) case userContactLinkDeleted(user: User)
case contactConnected(user: UserRef, contact: Contact, userCustomProfile: Profile?) case contactConnected(user: UserRef, contact: Contact, userCustomProfile: Profile?)
case contactConnecting(user: UserRef, contact: Contact) case contactConnecting(user: UserRef, contact: Contact)
@ -702,8 +702,8 @@ public enum ChatResponse: Decodable, Error {
case connectedToGroupMember(user: UserRef, groupInfo: GroupInfo, member: GroupMember, memberContact: Contact?) case connectedToGroupMember(user: UserRef, groupInfo: GroupInfo, member: GroupMember, memberContact: Contact?)
case groupRemoved(user: UserRef, groupInfo: GroupInfo) // unused case groupRemoved(user: UserRef, groupInfo: GroupInfo) // unused
case groupUpdated(user: UserRef, toGroup: GroupInfo) case groupUpdated(user: UserRef, toGroup: GroupInfo)
case groupLinkCreated(user: UserRef, groupInfo: GroupInfo, connReqContact: String, memberRole: GroupMemberRole) case groupLinkCreated(user: UserRef, groupInfo: GroupInfo, connLinkContact: CreatedConnLink, memberRole: GroupMemberRole)
case groupLink(user: UserRef, groupInfo: GroupInfo, connReqContact: String, memberRole: GroupMemberRole) case groupLink(user: UserRef, groupInfo: GroupInfo, connLinkContact: CreatedConnLink, memberRole: GroupMemberRole)
case groupLinkDeleted(user: UserRef, groupInfo: GroupInfo) case groupLinkDeleted(user: UserRef, groupInfo: GroupInfo)
case newMemberContact(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember) case newMemberContact(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember)
case newMemberContactSentInv(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember) case newMemberContactSentInv(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember)
@ -989,10 +989,10 @@ public enum ChatResponse: Decodable, Error {
case let .groupMemberCode(u, groupInfo, member, connectionCode): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionCode: \(connectionCode)") case let .groupMemberCode(u, groupInfo, member, connectionCode): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionCode: \(connectionCode)")
case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)") case let .connectionVerified(u, verified, expectedCode): return withUser(u, "verified: \(verified)\nconnectionCode: \(expectedCode)")
case let .tagsUpdated(u, userTags, chatTags): return withUser(u, "userTags: \(String(describing: userTags))\nchatTags: \(String(describing: chatTags))") case let .tagsUpdated(u, userTags, chatTags): return withUser(u, "userTags: \(String(describing: userTags))\nchatTags: \(String(describing: chatTags))")
case let .invitation(u, connReqInvitation, connection): return withUser(u, "connReqInvitation: \(connReqInvitation)\nconnection: \(connection)") case let .invitation(u, connLinkInvitation, connection): return withUser(u, "connLinkInvitation: \(connLinkInvitation)\nconnection: \(connection)")
case let .connectionIncognitoUpdated(u, toConnection): return withUser(u, String(describing: toConnection)) 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))\newUserId: \(String(describing: newUser.userId))") 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, connectionPlan): return withUser(u, String(describing: connectionPlan)) case let .connectionPlan(u, connLink, connectionPlan): return withUser(u, "connLink: \(String(describing: connLink))\nconnectionPlan: \(String(describing: connectionPlan))")
case let .sentConfirmation(u, connection): return withUser(u, String(describing: connection)) case let .sentConfirmation(u, connection): return withUser(u, String(describing: connection))
case let .sentInvitation(u, connection): return withUser(u, String(describing: connection)) case let .sentInvitation(u, connection): return withUser(u, String(describing: connection))
case let .sentInvitationToContact(u, contact, _): return withUser(u, String(describing: contact)) case let .sentInvitationToContact(u, contact, _): return withUser(u, String(describing: contact))
@ -1009,7 +1009,7 @@ public enum ChatResponse: Decodable, Error {
case let .contactPrefsUpdated(u, fromContact, toContact): return withUser(u, "fromContact: \(String(describing: fromContact))\ntoContact: \(String(describing: toContact))") case let .contactPrefsUpdated(u, fromContact, toContact): return withUser(u, "fromContact: \(String(describing: fromContact))\ntoContact: \(String(describing: toContact))")
case let .userContactLink(u, contactLink): return withUser(u, contactLink.responseDetails) case let .userContactLink(u, contactLink): return withUser(u, contactLink.responseDetails)
case let .userContactLinkUpdated(u, contactLink): return withUser(u, contactLink.responseDetails) case let .userContactLinkUpdated(u, contactLink): return withUser(u, contactLink.responseDetails)
case let .userContactLinkCreated(u, connReq): return withUser(u, connReq) case let .userContactLinkCreated(u, connLink): return withUser(u, String(describing: connLink))
case .userContactLinkDeleted: return noDetails case .userContactLinkDeleted: return noDetails
case let .contactConnected(u, contact, _): return withUser(u, String(describing: contact)) case let .contactConnected(u, contact, _): return withUser(u, String(describing: contact))
case let .contactConnecting(u, contact): return withUser(u, String(describing: contact)) case let .contactConnecting(u, contact): return withUser(u, String(describing: contact))
@ -1069,8 +1069,8 @@ public enum ChatResponse: Decodable, Error {
case let .connectedToGroupMember(u, groupInfo, member, memberContact): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)\nmemberContact: \(String(describing: memberContact))") case let .connectedToGroupMember(u, groupInfo, member, memberContact): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(member)\nmemberContact: \(String(describing: memberContact))")
case let .groupRemoved(u, groupInfo): return withUser(u, String(describing: groupInfo)) case let .groupRemoved(u, groupInfo): return withUser(u, String(describing: groupInfo))
case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup)) case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup))
case let .groupLinkCreated(u, groupInfo, connReqContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)\nmemberRole: \(memberRole)") case let .groupLinkCreated(u, groupInfo, connLinkContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnLinkContact: \(connLinkContact)\nmemberRole: \(memberRole)")
case let .groupLink(u, groupInfo, connReqContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnReqContact: \(connReqContact)\nmemberRole: \(memberRole)") case let .groupLink(u, groupInfo, connLinkContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnLinkContact: \(connLinkContact)\nmemberRole: \(memberRole)")
case let .groupLinkDeleted(u, groupInfo): return withUser(u, String(describing: groupInfo)) case let .groupLinkDeleted(u, groupInfo): return withUser(u, String(describing: groupInfo))
case let .newMemberContact(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)") case let .newMemberContact(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)")
case let .newMemberContactSentInv(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)") case let .newMemberContactSentInv(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)")
@ -1173,10 +1173,31 @@ public enum ChatDeleteMode: Codable {
} }
} }
public struct CreatedConnLink: Decodable, Hashable {
public var connFullLink: String
public var connShortLink: String?
public init(connFullLink: String, connShortLink: String?) {
self.connFullLink = connFullLink
self.connShortLink = connShortLink
}
public func simplexChatUri(short: Bool = true) -> String {
short ? (connShortLink ?? simplexChatLink(connFullLink)) : simplexChatLink(connFullLink)
}
}
public func simplexChatLink(_ uri: String) -> String {
uri.starts(with: "simplex:/")
? uri.replacingOccurrences(of: "simplex:/", with: "https://simplex.chat/")
: uri
}
public enum ConnectionPlan: Decodable, Hashable { public enum ConnectionPlan: Decodable, Hashable {
case invitationLink(invitationLinkPlan: InvitationLinkPlan) case invitationLink(invitationLinkPlan: InvitationLinkPlan)
case contactAddress(contactAddressPlan: ContactAddressPlan) case contactAddress(contactAddressPlan: ContactAddressPlan)
case groupLink(groupLinkPlan: GroupLinkPlan) case groupLink(groupLinkPlan: GroupLinkPlan)
case error(chatError: ChatError)
} }
public enum InvitationLinkPlan: Decodable, Hashable { public enum InvitationLinkPlan: Decodable, Hashable {
@ -2183,16 +2204,16 @@ public enum RatchetSyncState: String, Decodable {
} }
public struct UserContactLink: Decodable, Hashable { public struct UserContactLink: Decodable, Hashable {
public var connReqContact: String public var connLinkContact: CreatedConnLink
public var autoAccept: AutoAccept? public var autoAccept: AutoAccept?
public init(connReqContact: String, autoAccept: AutoAccept? = nil) { public init(connLinkContact: CreatedConnLink, autoAccept: AutoAccept? = nil) {
self.connReqContact = connReqContact self.connLinkContact = connLinkContact
self.autoAccept = autoAccept self.autoAccept = autoAccept
} }
var responseDetails: String { var responseDetails: String {
"connReqContact: \(connReqContact)\nautoAccept: \(AutoAccept.cmdString(autoAccept))" "connLinkContact: \(connLinkContact)\nautoAccept: \(AutoAccept.cmdString(autoAccept))"
} }
} }
@ -2404,8 +2425,8 @@ public enum ChatErrorType: Decodable, Hashable {
case chatNotStarted case chatNotStarted
case chatNotStopped case chatNotStopped
case chatStoreChanged case chatStoreChanged
case connectionPlan(connectionPlan: ConnectionPlan)
case invalidConnReq case invalidConnReq
case unsupportedConnReq
case invalidChatMessage(connection: Connection, message: String) case invalidChatMessage(connection: Connection, message: String)
case contactNotReady(contact: Contact) case contactNotReady(contact: Contact)
case contactNotActive(contact: Contact) case contactNotActive(contact: Contact)
@ -2521,6 +2542,7 @@ public enum StoreError: Decodable, Hashable {
case hostMemberIdNotFound(groupId: Int64) case hostMemberIdNotFound(groupId: Int64)
case contactNotFoundByFileId(fileId: Int64) case contactNotFoundByFileId(fileId: Int64)
case noGroupSndStatus(itemId: Int64, groupMemberId: Int64) case noGroupSndStatus(itemId: Int64, groupMemberId: Int64)
case dBException(message: String)
} }
public enum DatabaseError: Decodable, Hashable { public enum DatabaseError: Decodable, Hashable {

View file

@ -1853,7 +1853,7 @@ public struct PendingContactConnection: Decodable, NamedChat, Hashable {
public var viaContactUri: Bool public var viaContactUri: Bool
public var groupLinkId: String? public var groupLinkId: String?
public var customUserProfileId: Int64? public var customUserProfileId: Int64?
public var connReqInv: String? public var connLinkInv: CreatedConnLink?
public var localAlias: String public var localAlias: String
var createdAt: Date var createdAt: Date
public var updatedAt: Date public var updatedAt: Date
@ -4063,12 +4063,14 @@ public enum SimplexLinkType: String, Decodable, Hashable {
case contact case contact
case invitation case invitation
case group case group
case channel
public var description: String { public var description: String {
switch self { switch self {
case .contact: return NSLocalizedString("SimpleX contact address", comment: "simplex link type") case .contact: return NSLocalizedString("SimpleX contact address", comment: "simplex link type")
case .invitation: return NSLocalizedString("SimpleX one-time invitation", comment: "simplex link type") case .invitation: return NSLocalizedString("SimpleX one-time invitation", comment: "simplex link type")
case .group: return NSLocalizedString("SimpleX group link", comment: "simplex link type") case .group: return NSLocalizedString("SimpleX group link", comment: "simplex link type")
case .channel: return NSLocalizedString("SimpleX channel link", comment: "simplex link type")
} }
} }
} }

View file

@ -77,8 +77,33 @@
<data android:scheme="http" /> <data android:scheme="http" />
<data android:scheme="https" /> <data android:scheme="https" />
<data android:host="simplex.chat" /> <data android:host="simplex.chat" />
<data android:host="smp4.simplex.im" />
<data android:host="smp5.simplex.im" />
<data android:host="smp6.simplex.im" />
<data android:host="smp7.simplex.im" /> <!-- TODO remove -->
<data android:host="smp8.simplex.im" />
<data android:host="smp9.simplex.im" />
<data android:host="smp10.simplex.im" />
<data android:host="smp11.simplex.im" />
<data android:host="smp12.simplex.im" />
<data android:host="smp14.simplex.im" />
<data android:host="smp15.simplex.im" />
<data android:host="smp16.simplex.im" />
<data android:host="smp17.simplex.im" />
<data android:host="smp18.simplex.im" />
<data android:host="smp19.simplex.im" />
<data android:host="smp1.simplexonflux.com" />
<data android:host="smp2.simplexonflux.com" />
<data android:host="smp3.simplexonflux.com" />
<data android:host="smp4.simplexonflux.com" />
<data android:host="smp5.simplexonflux.com" />
<data android:host="smp6.simplexonflux.com" />
<data android:pathPrefix="/invitation" /> <data android:pathPrefix="/invitation" />
<data android:pathPrefix="/contact" /> <data android:pathPrefix="/contact" />
<data android:pathPrefix="/a" />
<data android:pathPrefix="/c" />
<data android:pathPrefix="/g" />
<data android:pathPrefix="/i" />
</intent-filter> </intent-filter>
<!-- Receive files from other apps --> <!-- Receive files from other apps -->
<intent-filter> <intent-filter>

View file

@ -1078,7 +1078,7 @@ interface ChatItemsChangesListener {
data class ShowingInvitation( data class ShowingInvitation(
val connId: String, val connId: String,
val connReq: String, val connLink: CreatedConnLink,
val connChatUsed: Boolean, val connChatUsed: Boolean,
val conn: PendingContactConnection val conn: PendingContactConnection
) )
@ -2198,7 +2198,7 @@ class PendingContactConnection(
val viaContactUri: Boolean, val viaContactUri: Boolean,
val groupLinkId: String? = null, val groupLinkId: String? = null,
val customUserProfileId: Long? = null, val customUserProfileId: Long? = null,
val connReqInv: String? = null, val connLinkInv: CreatedConnLink? = null,
override val localAlias: String, override val localAlias: String,
override val createdAt: Instant, override val createdAt: Instant,
override val updatedAt: Instant override val updatedAt: Instant
@ -3968,12 +3968,14 @@ sealed class Format {
enum class SimplexLinkType(val linkType: String) { enum class SimplexLinkType(val linkType: String) {
contact("contact"), contact("contact"),
invitation("invitation"), invitation("invitation"),
group("group"); group("group"),
channel("channel");
val description: String get() = generalGetString(when (this) { val description: String get() = generalGetString(when (this) {
contact -> MR.strings.simplex_link_contact contact -> MR.strings.simplex_link_contact
invitation -> MR.strings.simplex_link_invitation invitation -> MR.strings.simplex_link_invitation
group -> MR.strings.simplex_link_group group -> MR.strings.simplex_link_group
channel -> MR.strings.simplex_link_channel
}) })
} }

View file

@ -5,6 +5,8 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
@ -119,6 +121,7 @@ class AppPreferences {
) )
val privacyShowChatPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS, true) val privacyShowChatPreviews = mkBoolPreference(SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS, true)
val privacySaveLastDraft = mkBoolPreference(SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT, true) val privacySaveLastDraft = mkBoolPreference(SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT, true)
val privacyShortLinks = mkBoolPreference(SHARED_PREFS_PRIVACY_SHORT_LINKS, false)
val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false) val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false)
val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true) val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true)
val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true) val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true)
@ -378,6 +381,7 @@ class AppPreferences {
private const val SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE = "PrivacySimplexLinkMode" private const val SHARED_PREFS_PRIVACY_SIMPLEX_LINK_MODE = "PrivacySimplexLinkMode"
private const val SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS = "PrivacyShowChatPreviews" private const val SHARED_PREFS_PRIVACY_SHOW_CHAT_PREVIEWS = "PrivacyShowChatPreviews"
private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft" private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft"
private const val SHARED_PREFS_PRIVACY_SHORT_LINKS = "PrivacyShortLinks"
private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet" private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet"
private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles" private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles"
private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays" private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays"
@ -1364,11 +1368,12 @@ object ChatController {
suspend fun apiAddContact(rh: Long?, incognito: Boolean): Pair<Pair<String, PendingContactConnection>?, (() -> Unit)?> { suspend fun apiAddContact(rh: Long?, incognito: Boolean): Pair<Pair<CreatedConnLink, PendingContactConnection>?, (() -> Unit)?> {
val userId = try { currentUserId("apiAddContact") } catch (e: Exception) { return null to null } val userId = try { currentUserId("apiAddContact") } catch (e: Exception) { return null to null }
val r = sendCmd(rh, CC.APIAddContact(userId, incognito)) val short = appPrefs.privacyShortLinks.get()
val r = sendCmd(rh, CC.APIAddContact(userId, short = short, incognito = incognito))
return when (r) { return when (r) {
is CR.Invitation -> (r.connReqInvitation to r.connection) to null is CR.Invitation -> (r.connLinkInvitation to r.connection) to null
else -> { else -> {
if (!(networkErrorAlert(r))) { if (!(networkErrorAlert(r))) {
return null to { apiErrorAlert("apiAddContact", generalGetString(MR.strings.connection_error), r) } return null to { apiErrorAlert("apiAddContact", generalGetString(MR.strings.connection_error), r) }
@ -1406,34 +1411,45 @@ object ChatController {
} }
} }
suspend fun apiConnectPlan(rh: Long?, connReq: String): ConnectionPlan? { suspend fun apiConnectPlan(rh: Long?, connLink: String): Pair<CreatedConnLink, ConnectionPlan>? {
val userId = kotlin.runCatching { currentUserId("apiConnectPlan") }.getOrElse { return null } val userId = kotlin.runCatching { currentUserId("apiConnectPlan") }.getOrElse { return null }
val r = sendCmd(rh, CC.APIConnectPlan(userId, connReq)) val r = sendCmd(rh, CC.APIConnectPlan(userId, connLink))
if (r is CR.CRConnectionPlan) return r.connectionPlan if (r is CR.CRConnectionPlan) return r.connLink to r.connectionPlan
Log.e(TAG, "apiConnectPlan bad response: ${r.responseType} ${r.details}") apiConnectResponseAlert(r)
return null return null
} }
suspend fun apiConnect(rh: Long?, incognito: Boolean, connReq: String): PendingContactConnection? { suspend fun apiConnect(rh: Long?, incognito: Boolean, connLink: CreatedConnLink): PendingContactConnection? {
val userId = try { currentUserId("apiConnect") } catch (e: Exception) { return null } val userId = try { currentUserId("apiConnect") } catch (e: Exception) { return null }
val r = sendCmd(rh, CC.APIConnect(userId, incognito, connReq)) val r = sendCmd(rh, CC.APIConnect(userId, incognito, connLink))
when { when {
r is CR.SentConfirmation -> return r.connection r is CR.SentConfirmation -> return r.connection
r is CR.SentInvitation -> return r.connection r is CR.SentInvitation -> return r.connection
r is CR.ContactAlreadyExists -> { r is CR.ContactAlreadyExists ->
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.contact_already_exists), generalGetString(MR.strings.contact_already_exists),
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), r.contact.displayName) String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), r.contact.displayName)
) )
else -> apiConnectResponseAlert(r)
}
return null return null
} }
private fun apiConnectResponseAlert(r: CR) {
when {
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat
&& r.chatError.errorType is ChatErrorType.InvalidConnReq -> { && r.chatError.errorType is ChatErrorType.InvalidConnReq -> {
AlertManager.shared.showAlertMsg( AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.invalid_connection_link), generalGetString(MR.strings.invalid_connection_link),
generalGetString(MR.strings.please_check_correct_link_and_maybe_ask_for_a_new_one) generalGetString(MR.strings.please_check_correct_link_and_maybe_ask_for_a_new_one)
) )
return null }
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorChat
&& r.chatError.errorType is ChatErrorType.UnsupportedConnReq -> {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.unsupported_connection_link),
generalGetString(MR.strings.link_requires_newer_app_version_please_upgrade)
)
} }
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent
&& r.chatError.agentError is AgentErrorType.SMP && r.chatError.agentError is AgentErrorType.SMP
@ -1442,7 +1458,6 @@ object ChatController {
generalGetString(MR.strings.connection_error_auth), generalGetString(MR.strings.connection_error_auth),
generalGetString(MR.strings.connection_error_auth_desc) generalGetString(MR.strings.connection_error_auth_desc)
) )
return null
} }
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent
&& r.chatError.agentError is AgentErrorType.SMP && r.chatError.agentError is AgentErrorType.SMP
@ -1451,7 +1466,6 @@ object ChatController {
generalGetString(MR.strings.connection_error_blocked), generalGetString(MR.strings.connection_error_blocked),
generalGetString(MR.strings.connection_error_blocked_desc).format(r.chatError.agentError.smpErr.blockInfo.reason.text), generalGetString(MR.strings.connection_error_blocked_desc).format(r.chatError.agentError.smpErr.blockInfo.reason.text),
) )
return null
} }
r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent
&& r.chatError.agentError is AgentErrorType.SMP && r.chatError.agentError is AgentErrorType.SMP
@ -1460,13 +1474,11 @@ object ChatController {
generalGetString(MR.strings.connection_error_quota), generalGetString(MR.strings.connection_error_quota),
generalGetString(MR.strings.connection_error_quota_desc) generalGetString(MR.strings.connection_error_quota_desc)
) )
return null
} }
else -> { else -> {
if (!(networkErrorAlert(r))) { if (!(networkErrorAlert(r))) {
apiErrorAlert("apiConnect", generalGetString(MR.strings.connection_error), r) apiErrorAlert("apiConnect", generalGetString(MR.strings.connection_error), r)
} }
return null
} }
} }
} }
@ -1619,11 +1631,11 @@ object ChatController {
return false return false
} }
suspend fun apiCreateUserAddress(rh: Long?): String? { suspend fun apiCreateUserAddress(rh: Long?, short: Boolean): CreatedConnLink? {
val userId = kotlin.runCatching { currentUserId("apiCreateUserAddress") }.getOrElse { return null } val userId = kotlin.runCatching { currentUserId("apiCreateUserAddress") }.getOrElse { return null }
val r = sendCmd(rh, CC.ApiCreateMyAddress(userId)) val r = sendCmd(rh, CC.ApiCreateMyAddress(userId, short))
return when (r) { return when (r) {
is CR.UserContactLinkCreated -> r.connReqContact is CR.UserContactLinkCreated -> r.connLinkContact
else -> { else -> {
if (!(networkErrorAlert(r))) { if (!(networkErrorAlert(r))) {
apiErrorAlert("apiCreateUserAddress", generalGetString(MR.strings.error_creating_address), r) apiErrorAlert("apiCreateUserAddress", generalGetString(MR.strings.error_creating_address), r)
@ -2060,9 +2072,10 @@ object ChatController {
} }
} }
suspend fun apiCreateGroupLink(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair<String, GroupMemberRole>? { suspend fun apiCreateGroupLink(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair<CreatedConnLink, GroupMemberRole>? {
return when (val r = sendCmd(rh, CC.APICreateGroupLink(groupId, memberRole))) { val short = appPrefs.privacyShortLinks.get()
is CR.GroupLinkCreated -> r.connReqContact to r.memberRole return when (val r = sendCmd(rh, CC.APICreateGroupLink(groupId, memberRole, short))) {
is CR.GroupLinkCreated -> r.connLinkContact to r.memberRole
else -> { else -> {
if (!(networkErrorAlert(r))) { if (!(networkErrorAlert(r))) {
apiErrorAlert("apiCreateGroupLink", generalGetString(MR.strings.error_creating_link_for_group), r) apiErrorAlert("apiCreateGroupLink", generalGetString(MR.strings.error_creating_link_for_group), r)
@ -2072,9 +2085,9 @@ object ChatController {
} }
} }
suspend fun apiGroupLinkMemberRole(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair<String, GroupMemberRole>? { suspend fun apiGroupLinkMemberRole(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair<CreatedConnLink, GroupMemberRole>? {
return when (val r = sendCmd(rh, CC.APIGroupLinkMemberRole(groupId, memberRole))) { return when (val r = sendCmd(rh, CC.APIGroupLinkMemberRole(groupId, memberRole))) {
is CR.GroupLink -> r.connReqContact to r.memberRole is CR.GroupLink -> r.connLinkContact to r.memberRole
else -> { else -> {
if (!(networkErrorAlert(r))) { if (!(networkErrorAlert(r))) {
apiErrorAlert("apiGroupLinkMemberRole", generalGetString(MR.strings.error_updating_link_for_group), r) apiErrorAlert("apiGroupLinkMemberRole", generalGetString(MR.strings.error_updating_link_for_group), r)
@ -2096,9 +2109,9 @@ object ChatController {
} }
} }
suspend fun apiGetGroupLink(rh: Long?, groupId: Long): Pair<String, GroupMemberRole>? { suspend fun apiGetGroupLink(rh: Long?, groupId: Long): Pair<CreatedConnLink, GroupMemberRole>? {
return when (val r = sendCmd(rh, CC.APIGetGroupLink(groupId))) { return when (val r = sendCmd(rh, CC.APIGetGroupLink(groupId))) {
is CR.GroupLink -> r.connReqContact to r.memberRole is CR.GroupLink -> r.connLinkContact to r.memberRole
else -> { else -> {
Log.e(TAG, "apiGetGroupLink bad response: ${r.responseType} ${r.details}") Log.e(TAG, "apiGetGroupLink bad response: ${r.responseType} ${r.details}")
null null
@ -3466,7 +3479,7 @@ sealed class CC {
class ApiLeaveGroup(val groupId: Long): CC() class ApiLeaveGroup(val groupId: Long): CC()
class ApiListMembers(val groupId: Long): CC() class ApiListMembers(val groupId: Long): CC()
class ApiUpdateGroupProfile(val groupId: Long, val groupProfile: GroupProfile): CC() class ApiUpdateGroupProfile(val groupId: Long, val groupProfile: GroupProfile): CC()
class APICreateGroupLink(val groupId: Long, val memberRole: GroupMemberRole): CC() class APICreateGroupLink(val groupId: Long, val memberRole: GroupMemberRole, val short: Boolean): CC()
class APIGroupLinkMemberRole(val groupId: Long, val memberRole: GroupMemberRole): CC() class APIGroupLinkMemberRole(val groupId: Long, val memberRole: GroupMemberRole): CC()
class APIDeleteGroupLink(val groupId: Long): CC() class APIDeleteGroupLink(val groupId: Long): CC()
class APIGetGroupLink(val groupId: Long): CC() class APIGetGroupLink(val groupId: Long): CC()
@ -3505,11 +3518,11 @@ sealed class CC {
class APIGetGroupMemberCode(val groupId: Long, val groupMemberId: Long): CC() class APIGetGroupMemberCode(val groupId: Long, val groupMemberId: Long): CC()
class APIVerifyContact(val contactId: Long, val connectionCode: String?): CC() class APIVerifyContact(val contactId: Long, val connectionCode: String?): CC()
class APIVerifyGroupMember(val groupId: Long, val groupMemberId: Long, val connectionCode: String?): CC() class APIVerifyGroupMember(val groupId: Long, val groupMemberId: Long, val connectionCode: String?): CC()
class APIAddContact(val userId: Long, val incognito: Boolean): CC() class APIAddContact(val userId: Long, val short: Boolean, val incognito: Boolean): CC()
class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC() class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC()
class ApiChangeConnectionUser(val connId: Long, val userId: Long): CC() class ApiChangeConnectionUser(val connId: Long, val userId: Long): CC()
class APIConnectPlan(val userId: Long, val connReq: String): CC() class APIConnectPlan(val userId: Long, val connLink: String): CC()
class APIConnect(val userId: Long, val incognito: Boolean, val connReq: String): CC() class APIConnect(val userId: Long, val incognito: Boolean, val connLink: CreatedConnLink): CC()
class ApiConnectContactViaAddress(val userId: Long, val incognito: Boolean, val contactId: Long): CC() class ApiConnectContactViaAddress(val userId: Long, val incognito: Boolean, val contactId: Long): CC()
class ApiDeleteChat(val type: ChatType, val id: Long, val chatDeleteMode: ChatDeleteMode): CC() class ApiDeleteChat(val type: ChatType, val id: Long, val chatDeleteMode: ChatDeleteMode): CC()
class ApiClearChat(val type: ChatType, val id: Long): CC() class ApiClearChat(val type: ChatType, val id: Long): CC()
@ -3521,7 +3534,7 @@ sealed class CC {
class ApiSetConnectionAlias(val connId: Long, val localAlias: String): CC() class ApiSetConnectionAlias(val connId: Long, val localAlias: String): CC()
class ApiSetUserUIThemes(val userId: Long, val themes: ThemeModeOverrides?): CC() class ApiSetUserUIThemes(val userId: Long, val themes: ThemeModeOverrides?): CC()
class ApiSetChatUIThemes(val chatId: String, val themes: ThemeModeOverrides?): CC() class ApiSetChatUIThemes(val chatId: String, val themes: ThemeModeOverrides?): CC()
class ApiCreateMyAddress(val userId: Long): CC() class ApiCreateMyAddress(val userId: Long, val short: Boolean): CC()
class ApiDeleteMyAddress(val userId: Long): CC() class ApiDeleteMyAddress(val userId: Long): CC()
class ApiShowMyAddress(val userId: Long): CC() class ApiShowMyAddress(val userId: Long): CC()
class ApiSetProfileAddress(val userId: Long, val on: Boolean): CC() class ApiSetProfileAddress(val userId: Long, val on: Boolean): CC()
@ -3651,7 +3664,7 @@ sealed class CC {
is ApiLeaveGroup -> "/_leave #$groupId" is ApiLeaveGroup -> "/_leave #$groupId"
is ApiListMembers -> "/_members #$groupId" is ApiListMembers -> "/_members #$groupId"
is ApiUpdateGroupProfile -> "/_group_profile #$groupId ${json.encodeToString(groupProfile)}" is ApiUpdateGroupProfile -> "/_group_profile #$groupId ${json.encodeToString(groupProfile)}"
is APICreateGroupLink -> "/_create link #$groupId ${memberRole.name.lowercase()}" is APICreateGroupLink -> "/_create link #$groupId ${memberRole.name.lowercase()} short=${onOff(short)}"
is APIGroupLinkMemberRole -> "/_set link role #$groupId ${memberRole.name.lowercase()}" is APIGroupLinkMemberRole -> "/_set link role #$groupId ${memberRole.name.lowercase()}"
is APIDeleteGroupLink -> "/_delete link #$groupId" is APIDeleteGroupLink -> "/_delete link #$groupId"
is APIGetGroupLink -> "/_get link #$groupId" is APIGetGroupLink -> "/_get link #$groupId"
@ -3690,11 +3703,11 @@ sealed class CC {
is APIGetGroupMemberCode -> "/_get code #$groupId $groupMemberId" is APIGetGroupMemberCode -> "/_get code #$groupId $groupMemberId"
is APIVerifyContact -> "/_verify code @$contactId" + if (connectionCode != null) " $connectionCode" else "" is APIVerifyContact -> "/_verify code @$contactId" + if (connectionCode != null) " $connectionCode" else ""
is APIVerifyGroupMember -> "/_verify code #$groupId $groupMemberId" + if (connectionCode != null) " $connectionCode" else "" is APIVerifyGroupMember -> "/_verify code #$groupId $groupMemberId" + if (connectionCode != null) " $connectionCode" else ""
is APIAddContact -> "/_connect $userId incognito=${onOff(incognito)}" is APIAddContact -> "/_connect $userId short=${onOff(short)} incognito=${onOff(incognito)}"
is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}" is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}"
is ApiChangeConnectionUser -> "/_set conn user :$connId $userId" is ApiChangeConnectionUser -> "/_set conn user :$connId $userId"
is APIConnectPlan -> "/_connect plan $userId $connReq" is APIConnectPlan -> "/_connect plan $userId $connLink"
is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq" is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} ${connLink.connFullLink} ${connLink.connShortLink ?: ""}"
is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId" is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId"
is ApiDeleteChat -> "/_delete ${chatRef(type, id)} ${chatDeleteMode.cmdString}" is ApiDeleteChat -> "/_delete ${chatRef(type, id)} ${chatDeleteMode.cmdString}"
is ApiClearChat -> "/_clear chat ${chatRef(type, id)}" is ApiClearChat -> "/_clear chat ${chatRef(type, id)}"
@ -3706,7 +3719,7 @@ sealed class CC {
is ApiSetConnectionAlias -> "/_set alias :$connId ${localAlias.trim()}" is ApiSetConnectionAlias -> "/_set alias :$connId ${localAlias.trim()}"
is ApiSetUserUIThemes -> "/_set theme user $userId ${if (themes != null) json.encodeToString(themes) else ""}" is ApiSetUserUIThemes -> "/_set theme user $userId ${if (themes != null) json.encodeToString(themes) else ""}"
is ApiSetChatUIThemes -> "/_set theme $chatId ${if (themes != null) json.encodeToString(themes) else ""}" is ApiSetChatUIThemes -> "/_set theme $chatId ${if (themes != null) json.encodeToString(themes) else ""}"
is ApiCreateMyAddress -> "/_address $userId" is ApiCreateMyAddress -> "/_address $userId short=${onOff(short)}"
is ApiDeleteMyAddress -> "/_delete_address $userId" is ApiDeleteMyAddress -> "/_delete_address $userId"
is ApiShowMyAddress -> "/_show_address $userId" is ApiShowMyAddress -> "/_show_address $userId"
is ApiSetProfileAddress -> "/_profile_address $userId ${onOff(on)}" is ApiSetProfileAddress -> "/_profile_address $userId ${onOff(on)}"
@ -5799,10 +5812,10 @@ sealed class CR {
@Serializable @SerialName("groupMemberCode") class GroupMemberCode(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionCode: String): CR() @Serializable @SerialName("groupMemberCode") class GroupMemberCode(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionCode: String): CR()
@Serializable @SerialName("connectionVerified") class ConnectionVerified(val user: UserRef, val verified: Boolean, val expectedCode: String): CR() @Serializable @SerialName("connectionVerified") class ConnectionVerified(val user: UserRef, val verified: Boolean, val expectedCode: String): CR()
@Serializable @SerialName("tagsUpdated") class TagsUpdated(val user: UserRef, val userTags: List<ChatTag>, val chatTags: List<Long>): CR() @Serializable @SerialName("tagsUpdated") class TagsUpdated(val user: UserRef, val userTags: List<ChatTag>, val chatTags: List<Long>): CR()
@Serializable @SerialName("invitation") class Invitation(val user: UserRef, val connReqInvitation: String, val connection: PendingContactConnection): CR() @Serializable @SerialName("invitation") class Invitation(val user: UserRef, val connLinkInvitation: CreatedConnLink, val connection: PendingContactConnection): CR()
@Serializable @SerialName("connectionIncognitoUpdated") class ConnectionIncognitoUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR() @Serializable @SerialName("connectionIncognitoUpdated") class ConnectionIncognitoUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR()
@Serializable @SerialName("connectionUserChanged") class ConnectionUserChanged(val user: UserRef, val fromConnection: PendingContactConnection, val toConnection: PendingContactConnection, val newUser: UserRef): CR() @Serializable @SerialName("connectionUserChanged") class ConnectionUserChanged(val user: UserRef, val fromConnection: PendingContactConnection, val toConnection: PendingContactConnection, val newUser: UserRef): CR()
@Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connectionPlan: ConnectionPlan): CR() @Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connLink: CreatedConnLink, val connectionPlan: ConnectionPlan): CR()
@Serializable @SerialName("sentConfirmation") class SentConfirmation(val user: UserRef, val connection: PendingContactConnection): CR() @Serializable @SerialName("sentConfirmation") class SentConfirmation(val user: UserRef, val connection: PendingContactConnection): CR()
@Serializable @SerialName("sentInvitation") class SentInvitation(val user: UserRef, val connection: PendingContactConnection): CR() @Serializable @SerialName("sentInvitation") class SentInvitation(val user: UserRef, val connection: PendingContactConnection): CR()
@Serializable @SerialName("sentInvitationToContact") class SentInvitationToContact(val user: UserRef, val contact: Contact, val customUserProfile: Profile?): CR() @Serializable @SerialName("sentInvitationToContact") class SentInvitationToContact(val user: UserRef, val contact: Contact, val customUserProfile: Profile?): CR()
@ -5819,7 +5832,7 @@ sealed class CR {
@Serializable @SerialName("contactPrefsUpdated") class ContactPrefsUpdated(val user: UserRef, val fromContact: Contact, val toContact: Contact): CR() @Serializable @SerialName("contactPrefsUpdated") class ContactPrefsUpdated(val user: UserRef, val fromContact: Contact, val toContact: Contact): CR()
@Serializable @SerialName("userContactLink") class UserContactLink(val user: User, val contactLink: UserContactLinkRec): CR() @Serializable @SerialName("userContactLink") class UserContactLink(val user: User, val contactLink: UserContactLinkRec): CR()
@Serializable @SerialName("userContactLinkUpdated") class UserContactLinkUpdated(val user: User, val contactLink: UserContactLinkRec): CR() @Serializable @SerialName("userContactLinkUpdated") class UserContactLinkUpdated(val user: User, val contactLink: UserContactLinkRec): CR()
@Serializable @SerialName("userContactLinkCreated") class UserContactLinkCreated(val user: User, val connReqContact: String): CR() @Serializable @SerialName("userContactLinkCreated") class UserContactLinkCreated(val user: User, val connLinkContact: CreatedConnLink): CR()
@Serializable @SerialName("userContactLinkDeleted") class UserContactLinkDeleted(val user: User): CR() @Serializable @SerialName("userContactLinkDeleted") class UserContactLinkDeleted(val user: User): CR()
@Serializable @SerialName("contactConnected") class ContactConnected(val user: UserRef, val contact: Contact, val userCustomProfile: Profile? = null): CR() @Serializable @SerialName("contactConnected") class ContactConnected(val user: UserRef, val contact: Contact, val userCustomProfile: Profile? = null): CR()
@Serializable @SerialName("contactConnecting") class ContactConnecting(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("contactConnecting") class ContactConnecting(val user: UserRef, val contact: Contact): CR()
@ -5876,8 +5889,8 @@ sealed class CR {
@Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val memberContact: Contact? = null): CR() @Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val memberContact: Contact? = null): CR()
@Serializable @SerialName("groupRemoved") class GroupRemoved(val user: UserRef, val groupInfo: GroupInfo): CR() // unused @Serializable @SerialName("groupRemoved") class GroupRemoved(val user: UserRef, val groupInfo: GroupInfo): CR() // unused
@Serializable @SerialName("groupUpdated") class GroupUpdated(val user: UserRef, val toGroup: GroupInfo): CR() @Serializable @SerialName("groupUpdated") class GroupUpdated(val user: UserRef, val toGroup: GroupInfo): CR()
@Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: UserRef, val groupInfo: GroupInfo, val connReqContact: String, val memberRole: GroupMemberRole): CR() @Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: UserRef, val groupInfo: GroupInfo, val connLinkContact: CreatedConnLink, val memberRole: GroupMemberRole): CR()
@Serializable @SerialName("groupLink") class GroupLink(val user: UserRef, val groupInfo: GroupInfo, val connReqContact: String, val memberRole: GroupMemberRole): CR() @Serializable @SerialName("groupLink") class GroupLink(val user: UserRef, val groupInfo: GroupInfo, val connLinkContact: CreatedConnLink, val memberRole: GroupMemberRole): CR()
@Serializable @SerialName("groupLinkDeleted") class GroupLinkDeleted(val user: UserRef, val groupInfo: GroupInfo): CR() @Serializable @SerialName("groupLinkDeleted") class GroupLinkDeleted(val user: UserRef, val groupInfo: GroupInfo): CR()
@Serializable @SerialName("newMemberContact") class NewMemberContact(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("newMemberContact") class NewMemberContact(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR()
@Serializable @SerialName("newMemberContactSentInv") class NewMemberContactSentInv(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("newMemberContactSentInv") class NewMemberContactSentInv(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR()
@ -6165,10 +6178,10 @@ sealed class CR {
is GroupMemberCode -> withUser(user, "groupInfo: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionCode: $connectionCode") is GroupMemberCode -> withUser(user, "groupInfo: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionCode: $connectionCode")
is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode") is ConnectionVerified -> withUser(user, "verified: $verified\nconnectionCode: $expectedCode")
is TagsUpdated -> withUser(user, "userTags: ${json.encodeToString(userTags)}\nchatTags: ${json.encodeToString(chatTags)}") is TagsUpdated -> withUser(user, "userTags: ${json.encodeToString(userTags)}\nchatTags: ${json.encodeToString(chatTags)}")
is Invitation -> withUser(user, "connReqInvitation: $connReqInvitation\nconnection: $connection") is Invitation -> withUser(user, "connLinkInvitation: ${json.encodeToString(connLinkInvitation)}\nconnection: $connection")
is ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection)) is ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection))
is ConnectionUserChanged -> withUser(user, "fromConnection: ${json.encodeToString(fromConnection)}\ntoConnection: ${json.encodeToString(toConnection)}\nnewUser: ${json.encodeToString(newUser)}" ) is ConnectionUserChanged -> withUser(user, "fromConnection: ${json.encodeToString(fromConnection)}\ntoConnection: ${json.encodeToString(toConnection)}\nnewUser: ${json.encodeToString(newUser)}" )
is CRConnectionPlan -> withUser(user, json.encodeToString(connectionPlan)) is CRConnectionPlan -> withUser(user, "connLink: ${json.encodeToString(connLink)}\nconnectionPlan: ${json.encodeToString(connectionPlan)}")
is SentConfirmation -> withUser(user, json.encodeToString(connection)) is SentConfirmation -> withUser(user, json.encodeToString(connection))
is SentInvitation -> withUser(user, json.encodeToString(connection)) is SentInvitation -> withUser(user, json.encodeToString(connection))
is SentInvitationToContact -> withUser(user, json.encodeToString(contact)) is SentInvitationToContact -> withUser(user, json.encodeToString(contact))
@ -6185,7 +6198,7 @@ sealed class CR {
is ContactPrefsUpdated -> withUser(user, "fromContact: $fromContact\ntoContact: \n${json.encodeToString(toContact)}") is ContactPrefsUpdated -> withUser(user, "fromContact: $fromContact\ntoContact: \n${json.encodeToString(toContact)}")
is UserContactLink -> withUser(user, contactLink.responseDetails) is UserContactLink -> withUser(user, contactLink.responseDetails)
is UserContactLinkUpdated -> withUser(user, contactLink.responseDetails) is UserContactLinkUpdated -> withUser(user, contactLink.responseDetails)
is UserContactLinkCreated -> withUser(user, connReqContact) is UserContactLinkCreated -> withUser(user, json.encodeToString(connLinkContact))
is UserContactLinkDeleted -> withUser(user, noDetails()) is UserContactLinkDeleted -> withUser(user, noDetails())
is ContactConnected -> withUser(user, json.encodeToString(contact)) is ContactConnected -> withUser(user, json.encodeToString(contact))
is ContactConnecting -> withUser(user, json.encodeToString(contact)) is ContactConnecting -> withUser(user, json.encodeToString(contact))
@ -6239,8 +6252,8 @@ sealed class CR {
is ConnectedToGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member\nmemberContact: $memberContact") is ConnectedToGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member\nmemberContact: $memberContact")
is GroupRemoved -> withUser(user, json.encodeToString(groupInfo)) is GroupRemoved -> withUser(user, json.encodeToString(groupInfo))
is GroupUpdated -> withUser(user, json.encodeToString(toGroup)) is GroupUpdated -> withUser(user, json.encodeToString(toGroup))
is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole") is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnLinkContact: $connLinkContact\nmemberRole: $memberRole")
is GroupLink -> withUser(user, "groupInfo: $groupInfo\nconnReqContact: $connReqContact\nmemberRole: $memberRole") is GroupLink -> withUser(user, "groupInfo: $groupInfo\nconnLinkContact: $connLinkContact\nmemberRole: $memberRole")
is GroupLinkDeleted -> withUser(user, json.encodeToString(groupInfo)) is GroupLinkDeleted -> withUser(user, json.encodeToString(groupInfo))
is NewMemberContact -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member") is NewMemberContact -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member")
is NewMemberContactSentInv -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member") is NewMemberContactSentInv -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member")
@ -6350,11 +6363,34 @@ sealed class ChatDeleteMode {
} }
} }
@Serializable
data class CreatedConnLink(val connFullLink: String, val connShortLink: String?) {
fun simplexChatUri(short: Boolean): String =
if (short) connShortLink ?: simplexChatLink(connFullLink)
else simplexChatLink(connFullLink)
companion object {
val nullableStateSaver: Saver<CreatedConnLink?, Pair<String?, String?>> = Saver(
save = { link -> link?.connFullLink to link?.connShortLink },
restore = { saved ->
val connFullLink = saved.first
if (connFullLink == null) null
else CreatedConnLink(connFullLink = connFullLink, connShortLink = saved.second)
}
)
}
}
fun simplexChatLink(uri: String): String =
if (uri.startsWith("simplex:/")) uri.replace("simplex:/", "https://simplex.chat/")
else uri
@Serializable @Serializable
sealed class ConnectionPlan { sealed class ConnectionPlan {
@Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan() @Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan()
@Serializable @SerialName("contactAddress") class ContactAddress(val contactAddressPlan: ContactAddressPlan): ConnectionPlan() @Serializable @SerialName("contactAddress") class ContactAddress(val contactAddressPlan: ContactAddressPlan): ConnectionPlan()
@Serializable @SerialName("groupLink") class GroupLink(val groupLinkPlan: GroupLinkPlan): ConnectionPlan() @Serializable @SerialName("groupLink") class GroupLink(val groupLinkPlan: GroupLinkPlan): ConnectionPlan()
@Serializable @SerialName("error") class Error(val chatError: ChatError): ConnectionPlan()
} }
@Serializable @Serializable
@ -6487,8 +6523,8 @@ enum class RatchetSyncState {
} }
@Serializable @Serializable
class UserContactLinkRec(val connReqContact: String, val autoAccept: AutoAccept? = null) { class UserContactLinkRec(val connLinkContact: CreatedConnLink, val autoAccept: AutoAccept? = null) {
val responseDetails: String get() = "connReqContact: ${connReqContact}\nautoAccept: ${AutoAccept.cmdString(autoAccept)}" val responseDetails: String get() = "connLinkContact: ${connLinkContact}\nautoAccept: ${AutoAccept.cmdString(autoAccept)}"
} }
@Serializable @Serializable
@ -6580,6 +6616,7 @@ sealed class ChatErrorType {
is ChatStoreChanged -> "chatStoreChanged" is ChatStoreChanged -> "chatStoreChanged"
is ConnectionPlanChatError -> "connectionPlan" is ConnectionPlanChatError -> "connectionPlan"
is InvalidConnReq -> "invalidConnReq" is InvalidConnReq -> "invalidConnReq"
is UnsupportedConnReq -> "unsupportedConnReq"
is InvalidChatMessage -> "invalidChatMessage" is InvalidChatMessage -> "invalidChatMessage"
is ContactNotReady -> "contactNotReady" is ContactNotReady -> "contactNotReady"
is ContactNotActive -> "contactNotActive" is ContactNotActive -> "contactNotActive"
@ -6658,6 +6695,7 @@ sealed class ChatErrorType {
@Serializable @SerialName("chatStoreChanged") object ChatStoreChanged: ChatErrorType() @Serializable @SerialName("chatStoreChanged") object ChatStoreChanged: ChatErrorType()
@Serializable @SerialName("connectionPlan") class ConnectionPlanChatError(val connectionPlan: ConnectionPlan): ChatErrorType() @Serializable @SerialName("connectionPlan") class ConnectionPlanChatError(val connectionPlan: ConnectionPlan): ChatErrorType()
@Serializable @SerialName("invalidConnReq") object InvalidConnReq: ChatErrorType() @Serializable @SerialName("invalidConnReq") object InvalidConnReq: ChatErrorType()
@Serializable @SerialName("unsupportedConnReq") object UnsupportedConnReq: ChatErrorType()
@Serializable @SerialName("invalidChatMessage") class InvalidChatMessage(val connection: Connection, val message: String): ChatErrorType() @Serializable @SerialName("invalidChatMessage") class InvalidChatMessage(val connection: Connection, val message: String): ChatErrorType()
@Serializable @SerialName("contactNotReady") class ContactNotReady(val contact: Contact): ChatErrorType() @Serializable @SerialName("contactNotReady") class ContactNotReady(val contact: Contact): ChatErrorType()
@Serializable @SerialName("contactNotActive") class ContactNotActive(val contact: Contact): ChatErrorType() @Serializable @SerialName("contactNotActive") class ContactNotActive(val contact: Contact): ChatErrorType()
@ -6777,6 +6815,7 @@ sealed class StoreError {
is ContactNotFoundByFileId -> "contactNotFoundByFileId" is ContactNotFoundByFileId -> "contactNotFoundByFileId"
is NoGroupSndStatus -> "noGroupSndStatus" is NoGroupSndStatus -> "noGroupSndStatus"
is LargeMsg -> "largeMsg" is LargeMsg -> "largeMsg"
is DBException -> "dBException"
} }
@Serializable @SerialName("duplicateName") object DuplicateName: StoreError() @Serializable @SerialName("duplicateName") object DuplicateName: StoreError()
@ -6837,6 +6876,7 @@ sealed class StoreError {
@Serializable @SerialName("contactNotFoundByFileId") class ContactNotFoundByFileId(val fileId: Long): StoreError() @Serializable @SerialName("contactNotFoundByFileId") class ContactNotFoundByFileId(val fileId: Long): StoreError()
@Serializable @SerialName("noGroupSndStatus") class NoGroupSndStatus(val itemId: Long, val groupMemberId: Long): StoreError() @Serializable @SerialName("noGroupSndStatus") class NoGroupSndStatus(val itemId: Long, val groupMemberId: Long): StoreError()
@Serializable @SerialName("largeMsg") object LargeMsg: StoreError() @Serializable @SerialName("largeMsg") object LargeMsg: StoreError()
@Serializable @SerialName("dBException") class DBException(val message: String): StoreError()
} }
@Serializable @Serializable

View file

@ -263,7 +263,7 @@ fun ChatView(
// The idea is to preload information before showing a modal because large groups can take time to load all members // The idea is to preload information before showing a modal because large groups can take time to load all members
var preloadedContactInfo: Pair<ConnectionStats?, Profile?>? = null var preloadedContactInfo: Pair<ConnectionStats?, Profile?>? = null
var preloadedCode: String? = null var preloadedCode: String? = null
var preloadedLink: Pair<String, GroupMemberRole>? = null var preloadedLink: Pair<CreatedConnLink, GroupMemberRole>? = null
if (chatInfo is ChatInfo.Direct) { if (chatInfo is ChatInfo.Direct) {
preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId) preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId)
preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second
@ -291,7 +291,7 @@ fun ChatView(
showSearch.value = true showSearch.value = true
} }
} else if (chatInfo is ChatInfo.Group) { } else if (chatInfo is ChatInfo.Group) {
var link: Pair<String, GroupMemberRole>? by remember(chatInfo.id) { mutableStateOf(preloadedLink) } var link: Pair<CreatedConnLink, GroupMemberRole>? by remember(chatInfo.id) { mutableStateOf(preloadedLink) }
KeyChangeEffect(chatInfo.id) { KeyChangeEffect(chatInfo.id) {
setGroupMembers(chatRh, chatInfo.groupInfo, chatModel) setGroupMembers(chatRh, chatInfo.groupInfo, chatModel)
link = chatModel.controller.apiGetGroupLink(chatRh, chatInfo.groupInfo.groupId) link = chatModel.controller.apiGetGroupLink(chatRh, chatInfo.groupInfo.groupId)
@ -632,7 +632,7 @@ fun ChatView(
is ChatInfo.ContactConnection -> { is ChatInfo.ContactConnection -> {
val close = { chatModel.chatId.value = null } val close = { chatModel.chatId.value = null }
ModalView(close, showClose = appPlatform.isAndroid, content = { ModalView(close, showClose = appPlatform.isAndroid, content = {
ContactConnectionInfoView(chatModel, chatRh, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, false, close) ContactConnectionInfoView(chatModel, chatRh, chatInfo.contactConnection.connLinkInv, chatInfo.contactConnection, false, close)
}) })
LaunchedEffect(chatInfo.id) { LaunchedEffect(chatInfo.id) {
onComposed(chatInfo.id) onComposed(chatInfo.id)

View file

@ -53,12 +53,12 @@ val MEMBER_ROW_VERTICAL_PADDING = 8.dp
fun ModalData.GroupChatInfoView( fun ModalData.GroupChatInfoView(
rhId: Long?, rhId: Long?,
chatId: String, chatId: String,
groupLink: String?, groupLink: CreatedConnLink?,
groupLinkMemberRole: GroupMemberRole?, groupLinkMemberRole: GroupMemberRole?,
selectedItems: MutableState<Set<Long>?>, selectedItems: MutableState<Set<Long>?>,
appBar: MutableState<@Composable (BoxScope.() -> Unit)?>, appBar: MutableState<@Composable (BoxScope.() -> Unit)?>,
scrollToItemId: MutableState<Long?>, scrollToItemId: MutableState<Long?>,
onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, onGroupLinkUpdated: (Pair<CreatedConnLink, GroupMemberRole>?) -> Unit,
close: () -> Unit, close: () -> Unit,
onSearchClicked: () -> Unit onSearchClicked: () -> Unit
) { ) {
@ -328,7 +328,7 @@ fun ModalData.GroupChatInfoLayout(
activeSortedMembers: List<GroupMember>, activeSortedMembers: List<GroupMember>,
developerTools: Boolean, developerTools: Boolean,
onLocalAliasChanged: (String) -> Unit, onLocalAliasChanged: (String) -> Unit,
groupLink: String?, groupLink: CreatedConnLink?,
selectedItems: MutableState<Set<Long>?>, selectedItems: MutableState<Set<Long>?>,
appBar: MutableState<@Composable (BoxScope.() -> Unit)?>, appBar: MutableState<@Composable (BoxScope.() -> Unit)?>,
scrollToItemId: MutableState<Long?>, scrollToItemId: MutableState<Long?>,

View file

@ -1,6 +1,7 @@
package chat.simplex.common.views.chat.group package chat.simplex.common.views.chat.group
import SectionBottomSpacer import SectionBottomSpacer
import SectionViewWithButton
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
@ -15,11 +16,11 @@ import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import chat.simplex.common.model.* import chat.simplex.common.model.*
import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.platform.*
import chat.simplex.common.platform.shareText
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.newchat.* import chat.simplex.common.views.newchat.*
import chat.simplex.common.views.usersettings.SettingsActionItem
import chat.simplex.res.MR import chat.simplex.res.MR
@Composable @Composable
@ -27,13 +28,13 @@ fun GroupLinkView(
chatModel: ChatModel, chatModel: ChatModel,
rhId: Long?, rhId: Long?,
groupInfo: GroupInfo, groupInfo: GroupInfo,
connReqContact: String?, connLinkContact: CreatedConnLink?,
memberRole: GroupMemberRole?, memberRole: GroupMemberRole?,
onGroupLinkUpdated: ((Pair<String, GroupMemberRole>?) -> Unit)?, onGroupLinkUpdated: ((Pair<CreatedConnLink, GroupMemberRole>?) -> Unit)?,
creatingGroup: Boolean = false, creatingGroup: Boolean = false,
close: (() -> Unit)? = null close: (() -> Unit)? = null
) { ) {
var groupLink by rememberSaveable { mutableStateOf(connReqContact) } var groupLink by rememberSaveable(stateSaver = CreatedConnLink.nullableStateSaver) { mutableStateOf(connLinkContact) }
val groupLinkMemberRole = rememberSaveable { mutableStateOf(memberRole) } val groupLinkMemberRole = rememberSaveable { mutableStateOf(memberRole) }
var creatingLink by rememberSaveable { mutableStateOf(false) } var creatingLink by rememberSaveable { mutableStateOf(false) }
fun createLink() { fun createLink() {
@ -99,7 +100,7 @@ fun GroupLinkView(
@Composable @Composable
fun GroupLinkLayout( fun GroupLinkLayout(
groupLink: String?, groupLink: CreatedConnLink?,
groupInfo: GroupInfo, groupInfo: GroupInfo,
groupLinkMemberRole: MutableState<GroupMemberRole?>, groupLinkMemberRole: MutableState<GroupMemberRole?>,
creatingLink: Boolean, creatingLink: Boolean,
@ -150,7 +151,15 @@ fun GroupLinkLayout(
} }
initialLaunch = false initialLaunch = false
} }
SimpleXLinkQRCode(groupLink) val showShortLink = remember { mutableStateOf(true) }
Spacer(Modifier.height(DEFAULT_PADDING_HALF))
if (groupLink.connShortLink == null) {
SimpleXCreatedLinkQRCode(groupLink, short = false)
} else {
SectionViewWithButton(titleButton = { ToggleShortLinkButton(showShortLink) }) {
SimpleXCreatedLinkQRCode(groupLink, short = showShortLink.value)
}
}
Row( Row(
horizontalArrangement = Arrangement.spacedBy(10.dp), horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -160,7 +169,7 @@ fun GroupLinkLayout(
SimpleButton( SimpleButton(
stringResource(MR.strings.share_link), stringResource(MR.strings.share_link),
icon = painterResource(MR.images.ic_share), icon = painterResource(MR.images.ic_share),
click = { clipboard.shareText(simplexChatLink(groupLink)) } click = { clipboard.shareText(groupLink.simplexChatUri(short = showShortLink.value)) }
) )
if (creatingGroup && close != null) { if (creatingGroup && close != null) {
ContinueButton(close) ContinueButton(close)

View file

@ -547,7 +547,7 @@ fun ContactConnectionMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactConnection
ModalManager.center.closeModals() ModalManager.center.closeModals()
ModalManager.end.closeModals() ModalManager.end.closeModals()
ModalManager.center.showModalCloseable(true, showClose = appPlatform.isAndroid) { close -> ModalManager.center.showModalCloseable(true, showClose = appPlatform.isAndroid) { close ->
ContactConnectionInfoView(chatModel, rhId, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, true, close) ContactConnectionInfoView(chatModel, rhId, chatInfo.contactConnection.connLinkInv, chatInfo.contactConnection, true, close)
} }
showMenu.value = false showMenu.value = false
}, },

View file

@ -53,6 +53,24 @@ fun SectionView(
} }
} }
@Composable
fun SectionViewWithButton(title: String? = null, titleButton: (@Composable () -> Unit)?, contentPadding: PaddingValues = PaddingValues(), headerBottomPadding: Dp = DEFAULT_PADDING, content: (@Composable ColumnScope.() -> Unit)) {
Column {
if (title != null || titleButton != null) {
Row(modifier = Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = headerBottomPadding).fillMaxWidth()) {
if (title != null) {
Text(title, color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, fontSize = 12.sp)
}
if (titleButton != null) {
Spacer(modifier = Modifier.weight(1f))
titleButton()
}
}
}
Column(Modifier.padding(contentPadding).fillMaxWidth()) { content() }
}
}
@Composable @Composable
fun <T> SectionViewSelectable( fun <T> SectionViewSelectable(
title: String?, title: String?,

View file

@ -57,7 +57,7 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit, c
} }
} else { } else {
ModalManager.end.showModalCloseable(true) { close -> ModalManager.end.showModalCloseable(true) { close ->
GroupLinkView(chatModel, rhId, groupInfo, connReqContact = null, memberRole = null, onGroupLinkUpdated = null, creatingGroup = true, close) GroupLinkView(chatModel, rhId, groupInfo, connLinkContact = null, memberRole = null, onGroupLinkUpdated = null, creatingGroup = true, close)
} }
} }
} }

View file

@ -19,7 +19,7 @@ enum class ConnectionLinkType {
suspend fun planAndConnect( suspend fun planAndConnect(
rhId: Long?, rhId: Long?,
uri: String, shortOrFullLink: String,
incognito: Boolean?, incognito: Boolean?,
close: (() -> Unit)?, close: (() -> Unit)?,
cleanup: (() -> Unit)? = null, cleanup: (() -> Unit)? = null,
@ -27,18 +27,19 @@ suspend fun planAndConnect(
filterKnownGroup: ((GroupInfo) -> Unit)? = null, filterKnownGroup: ((GroupInfo) -> Unit)? = null,
): CompletableDeferred<Boolean> { ): CompletableDeferred<Boolean> {
val completable = CompletableDeferred<Boolean>() val completable = CompletableDeferred<Boolean>()
val close: (() -> Unit)? = { val close: (() -> Unit) = {
close?.invoke() close?.invoke()
// if close was called, it means the connection was created // if close was called, it means the connection was created
completable.complete(true) completable.complete(true)
} }
val cleanup: (() -> Unit)? = { val cleanup: (() -> Unit) = {
cleanup?.invoke() cleanup?.invoke()
completable.complete(!completable.isActive) completable.complete(!completable.isActive)
} }
val connectionPlan = chatModel.controller.apiConnectPlan(rhId, uri) val result = chatModel.controller.apiConnectPlan(rhId, shortOrFullLink)
if (connectionPlan != null) { if (result != null) {
val link = strHasSingleSimplexLink(uri.trim()) val (connectionLink, connectionPlan) = result
val link = strHasSingleSimplexLink(shortOrFullLink.trim())
val linkText = if (link?.format is Format.SimplexLink) val linkText = if (link?.format is Format.SimplexLink)
"<br><br><u>${link.simplexLinkText(link.format.linkType, link.format.smpHosts)}</u>" "<br><br><u>${link.simplexLinkText(link.format.linkType, link.format.smpHosts)}</u>"
else else
@ -48,10 +49,10 @@ suspend fun planAndConnect(
InvitationLinkPlan.Ok -> { InvitationLinkPlan.Ok -> {
Log.d(TAG, "planAndConnect, .InvitationLink, .Ok, incognito=$incognito") Log.d(TAG, "planAndConnect, .InvitationLink, .Ok, incognito=$incognito")
if (incognito != null) { if (incognito != null) {
connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup)
} else { } else {
askCurrentOrIncognitoProfileAlert( askCurrentOrIncognitoProfileAlert(
chatModel, rhId, uri, connectionPlan, close, chatModel, rhId, connectionLink, connectionPlan, close,
title = generalGetString(MR.strings.connect_via_invitation_link), title = generalGetString(MR.strings.connect_via_invitation_link),
text = generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link) + linkText, text = generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link) + linkText,
connectDestructive = false, connectDestructive = false,
@ -66,7 +67,7 @@ suspend fun planAndConnect(
title = generalGetString(MR.strings.connect_plan_connect_to_yourself), title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link) + linkText, text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link) + linkText,
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
onConfirm = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } }, onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } },
onDismiss = cleanup, onDismiss = cleanup,
onDismissRequest = cleanup, onDismissRequest = cleanup,
destructive = true, destructive = true,
@ -74,7 +75,7 @@ suspend fun planAndConnect(
) )
} else { } else {
askCurrentOrIncognitoProfileAlert( askCurrentOrIncognitoProfileAlert(
chatModel, rhId, uri, connectionPlan, close, chatModel, rhId, connectionLink, connectionPlan, close,
title = generalGetString(MR.strings.connect_plan_connect_to_yourself), title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link) + linkText, text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link) + linkText,
connectDestructive = true, connectDestructive = true,
@ -95,7 +96,7 @@ suspend fun planAndConnect(
String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName) + linkText, String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName) + linkText,
hostDevice = hostDevice(rhId), hostDevice = hostDevice(rhId),
) )
cleanup?.invoke() cleanup()
} }
} else { } else {
AlertManager.privacySensitive.showAlertMsg( AlertManager.privacySensitive.showAlertMsg(
@ -103,7 +104,7 @@ suspend fun planAndConnect(
generalGetString(MR.strings.connect_plan_you_are_already_connecting_via_this_one_time_link) + linkText, generalGetString(MR.strings.connect_plan_you_are_already_connecting_via_this_one_time_link) + linkText,
hostDevice = hostDevice(rhId), hostDevice = hostDevice(rhId),
) )
cleanup?.invoke() cleanup()
} }
} }
is InvitationLinkPlan.Known -> { is InvitationLinkPlan.Known -> {
@ -118,7 +119,7 @@ suspend fun planAndConnect(
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName) + linkText, String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName) + linkText,
hostDevice = hostDevice(rhId), hostDevice = hostDevice(rhId),
) )
cleanup?.invoke() cleanup()
} }
} }
} }
@ -126,10 +127,10 @@ suspend fun planAndConnect(
ContactAddressPlan.Ok -> { ContactAddressPlan.Ok -> {
Log.d(TAG, "planAndConnect, .ContactAddress, .Ok, incognito=$incognito") Log.d(TAG, "planAndConnect, .ContactAddress, .Ok, incognito=$incognito")
if (incognito != null) { if (incognito != null) {
connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup)
} else { } else {
askCurrentOrIncognitoProfileAlert( askCurrentOrIncognitoProfileAlert(
chatModel, rhId, uri, connectionPlan, close, chatModel, rhId, connectionLink, connectionPlan, close,
title = generalGetString(MR.strings.connect_via_contact_link), title = generalGetString(MR.strings.connect_via_contact_link),
text = generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link) + linkText, text = generalGetString(MR.strings.profile_will_be_sent_to_contact_sending_link) + linkText,
connectDestructive = false, connectDestructive = false,
@ -144,7 +145,7 @@ suspend fun planAndConnect(
title = generalGetString(MR.strings.connect_plan_connect_to_yourself), title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address) + linkText, text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address) + linkText,
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
onConfirm = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } }, onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } },
destructive = true, destructive = true,
onDismiss = cleanup, onDismiss = cleanup,
onDismissRequest = cleanup, onDismissRequest = cleanup,
@ -152,7 +153,7 @@ suspend fun planAndConnect(
) )
} else { } else {
askCurrentOrIncognitoProfileAlert( askCurrentOrIncognitoProfileAlert(
chatModel, rhId, uri, connectionPlan, close, chatModel, rhId, connectionLink, connectionPlan, close,
title = generalGetString(MR.strings.connect_plan_connect_to_yourself), title = generalGetString(MR.strings.connect_plan_connect_to_yourself),
text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address) + linkText, text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address) + linkText,
connectDestructive = true, connectDestructive = true,
@ -167,7 +168,7 @@ suspend fun planAndConnect(
title = generalGetString(MR.strings.connect_plan_repeat_connection_request), title = generalGetString(MR.strings.connect_plan_repeat_connection_request),
text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address) + linkText, text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address) + linkText,
confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb),
onConfirm = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } }, onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } },
onDismiss = cleanup, onDismiss = cleanup,
onDismissRequest = cleanup, onDismissRequest = cleanup,
destructive = true, destructive = true,
@ -175,7 +176,7 @@ suspend fun planAndConnect(
) )
} else { } else {
askCurrentOrIncognitoProfileAlert( askCurrentOrIncognitoProfileAlert(
chatModel, rhId, uri, connectionPlan, close, chatModel, rhId, connectionLink, connectionPlan, close,
title = generalGetString(MR.strings.connect_plan_repeat_connection_request), title = generalGetString(MR.strings.connect_plan_repeat_connection_request),
text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address) + linkText, text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address) + linkText,
connectDestructive = true, connectDestructive = true,
@ -195,7 +196,7 @@ suspend fun planAndConnect(
String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName) + linkText, String.format(generalGetString(MR.strings.connect_plan_you_are_already_connecting_to_vName), contact.displayName) + linkText,
hostDevice = hostDevice(rhId), hostDevice = hostDevice(rhId),
) )
cleanup?.invoke() cleanup()
} }
} }
is ContactAddressPlan.Known -> { is ContactAddressPlan.Known -> {
@ -210,19 +211,19 @@ suspend fun planAndConnect(
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName) + linkText, String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName) + linkText,
hostDevice = hostDevice(rhId), hostDevice = hostDevice(rhId),
) )
cleanup?.invoke() cleanup()
} }
} }
is ContactAddressPlan.ContactViaAddress -> { is ContactAddressPlan.ContactViaAddress -> {
Log.d(TAG, "planAndConnect, .ContactAddress, .ContactViaAddress, incognito=$incognito") Log.d(TAG, "planAndConnect, .ContactAddress, .ContactViaAddress, incognito=$incognito")
val contact = connectionPlan.contactAddressPlan.contact val contact = connectionPlan.contactAddressPlan.contact
if (incognito != null) { if (incognito != null) {
close?.invoke() close()
connectContactViaAddress(chatModel, rhId, contact.contactId, incognito) connectContactViaAddress(chatModel, rhId, contact.contactId, incognito)
} else { } else {
askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close, openChat = false) askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close, openChat = false)
} }
cleanup?.invoke() cleanup()
} }
} }
is ConnectionPlan.GroupLink -> when (connectionPlan.groupLinkPlan) { is ConnectionPlan.GroupLink -> when (connectionPlan.groupLinkPlan) {
@ -233,14 +234,14 @@ suspend fun planAndConnect(
title = generalGetString(MR.strings.connect_via_group_link), title = generalGetString(MR.strings.connect_via_group_link),
text = generalGetString(MR.strings.you_will_join_group) + linkText, text = generalGetString(MR.strings.you_will_join_group) + linkText,
confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button),
onConfirm = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } }, onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } },
onDismiss = cleanup, onDismiss = cleanup,
onDismissRequest = cleanup, onDismissRequest = cleanup,
hostDevice = hostDevice(rhId), hostDevice = hostDevice(rhId),
) )
} else { } else {
askCurrentOrIncognitoProfileAlert( askCurrentOrIncognitoProfileAlert(
chatModel, rhId, uri, connectionPlan, close, chatModel, rhId, connectionLink, connectionPlan, close,
title = generalGetString(MR.strings.connect_via_group_link), title = generalGetString(MR.strings.connect_via_group_link),
text = generalGetString(MR.strings.you_will_join_group) + linkText, text = generalGetString(MR.strings.you_will_join_group) + linkText,
connectDestructive = false, connectDestructive = false,
@ -254,7 +255,7 @@ suspend fun planAndConnect(
if (filterKnownGroup != null) { if (filterKnownGroup != null) {
filterKnownGroup(groupInfo) filterKnownGroup(groupInfo)
} else { } else {
ownGroupLinkConfirmConnect(chatModel, rhId, uri, linkText, incognito, connectionPlan, groupInfo, close, cleanup) ownGroupLinkConfirmConnect(chatModel, rhId, connectionLink, linkText, incognito, connectionPlan, groupInfo, close, cleanup)
} }
} }
GroupLinkPlan.ConnectingConfirmReconnect -> { GroupLinkPlan.ConnectingConfirmReconnect -> {
@ -264,7 +265,7 @@ suspend fun planAndConnect(
title = generalGetString(MR.strings.connect_plan_repeat_join_request), title = generalGetString(MR.strings.connect_plan_repeat_join_request),
text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + linkText, text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + linkText,
confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button),
onConfirm = { withBGApi { connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) } }, onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } },
onDismiss = cleanup, onDismiss = cleanup,
onDismissRequest = cleanup, onDismissRequest = cleanup,
destructive = true, destructive = true,
@ -272,7 +273,7 @@ suspend fun planAndConnect(
) )
} else { } else {
askCurrentOrIncognitoProfileAlert( askCurrentOrIncognitoProfileAlert(
chatModel, rhId, uri, connectionPlan, close, chatModel, rhId, connectionLink, connectionPlan, close,
title = generalGetString(MR.strings.connect_plan_repeat_join_request), title = generalGetString(MR.strings.connect_plan_repeat_join_request),
text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + linkText, text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + linkText,
connectDestructive = true, connectDestructive = true,
@ -302,7 +303,7 @@ suspend fun planAndConnect(
hostDevice = hostDevice(rhId), hostDevice = hostDevice(rhId),
) )
} }
cleanup?.invoke() cleanup()
} }
is GroupLinkPlan.Known -> { is GroupLinkPlan.Known -> {
Log.d(TAG, "planAndConnect, .GroupLink, .Known, incognito=$incognito") Log.d(TAG, "planAndConnect, .GroupLink, .Known, incognito=$incognito")
@ -324,38 +325,39 @@ suspend fun planAndConnect(
hostDevice = hostDevice(rhId), hostDevice = hostDevice(rhId),
) )
} }
cleanup?.invoke() cleanup()
} }
} }
} }
} is ConnectionPlan.Error -> {
} else { Log.d(TAG, "planAndConnect, error ${connectionPlan.chatError}")
Log.d(TAG, "planAndConnect, plan error")
if (incognito != null) { if (incognito != null) {
connectViaUri(chatModel, rhId, uri, incognito, connectionPlan = null, close, cleanup) connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan = null, close, cleanup)
} else { } else {
askCurrentOrIncognitoProfileAlert( askCurrentOrIncognitoProfileAlert(
chatModel, rhId, uri, connectionPlan = null, close, chatModel, rhId, connectionLink, connectionPlan = null, close,
title = generalGetString(MR.strings.connect_plan_connect_via_link), title = generalGetString(MR.strings.connect_plan_connect_via_link),
connectDestructive = false, connectDestructive = false,
cleanup = cleanup, cleanup = cleanup,
) )
} }
} }
}
}
return completable return completable
} }
suspend fun connectViaUri( suspend fun connectViaUri(
chatModel: ChatModel, chatModel: ChatModel,
rhId: Long?, rhId: Long?,
uri: String, connLink: CreatedConnLink,
incognito: Boolean, incognito: Boolean,
connectionPlan: ConnectionPlan?, connectionPlan: ConnectionPlan?,
close: (() -> Unit)?, close: (() -> Unit)?,
cleanup: (() -> Unit)?, cleanup: (() -> Unit)?,
): Boolean { ): Boolean {
val pcc = chatModel.controller.apiConnect(rhId, incognito, uri) val pcc = chatModel.controller.apiConnect(rhId, incognito, connLink)
val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) ?: ConnectionLinkType.INVITATION else ConnectionLinkType.INVITATION
if (pcc != null) { if (pcc != null) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
chatModel.chatsContext.updateContactConnection(rhId, pcc) chatModel.chatsContext.updateContactConnection(rhId, pcc)
@ -376,18 +378,19 @@ suspend fun connectViaUri(
return pcc != null return pcc != null
} }
fun planToConnectionLinkType(connectionPlan: ConnectionPlan): ConnectionLinkType { fun planToConnectionLinkType(connectionPlan: ConnectionPlan): ConnectionLinkType? {
return when(connectionPlan) { return when(connectionPlan) {
is ConnectionPlan.InvitationLink -> ConnectionLinkType.INVITATION is ConnectionPlan.InvitationLink -> ConnectionLinkType.INVITATION
is ConnectionPlan.ContactAddress -> ConnectionLinkType.CONTACT is ConnectionPlan.ContactAddress -> ConnectionLinkType.CONTACT
is ConnectionPlan.GroupLink -> ConnectionLinkType.GROUP is ConnectionPlan.GroupLink -> ConnectionLinkType.GROUP
is ConnectionPlan.Error -> null
} }
} }
fun askCurrentOrIncognitoProfileAlert( fun askCurrentOrIncognitoProfileAlert(
chatModel: ChatModel, chatModel: ChatModel,
rhId: Long?, rhId: Long?,
uri: String, connectionLink: CreatedConnLink,
connectionPlan: ConnectionPlan?, connectionPlan: ConnectionPlan?,
close: (() -> Unit)?, close: (() -> Unit)?,
title: String, title: String,
@ -404,7 +407,7 @@ fun askCurrentOrIncognitoProfileAlert(
SectionItemView({ SectionItemView({
AlertManager.privacySensitive.hideAlert() AlertManager.privacySensitive.hideAlert()
withBGApi { withBGApi {
connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close, cleanup) connectViaUri(chatModel, rhId, connectionLink, incognito = false, connectionPlan, close, cleanup)
} }
}) { }) {
Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor) Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor)
@ -412,7 +415,7 @@ fun askCurrentOrIncognitoProfileAlert(
SectionItemView({ SectionItemView({
AlertManager.privacySensitive.hideAlert() AlertManager.privacySensitive.hideAlert()
withBGApi { withBGApi {
connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close, cleanup) connectViaUri(chatModel, rhId, connectionLink, incognito = true, connectionPlan, close, cleanup)
} }
}) { }) {
Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor) Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = connectColor)
@ -443,7 +446,7 @@ fun openKnownContact(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, co
fun ownGroupLinkConfirmConnect( fun ownGroupLinkConfirmConnect(
chatModel: ChatModel, chatModel: ChatModel,
rhId: Long?, rhId: Long?,
uri: String, connectionLink: CreatedConnLink,
linkText: String, linkText: String,
incognito: Boolean?, incognito: Boolean?,
connectionPlan: ConnectionPlan?, connectionPlan: ConnectionPlan?,
@ -469,7 +472,7 @@ fun ownGroupLinkConfirmConnect(
SectionItemView({ SectionItemView({
AlertManager.privacySensitive.hideAlert() AlertManager.privacySensitive.hideAlert()
withBGApi { withBGApi {
connectViaUri(chatModel, rhId, uri, incognito, connectionPlan, close, cleanup) connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup)
} }
}) { }) {
Text( Text(
@ -482,7 +485,7 @@ fun ownGroupLinkConfirmConnect(
SectionItemView({ SectionItemView({
AlertManager.privacySensitive.hideAlert() AlertManager.privacySensitive.hideAlert()
withBGApi { withBGApi {
connectViaUri(chatModel, rhId, uri, incognito = false, connectionPlan, close, cleanup) connectViaUri(chatModel, rhId, connectionLink, incognito = false, connectionPlan, close, cleanup)
} }
}) { }) {
Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)
@ -491,7 +494,7 @@ fun ownGroupLinkConfirmConnect(
SectionItemView({ SectionItemView({
AlertManager.privacySensitive.hideAlert() AlertManager.privacySensitive.hideAlert()
withBGApi { withBGApi {
connectViaUri(chatModel, rhId, uri, incognito = true, connectionPlan, close, cleanup) connectViaUri(chatModel, rhId, connectionLink, incognito = true, connectionPlan, close, cleanup)
} }
}) { }) {
Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error)

View file

@ -4,6 +4,7 @@ import SectionBottomSpacer
import SectionDividerSpaced import SectionDividerSpaced
import SectionTextFooter import SectionTextFooter
import SectionView import SectionView
import SectionViewWithButton
import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.* import androidx.compose.material.*
@ -31,14 +32,14 @@ import kotlinx.coroutines.*
fun ContactConnectionInfoView( fun ContactConnectionInfoView(
chatModel: ChatModel, chatModel: ChatModel,
rhId: Long?, rhId: Long?,
connReqInvitation: String?, connLinkInvitation: CreatedConnLink?,
contactConnection: PendingContactConnection, contactConnection: PendingContactConnection,
focusAlias: Boolean, focusAlias: Boolean,
close: () -> Unit close: () -> Unit
) { ) {
LaunchedEffect(connReqInvitation) { LaunchedEffect(connLinkInvitation) {
if (connReqInvitation != null) { if (connLinkInvitation != null) {
chatModel.showingInvitation.value = ShowingInvitation(contactConnection.id, connReqInvitation, false, conn = contactConnection) chatModel.showingInvitation.value = ShowingInvitation(contactConnection.id, connLinkInvitation, false, conn = contactConnection)
} }
} }
/** When [AddContactLearnMore] is open, we don't need to drop [ChatModel.showingInvitation]. /** When [AddContactLearnMore] is open, we don't need to drop [ChatModel.showingInvitation].
@ -53,16 +54,16 @@ fun ContactConnectionInfoView(
} }
} }
} }
val clipboard = LocalClipboardManager.current val showShortLink = remember { mutableStateOf(true) }
ContactConnectionInfoLayout( ContactConnectionInfoLayout(
chatModel = chatModel, chatModel = chatModel,
connReq = connReqInvitation, connLink = connLinkInvitation,
showShortLink = showShortLink,
contactConnection = contactConnection, contactConnection = contactConnection,
focusAlias = focusAlias, focusAlias = focusAlias,
rhId = rhId, rhId = rhId,
deleteConnection = { deleteContactConnectionAlert(rhId, contactConnection, chatModel, close) }, deleteConnection = { deleteContactConnectionAlert(rhId, contactConnection, chatModel, close) },
onLocalAliasChanged = { setContactAlias(rhId, contactConnection, it, chatModel) }, onLocalAliasChanged = { setContactAlias(rhId, contactConnection, it, chatModel) },
share = { if (connReqInvitation != null) clipboard.shareText(connReqInvitation) },
learnMore = { learnMore = {
ModalManager.end.showModalCloseable { close -> ModalManager.end.showModalCloseable { close ->
AddContactLearnMore(close) AddContactLearnMore(close)
@ -74,13 +75,13 @@ fun ContactConnectionInfoView(
@Composable @Composable
private fun ContactConnectionInfoLayout( private fun ContactConnectionInfoLayout(
chatModel: ChatModel, chatModel: ChatModel,
connReq: String?, connLink: CreatedConnLink?,
showShortLink: MutableState<Boolean>,
contactConnection: PendingContactConnection, contactConnection: PendingContactConnection,
focusAlias: Boolean, focusAlias: Boolean,
rhId: Long?, rhId: Long?,
deleteConnection: () -> Unit, deleteConnection: () -> Unit,
onLocalAliasChanged: (String) -> Unit, onLocalAliasChanged: (String) -> Unit,
share: () -> Unit,
learnMore: () -> Unit, learnMore: () -> Unit,
) { ) {
@Composable fun incognitoEnabled() { @Composable fun incognitoEnabled() {
@ -126,13 +127,19 @@ private fun ContactConnectionInfoLayout(
LocalAliasEditor(contactConnection.id, contactConnection.localAlias, center = false, leadingIcon = true, focus = focusAlias, updateValue = onLocalAliasChanged) LocalAliasEditor(contactConnection.id, contactConnection.localAlias, center = false, leadingIcon = true, focus = focusAlias, updateValue = onLocalAliasChanged)
} }
SectionView { if (connLink != null && connLink.connFullLink.isNotEmpty() && contactConnection.initiated) {
if (!connReq.isNullOrEmpty() && contactConnection.initiated) { Spacer(Modifier.height(DEFAULT_PADDING))
SimpleXLinkQRCode(connReq) SectionViewWithButton(
stringResource(MR.strings.one_time_link).uppercase(),
titleButton = if (connLink.connShortLink == null) null else {{ ToggleShortLinkButton(showShortLink) }}
) {
SimpleXCreatedLinkQRCode(connLink, short = showShortLink.value)
incognitoEnabled() incognitoEnabled()
ShareLinkButton(connReq) ShareLinkButton(connLink.simplexChatUri(short = showShortLink.value))
OneTimeLinkLearnMoreButton(learnMore) OneTimeLinkLearnMoreButton(learnMore)
}
} else { } else {
SectionView {
incognitoEnabled() incognitoEnabled()
OneTimeLinkLearnMoreButton(learnMore) OneTimeLinkLearnMoreButton(learnMore)
} }
@ -148,14 +155,14 @@ private fun ContactConnectionInfoLayout(
} }
@Composable @Composable
fun ShareLinkButton(connReqInvitation: String) { fun ShareLinkButton(linkUri: String) {
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
SettingsActionItem( SettingsActionItem(
painterResource(MR.images.ic_share), painterResource(MR.images.ic_share),
stringResource(MR.strings.share_invitation_link), stringResource(MR.strings.share_invitation_link),
click = { click = {
chatModel.showingInvitation.value = chatModel.showingInvitation.value?.copy(connChatUsed = true) chatModel.showingInvitation.value = chatModel.showingInvitation.value?.copy(connChatUsed = true)
clipboard.shareText(simplexChatLink(connReqInvitation)) clipboard.shareText(simplexChatLink(linkUri))
}, },
iconColor = MaterialTheme.colors.primary, iconColor = MaterialTheme.colors.primary,
textColor = MaterialTheme.colors.primary, textColor = MaterialTheme.colors.primary,
@ -200,13 +207,13 @@ private fun PreviewContactConnectionInfoView() {
SimpleXTheme { SimpleXTheme {
ContactConnectionInfoLayout( ContactConnectionInfoLayout(
chatModel = ChatModel, chatModel = ChatModel,
connReq = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", connLink = CreatedConnLink("https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", null),
showShortLink = remember { mutableStateOf(true) },
contactConnection = PendingContactConnection.getSampleData(), contactConnection = PendingContactConnection.getSampleData(),
focusAlias = false, focusAlias = false,
rhId = null, rhId = null,
deleteConnection = {}, deleteConnection = {},
onLocalAliasChanged = {}, onLocalAliasChanged = {},
share = {},
learnMore = {} learnMore = {}
) )
} }

View file

@ -4,6 +4,7 @@ import SectionBottomSpacer
import SectionItemView import SectionItemView
import SectionTextFooter import SectionTextFooter
import SectionView import SectionView
import SectionViewWithButton
import TextIconSpaced import TextIconSpaced
import androidx.compose.foundation.* import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
@ -48,17 +49,17 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC
val selection = remember { stateGetOrPut("selection") { selection } } val selection = remember { stateGetOrPut("selection") { selection } }
val showQRCodeScanner = remember { stateGetOrPut("showQRCodeScanner") { showQRCodeScanner } } val showQRCodeScanner = remember { stateGetOrPut("showQRCodeScanner") { showQRCodeScanner } }
val contactConnection: MutableState<PendingContactConnection?> = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(chatModel.showingInvitation.value?.conn) } val contactConnection: MutableState<PendingContactConnection?> = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(chatModel.showingInvitation.value?.conn) }
val connReqInvitation by remember { derivedStateOf { chatModel.showingInvitation.value?.connReq ?: "" } } val connLinkInvitation by remember { derivedStateOf { chatModel.showingInvitation.value?.connLink ?: CreatedConnLink("", null) } }
val creatingConnReq = rememberSaveable { mutableStateOf(false) } val creatingConnReq = rememberSaveable { mutableStateOf(false) }
val pastedLink = rememberSaveable { mutableStateOf("") } val pastedLink = rememberSaveable { mutableStateOf("") }
LaunchedEffect(selection.value) { LaunchedEffect(selection.value) {
if ( if (
selection.value == NewChatOption.INVITE selection.value == NewChatOption.INVITE
&& connReqInvitation.isEmpty() && connLinkInvitation.connFullLink.isEmpty()
&& contactConnection.value == null && contactConnection.value == null
&& !creatingConnReq.value && !creatingConnReq.value
) { ) {
createInvitation(rh?.remoteHostId, creatingConnReq, connReqInvitation, contactConnection) createInvitation(rh?.remoteHostId, creatingConnReq, connLinkInvitation, contactConnection)
} }
} }
DisposableEffect(Unit) { DisposableEffect(Unit) {
@ -143,12 +144,12 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.heightIn(min = this@BoxWithConstraints.maxHeight - 150.dp), .heightIn(min = this@BoxWithConstraints.maxHeight - 150.dp),
verticalArrangement = if (index == NewChatOption.INVITE.ordinal && connReqInvitation.isEmpty()) Arrangement.Center else Arrangement.Top verticalArrangement = if (index == NewChatOption.INVITE.ordinal && connLinkInvitation.connFullLink.isEmpty()) Arrangement.Center else Arrangement.Top
) { ) {
Spacer(Modifier.height(DEFAULT_PADDING)) Spacer(Modifier.height(DEFAULT_PADDING))
when (index) { when (index) {
NewChatOption.INVITE.ordinal -> { NewChatOption.INVITE.ordinal -> {
PrepareAndInviteView(rh?.remoteHostId, contactConnection, connReqInvitation, creatingConnReq) PrepareAndInviteView(rh?.remoteHostId, contactConnection, connLinkInvitation, creatingConnReq)
} }
NewChatOption.CONNECT.ordinal -> { NewChatOption.CONNECT.ordinal -> {
ConnectView(rh?.remoteHostId, showQRCodeScanner, pastedLink, close) ConnectView(rh?.remoteHostId, showQRCodeScanner, pastedLink, close)
@ -162,17 +163,17 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC
} }
@Composable @Composable
private fun PrepareAndInviteView(rhId: Long?, contactConnection: MutableState<PendingContactConnection?>, connReqInvitation: String, creatingConnReq: MutableState<Boolean>) { private fun PrepareAndInviteView(rhId: Long?, contactConnection: MutableState<PendingContactConnection?>, connLinkInvitation: CreatedConnLink, creatingConnReq: MutableState<Boolean>) {
if (connReqInvitation.isNotEmpty()) { if (connLinkInvitation.connFullLink.isNotEmpty()) {
InviteView( InviteView(
rhId, rhId,
connReqInvitation = connReqInvitation, connLinkInvitation = connLinkInvitation,
contactConnection = contactConnection, contactConnection = contactConnection,
) )
} else if (creatingConnReq.value) { } else if (creatingConnReq.value) {
CreatingLinkProgressView() CreatingLinkProgressView()
} else { } else {
RetryButton { createInvitation(rhId, creatingConnReq, connReqInvitation, contactConnection) } RetryButton { createInvitation(rhId, creatingConnReq, connLinkInvitation, contactConnection) }
} }
} }
@ -185,7 +186,7 @@ private fun updateShownConnection(conn: PendingContactConnection) {
chatModel.showingInvitation.value = chatModel.showingInvitation.value?.copy( chatModel.showingInvitation.value = chatModel.showingInvitation.value?.copy(
conn = conn, conn = conn,
connId = conn.id, connId = conn.id,
connReq = conn.connReqInv ?: "", connLink = conn.connLinkInv ?: CreatedConnLink("", null),
connChatUsed = true connChatUsed = true
) )
} }
@ -449,15 +450,21 @@ fun ActiveProfilePicker(
} }
@Composable @Composable
private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection: MutableState<PendingContactConnection?>) { private fun InviteView(rhId: Long?, connLinkInvitation: CreatedConnLink, contactConnection: MutableState<PendingContactConnection?>) {
SectionView(stringResource(MR.strings.share_this_1_time_link).uppercase(), headerBottomPadding = 5.dp) { val showShortLink = remember { mutableStateOf(true) }
LinkTextView(connReqInvitation, true)
}
Spacer(Modifier.height(10.dp)) Spacer(Modifier.height(10.dp))
SectionView(stringResource(MR.strings.or_show_this_qr_code).uppercase(), headerBottomPadding = 5.dp) { SectionView(stringResource(MR.strings.share_this_1_time_link).uppercase(), headerBottomPadding = 5.dp) {
SimpleXLinkQRCode(connReqInvitation, onShare = { chatModel.markShowingInvitationUsed() }) LinkTextView(connLinkInvitation.simplexChatUri(short = showShortLink.value), true)
}
Spacer(Modifier.height(DEFAULT_PADDING))
SectionViewWithButton(
stringResource(MR.strings.or_show_this_qr_code).uppercase(),
titleButton = if (connLinkInvitation.connShortLink != null) {{ ToggleShortLinkButton(showShortLink) }} else null
) {
SimpleXCreatedLinkQRCode(connLinkInvitation, short = showShortLink.value, onShare = { chatModel.markShowingInvitationUsed() })
} }
Spacer(Modifier.height(DEFAULT_PADDING)) Spacer(Modifier.height(DEFAULT_PADDING))
@ -528,6 +535,18 @@ private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection
} }
} }
@Composable
fun ToggleShortLinkButton(short: MutableState<Boolean>) {
Text(
stringResource(if (short.value) MR.strings.full_link_button_text else MR.strings.short_link_button_text),
modifier = Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null
) { short.value = !short.value },
style = MaterialTheme.typography.body2, fontSize = 14.sp, color = MaterialTheme.colors.primary
)
}
@Composable @Composable
fun AddContactLearnMoreButton() { fun AddContactLearnMoreButton() {
IconButton( IconButton(
@ -675,17 +694,17 @@ private suspend fun connect(rhId: Long?, link: String, close: () -> Unit, cleanu
private fun createInvitation( private fun createInvitation(
rhId: Long?, rhId: Long?,
creatingConnReq: MutableState<Boolean>, creatingConnReq: MutableState<Boolean>,
connReqInvitation: String, connLinkInvitation: CreatedConnLink,
contactConnection: MutableState<PendingContactConnection?> contactConnection: MutableState<PendingContactConnection?>
) { ) {
if (connReqInvitation.isNotEmpty() || contactConnection.value != null || creatingConnReq.value) return if (connLinkInvitation.connFullLink.isNotEmpty() || contactConnection.value != null || creatingConnReq.value) return
creatingConnReq.value = true creatingConnReq.value = true
withBGApi { withBGApi {
val (r, alert) = controller.apiAddContact(rhId, incognito = controller.appPrefs.incognito.get()) val (r, alert) = controller.apiAddContact(rhId, incognito = controller.appPrefs.incognito.get())
if (r != null) { if (r != null) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
chatModel.chatsContext.updateContactConnection(rhId, r.second) chatModel.chatsContext.updateContactConnection(rhId, r.second)
chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false, conn = r.second) chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connLink = r.first, connChatUsed = false, conn = r.second)
contactConnection.value = r.second contactConnection.value = r.second
} }
} else { } else {

View file

@ -12,13 +12,33 @@ import androidx.compose.ui.unit.dp
import dev.icerock.moko.resources.compose.stringResource import dev.icerock.moko.resources.compose.stringResource
import boofcv.alg.drawing.FiducialImageEngine import boofcv.alg.drawing.FiducialImageEngine
import boofcv.alg.fiducial.qrcode.* import boofcv.alg.fiducial.qrcode.*
import chat.simplex.common.model.CryptoFile import chat.simplex.common.model.*
import chat.simplex.common.platform.* import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.* import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR import chat.simplex.res.MR
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable
fun SimpleXCreatedLinkQRCode(
connLink: CreatedConnLink,
short: Boolean,
modifier: Modifier = Modifier,
padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING * 2f, vertical = DEFAULT_PADDING_HALF),
tintColor: Color = Color(0xff062d56),
withLogo: Boolean = true,
onShare: (() -> Unit)? = null,
) {
QRCode(
connLink.simplexChatUri(short),
modifier,
padding,
tintColor,
withLogo,
onShare,
)
}
@Composable @Composable
fun SimpleXLinkQRCode( fun SimpleXLinkQRCode(
connReq: String, connReq: String,
@ -38,14 +58,6 @@ fun SimpleXLinkQRCode(
) )
} }
fun simplexChatLink(uri: String): String {
return if (uri.startsWith("simplex:/")) {
uri.replace("simplex:/", "https://simplex.chat/")
} else {
uri
}
}
@Composable @Composable
fun QRCode( fun QRCode(
connReq: String, connReq: String,

View file

@ -88,6 +88,13 @@ fun PrivacySettingsView(
simplexLinkMode.set(it) simplexLinkMode.set(it)
chatModel.simplexLinkMode.value = it chatModel.simplexLinkMode.value = it
}) })
if (appPrefs.developerTools.get()) {
SettingsPreferenceItem(
null,
stringResource(MR.strings.privacy_short_links),
chatModel.controller.appPrefs.privacyShortLinks
)
}
} }
SectionDividerSpaced() SectionDividerSpaced()

View file

@ -5,6 +5,7 @@ import SectionDividerSpaced
import SectionItemView import SectionItemView
import SectionTextFooter import SectionTextFooter
import SectionView import SectionView
import SectionViewWithButton
import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@ -61,7 +62,8 @@ fun UserAddressView(
fun createAddress() { fun createAddress() {
withBGApi { withBGApi {
progressIndicator = true progressIndicator = true
val connReqContact = chatModel.controller.apiCreateUserAddress(user?.value?.remoteHostId) val short = appPreferences.privacyShortLinks.get()
val connReqContact = chatModel.controller.apiCreateUserAddress(user.value?.remoteHostId, short = short)
if (connReqContact != null) { if (connReqContact != null) {
chatModel.userAddress.value = UserContactLinkRec(connReqContact) chatModel.userAddress.value = UserContactLinkRec(connReqContact)
@ -102,7 +104,7 @@ fun UserAddressView(
sendEmail = { userAddress -> sendEmail = { userAddress ->
uriHandler.sendEmail( uriHandler.sendEmail(
generalGetString(MR.strings.email_invite_subject), generalGetString(MR.strings.email_invite_subject),
generalGetString(MR.strings.email_invite_body).format(simplexChatLink(userAddress.connReqContact)) generalGetString(MR.strings.email_invite_body).format(simplexChatLink(userAddress.connLinkContact.connFullLink)) // TODO [short links] replace with short link
) )
}, },
setProfileAddress = ::setProfileAddress, setProfileAddress = ::setProfileAddress,
@ -198,10 +200,14 @@ private fun UserAddressLayout(
} else { } else {
val autoAcceptState = remember { mutableStateOf(AutoAcceptState(userAddress)) } val autoAcceptState = remember { mutableStateOf(AutoAcceptState(userAddress)) }
val autoAcceptStateSaved = remember { mutableStateOf(autoAcceptState.value) } val autoAcceptStateSaved = remember { mutableStateOf(autoAcceptState.value) }
val showShortLink = remember { mutableStateOf(true) }
SectionView(stringResource(MR.strings.for_social_media).uppercase()) { SectionViewWithButton(
SimpleXLinkQRCode(userAddress.connReqContact) stringResource(MR.strings.for_social_media).uppercase(),
ShareAddressButton { share(simplexChatLink(userAddress.connReqContact)) } titleButton = if (userAddress.connLinkContact.connShortLink != null) {{ ToggleShortLinkButton(showShortLink) }} else null
) {
SimpleXCreatedLinkQRCode(userAddress.connLinkContact, short = showShortLink.value)
ShareAddressButton { share(userAddress.connLinkContact.simplexChatUri(short = showShortLink.value)) }
// ShareViaEmailButton { sendEmail(userAddress) } // ShareViaEmailButton { sendEmail(userAddress) }
BusinessAddressToggle(autoAcceptState) { saveAas(autoAcceptState.value, autoAcceptStateSaved) } BusinessAddressToggle(autoAcceptState) { saveAas(autoAcceptState.value, autoAcceptStateSaved) }
AddressSettingsButton(user, userAddress, shareViaProfile, setProfileAddress, saveAas) AddressSettingsButton(user, userAddress, shareViaProfile, setProfileAddress, saveAas)
@ -584,7 +590,7 @@ fun PreviewUserAddressLayoutAddressCreated() {
SimpleXTheme { SimpleXTheme {
UserAddressLayout( UserAddressLayout(
user = User.sampleData, user = User.sampleData,
userAddress = UserContactLinkRec("https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D"), userAddress = UserContactLinkRec(CreatedConnLink("https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", null)),
createAddress = {}, createAddress = {},
share = { _ -> }, share = { _ -> },
deleteAddress = {}, deleteAddress = {},

View file

@ -91,12 +91,14 @@
<string name="simplex_link_contact">SimpleX contact address</string> <string name="simplex_link_contact">SimpleX contact address</string>
<string name="simplex_link_invitation">SimpleX one-time invitation</string> <string name="simplex_link_invitation">SimpleX one-time invitation</string>
<string name="simplex_link_group">SimpleX group link</string> <string name="simplex_link_group">SimpleX group link</string>
<string name="simplex_link_channel">SimpleX channel link</string>
<string name="simplex_link_connection">via %1$s</string> <string name="simplex_link_connection">via %1$s</string>
<string name="simplex_link_mode">SimpleX links</string> <string name="simplex_link_mode">SimpleX links</string>
<string name="simplex_link_mode_description">Description</string> <string name="simplex_link_mode_description">Description</string>
<string name="simplex_link_mode_full">Full link</string> <string name="simplex_link_mode_full">Full link</string>
<string name="simplex_link_mode_browser">Via browser</string> <string name="simplex_link_mode_browser">Via browser</string>
<string name="simplex_link_mode_browser_warning">Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red.</string> <string name="simplex_link_mode_browser_warning">Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red.</string>
<string name="privacy_short_links">Use short links (BETA)</string>
<!-- Reports - ChatModel.kt --> <!-- Reports - ChatModel.kt -->
<string name="report_reason_spam">Spam</string> <string name="report_reason_spam">Spam</string>
@ -168,6 +170,8 @@
<string name="you_are_already_connected_to_vName_via_this_link">You are already connected to %1$s.</string> <string name="you_are_already_connected_to_vName_via_this_link">You are already connected to %1$s.</string>
<string name="invalid_connection_link">Invalid connection link</string> <string name="invalid_connection_link">Invalid connection link</string>
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Please check that you used the correct link or ask your contact to send you another one.</string> <string name="please_check_correct_link_and_maybe_ask_for_a_new_one">Please check that you used the correct link or ask your contact to send you another one.</string>
<string name="unsupported_connection_link">Unsupported connection link</string>
<string name="link_requires_newer_app_version_please_upgrade">This link requires a newer app version. Please upgrade the app or ask your contact to send a compatible link.</string>
<string name="connection_error_auth">Connection error (AUTH)</string> <string name="connection_error_auth">Connection error (AUTH)</string>
<string name="connection_error_auth_desc">Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection.</string> <string name="connection_error_auth_desc">Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection.</string>
<string name="connection_error_blocked">Connection blocked</string> <string name="connection_error_blocked">Connection blocked</string>
@ -795,6 +799,8 @@
<string name="one_time_link_short">1-time link</string> <string name="one_time_link_short">1-time link</string>
<string name="simplex_address">SimpleX address</string> <string name="simplex_address">SimpleX address</string>
<string name="or_show_this_qr_code">Or show this code</string> <string name="or_show_this_qr_code">Or show this code</string>
<string name="full_link_button_text">Full link</string>
<string name="short_link_button_text">Short link</string>
<string name="new_chat_share_profile">Share profile</string> <string name="new_chat_share_profile">Share profile</string>
<string name="select_chat_profile">Select chat profile</string> <string name="select_chat_profile">Select chat profile</string>
<string name="switching_profile_error_title">Error switching profile</string> <string name="switching_profile_error_title">Error switching profile</string>

View file

@ -61,8 +61,9 @@ import Simplex.Chat.Terminal.Main (simplexChatCLI')
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Shared
import Simplex.Chat.View (serializeChatResponse, simplexChatContact, viewContactName, viewGroupName) import Simplex.Chat.View (serializeChatResponse, simplexChatContact, viewContactName, viewGroupName)
import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ConnectionLink (..), CreatedConnLink (..))
import Simplex.Messaging.Agent.Store.Common (withTransaction) import Simplex.Messaging.Agent.Store.Common (withTransaction)
import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri (..), SConnectionMode (..), sameConnReqContact) import Simplex.Messaging.Agent.Protocol (SConnectionMode (..), sameConnReqContact, sameShortLinkContact)
import qualified Simplex.Messaging.Agent.Store.DB as DB import qualified Simplex.Messaging.Agent.Store.DB as DB
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.TMap (TMap)
@ -347,15 +348,15 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
setGroupRegOwner st gr owner setGroupRegOwner st gr owner
let GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = g let GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = g
notifyOwner gr $ "Joined the group " <> displayName <> ", creating the link…" notifyOwner gr $ "Joined the group " <> displayName <> ", creating the link…"
sendChatCmd cc (APICreateGroupLink groupId GRMember) >>= \case sendChatCmd cc (APICreateGroupLink groupId GRMember False) >>= \case
CRGroupLinkCreated {connReqContact} -> do CRGroupLinkCreated {connLinkContact = CCLink gLink _} -> do
setGroupStatus st gr GRSPendingUpdate setGroupStatus st gr GRSPendingUpdate
notifyOwner notifyOwner
gr gr
"Created the public link to join the group via this directory service that is always online.\n\n\ "Created the public link to join the group via this directory service that is always online.\n\n\
\Please add it to the group welcome message.\n\ \Please add it to the group welcome message.\n\
\For example, add:" \For example, add:"
notifyOwner gr $ "Link to join the group " <> displayName <> ": " <> strEncodeTxt (simplexChatContact connReqContact) notifyOwner gr $ "Link to join the group " <> displayName <> ": " <> strEncodeTxt (simplexChatContact gLink)
CRChatCmdError _ (ChatError e) -> case e of CRChatCmdError _ (ChatError e) -> case e of
CEGroupUserRole {} -> notifyOwner gr "Failed creating group link, as service is no longer an admin." CEGroupUserRole {} -> notifyOwner gr "Failed creating group link, as service is no longer an admin."
CEGroupMemberUserRemoved -> notifyOwner gr "Failed creating group link, as service is removed from the group." CEGroupMemberUserRemoved -> notifyOwner gr "Failed creating group link, as service is removed from the group."
@ -445,13 +446,15 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
groupProfileUpdate = profileUpdate <$> sendChatCmd cc (APIGetGroupLink groupId) groupProfileUpdate = profileUpdate <$> sendChatCmd cc (APIGetGroupLink groupId)
where where
profileUpdate = \case profileUpdate = \case
CRGroupLink {connReqContact} -> CRGroupLink {connLinkContact = CCLink cr sl_} ->
let hadLinkBefore = profileHasGroupLink fromGroup let hadLinkBefore = profileHasGroupLink fromGroup
hasLinkNow = profileHasGroupLink toGroup hasLinkNow = profileHasGroupLink toGroup
profileHasGroupLink GroupInfo {groupProfile = gp} = profileHasGroupLink GroupInfo {groupProfile = gp} =
maybe False (any ftHasLink) $ parseMaybeMarkdownList =<< description gp maybe False (any ftHasLink) $ parseMaybeMarkdownList =<< description gp
ftHasLink = \case ftHasLink = \case
FormattedText (Just SimplexLink {simplexUri = ACR SCMContact cr'}) _ -> sameConnReqContact connReqContact cr' FormattedText (Just SimplexLink {simplexUri = ACL SCMContact cLink}) _ -> case cLink of
CLFull cr' -> sameConnReqContact cr' cr
CLShort sl' -> maybe False (sameShortLinkContact sl') sl_
_ -> False _ -> False
in if in if
| hadLinkBefore && hasLinkNow -> GPHasServiceLink | hadLinkBefore && hasLinkNow -> GPHasServiceLink
@ -713,7 +716,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
case mRole_ of case mRole_ of
Nothing -> Nothing ->
getGroupLinkRole cc user g >>= \case getGroupLinkRole cc user g >>= \case
Just (_, gLink, mRole) -> do Just (_, CCLink gLink _, mRole) -> do
let anotherRole = case mRole of GRObserver -> GRMember; _ -> GRObserver let anotherRole = case mRole of GRObserver -> GRMember; _ -> GRObserver
sendReply $ sendReply $
initialRole n mRole initialRole n mRole
@ -893,10 +896,10 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
let groupRef = groupReference' groupId gName let groupRef = groupReference' groupId gName
withGroupAndReg sendReply groupId gName $ \_ _ -> withGroupAndReg sendReply groupId gName $ \_ _ ->
sendChatCmd cc (APIGetGroupLink groupId) >>= \case sendChatCmd cc (APIGetGroupLink groupId) >>= \case
CRGroupLink {connReqContact, memberRole} -> CRGroupLink {connLinkContact = CCLink cReq _, memberRole} ->
sendReply $ T.unlines sendReply $ T.unlines
[ "The link to join the group " <> groupRef <> ":", [ "The link to join the group " <> groupRef <> ":",
strEncodeTxt $ simplexChatContact connReqContact, strEncodeTxt $ simplexChatContact cReq,
"New member role: " <> strEncodeTxt memberRole "New member role: " <> strEncodeTxt memberRole
] ]
CRChatCmdError _ (ChatErrorStore (SEGroupLinkNotFound _)) -> CRChatCmdError _ (ChatErrorStore (SEGroupLinkNotFound _)) ->
@ -1039,7 +1042,7 @@ vr :: ChatController -> VersionRangeChat
vr ChatController {config = ChatConfig {chatVRange}} = chatVRange vr ChatController {config = ChatConfig {chatVRange}} = chatVRange
{-# INLINE vr #-} {-# INLINE vr #-}
getGroupLinkRole :: ChatController -> User -> GroupInfo -> IO (Maybe (Int64, ConnReqContact, GroupMemberRole)) getGroupLinkRole :: ChatController -> User -> GroupInfo -> IO (Maybe (Int64, CreatedLinkContact, GroupMemberRole))
getGroupLinkRole cc user gInfo = getGroupLinkRole cc user gInfo =
withDB "getGroupLink" cc $ \db -> getGroupLink db user gInfo withDB "getGroupLink" cc $ \db -> getGroupLink db user gInfo
@ -1047,7 +1050,7 @@ setGroupLinkRole :: ChatController -> GroupInfo -> GroupMemberRole -> IO (Maybe
setGroupLinkRole cc GroupInfo {groupId} mRole = resp <$> sendChatCmd cc (APIGroupLinkMemberRole groupId mRole) setGroupLinkRole cc GroupInfo {groupId} mRole = resp <$> sendChatCmd cc (APIGroupLinkMemberRole groupId mRole)
where where
resp = \case resp = \case
CRGroupLink _ _ gLink _ -> Just gLink CRGroupLink _ _ (CCLink gLink _) _ -> Just gLink
_ -> Nothing _ -> Nothing
unexpectedError :: Text -> Text unexpectedError :: Text -> Text

View file

@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
source-repository-package source-repository-package
type: git type: git
location: https://github.com/simplex-chat/simplexmq.git location: https://github.com/simplex-chat/simplexmq.git
tag: 9abc0fa88dd70a7e30a041697335bb663c1140b7 tag: 305f79d2a66a8d122bf457e023988200bb7fe00c
source-repository-package source-repository-package
type: git type: git

View file

@ -1,5 +1,5 @@
{ {
"https://github.com/simplex-chat/simplexmq.git"."9abc0fa88dd70a7e30a041697335bb663c1140b7" = "0gaqqvhb5s9xw5mq2iy8swp7w34zrkwkbjlyhggz2q9nr5680z84"; "https://github.com/simplex-chat/simplexmq.git"."305f79d2a66a8d122bf457e023988200bb7fe00c" = "1lawc5pf4hgc6wym2xz8gi92izi1vk98ppv3ldrpajz1mq62ifpc";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";

View file

@ -55,6 +55,7 @@ library
Simplex.Chat.Mobile.WebRTC Simplex.Chat.Mobile.WebRTC
Simplex.Chat.Operators Simplex.Chat.Operators
Simplex.Chat.Operators.Conditions Simplex.Chat.Operators.Conditions
Simplex.Chat.Operators.Presets
Simplex.Chat.Options Simplex.Chat.Options
Simplex.Chat.Options.DB Simplex.Chat.Options.DB
Simplex.Chat.ProfileGenerator Simplex.Chat.ProfileGenerator
@ -102,6 +103,7 @@ library
Simplex.Chat.Options.Postgres Simplex.Chat.Options.Postgres
Simplex.Chat.Store.Postgres.Migrations Simplex.Chat.Store.Postgres.Migrations
Simplex.Chat.Store.Postgres.Migrations.M20241220_initial Simplex.Chat.Store.Postgres.Migrations.M20241220_initial
Simplex.Chat.Store.Postgres.Migrations.M20250402_short_links
else else
exposed-modules: exposed-modules:
Simplex.Chat.Archive Simplex.Chat.Archive
@ -231,6 +233,7 @@ library
Simplex.Chat.Store.SQLite.Migrations.M20250126_mentions Simplex.Chat.Store.SQLite.Migrations.M20250126_mentions
Simplex.Chat.Store.SQLite.Migrations.M20250129_delete_unused_contacts Simplex.Chat.Store.SQLite.Migrations.M20250129_delete_unused_contacts
Simplex.Chat.Store.SQLite.Migrations.M20250130_indexes Simplex.Chat.Store.SQLite.Migrations.M20250130_indexes
Simplex.Chat.Store.SQLite.Migrations.M20250402_short_links
other-modules: other-modules:
Paths_simplex_chat Paths_simplex_chat
hs-source-dirs: hs-source-dirs:

View file

@ -30,6 +30,7 @@ import Data.Time.Clock (getCurrentTime)
import Simplex.Chat.Controller import Simplex.Chat.Controller
import Simplex.Chat.Library.Commands import Simplex.Chat.Library.Commands
import Simplex.Chat.Operators import Simplex.Chat.Operators
import Simplex.Chat.Operators.Presets
import Simplex.Chat.Options import Simplex.Chat.Options
import Simplex.Chat.Options.DB import Simplex.Chat.Options.DB
import Simplex.Chat.Protocol import Simplex.Chat.Protocol
@ -39,7 +40,7 @@ import Simplex.Chat.Types
import Simplex.Chat.Util (shuffle) import Simplex.Chat.Util (shuffle)
import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) import Simplex.FileTransfer.Client.Presets (defaultXFTPServers)
import Simplex.Messaging.Agent as Agent import Simplex.Messaging.Agent as Agent
import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), ServerCfg (..), ServerRoles (..), allRoles, createAgentStore, defaultAgentConfig, presetServerCfg) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), ServerCfg (..), allRoles, createAgentStore, defaultAgentConfig, presetServerCfg)
import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Protocol
import Simplex.Messaging.Agent.Store.Common (DBStore (dbNew)) import Simplex.Messaging.Agent.Store.Common (DBStore (dbNew))
import qualified Simplex.Messaging.Agent.Store.DB as DB import qualified Simplex.Messaging.Agent.Store.DB as DB
@ -51,34 +52,6 @@ import qualified Simplex.Messaging.TMap as TM
import qualified UnliftIO.Exception as E import qualified UnliftIO.Exception as E
import UnliftIO.STM import UnliftIO.STM
operatorSimpleXChat :: NewServerOperator
operatorSimpleXChat =
ServerOperator
{ operatorId = DBNewEntity,
operatorTag = Just OTSimplex,
tradeName = "SimpleX Chat",
legalName = Just "SimpleX Chat Ltd",
serverDomains = ["simplex.im"],
conditionsAcceptance = CARequired Nothing,
enabled = True,
smpRoles = allRoles,
xftpRoles = allRoles
}
operatorFlux :: NewServerOperator
operatorFlux =
ServerOperator
{ operatorId = DBNewEntity,
operatorTag = Just OTFlux,
tradeName = "Flux",
legalName = Just "InFlux Technologies Limited",
serverDomains = ["simplexonflux.com"],
conditionsAcceptance = CARequired Nothing,
enabled = False,
smpRoles = ServerRoles {storage = False, proxy = True},
xftpRoles = ServerRoles {storage = False, proxy = True}
}
defaultChatConfig :: ChatConfig defaultChatConfig :: ChatConfig
defaultChatConfig = defaultChatConfig =
ChatConfig ChatConfig
@ -112,6 +85,10 @@ defaultChatConfig =
ntf = _defaultNtfServers, ntf = _defaultNtfServers,
netCfg = defaultNetworkConfig netCfg = defaultNetworkConfig
}, },
-- please note: if these servers are changed, this option needs to be split to two,
-- to have a different set of servers on the receiving end and on the sending end.
-- To preserve backward compatibility receiving end should update before the sending.
shortLinkPresetServers = allPresetServers,
tbqSize = 1024, tbqSize = 1024,
fileChunkSize = 15780, -- do not change fileChunkSize = 15780, -- do not change
xftpDescrPartSize = 14000, xftpDescrPartSize = 14000,
@ -133,53 +110,6 @@ defaultChatConfig =
chatHooks = defaultChatHooks chatHooks = defaultChatHooks
} }
simplexChatSMPServers :: [NewUserServer 'PSMP]
simplexChatSMPServers =
map
(presetServer True)
[ "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion",
"smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im,jssqzccmrcws6bhmn77vgmhfjmhwlyr3u7puw4erkyoosywgl67slqqd.onion",
"smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im,rb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion",
"smp://1OwYGt-yqOfe2IyVHhxz3ohqo3aCCMjtB-8wn4X_aoY=@smp11.simplex.im,6ioorbm6i3yxmuoezrhjk6f6qgkc4syabh7m3so74xunb5nzr4pwgfqd.onion",
"smp://UkMFNAXLXeAAe0beCa4w6X_zp18PwxSaSjY17BKUGXQ=@smp12.simplex.im,ie42b5weq7zdkghocs3mgxdjeuycheeqqmksntj57rmejagmg4eor5yd.onion",
"smp://enEkec4hlR3UtKx2NMpOUK_K4ZuDxjWBO1d9Y4YXVaA=@smp14.simplex.im,aspkyu2sopsnizbyfabtsicikr2s4r3ti35jogbcekhm3fsoeyjvgrid.onion",
"smp://h--vW7ZSkXPeOUpfxlFGgauQmXNFOzGoizak7Ult7cw=@smp15.simplex.im,oauu4bgijybyhczbnxtlggo6hiubahmeutaqineuyy23aojpih3dajad.onion",
"smp://hejn2gVIqNU6xjtGM3OwQeuk8ZEbDXVJXAlnSBJBWUA=@smp16.simplex.im,p3ktngodzi6qrf7w64mmde3syuzrv57y55hxabqcq3l5p6oi7yzze6qd.onion",
"smp://ZKe4uxF4Z_aLJJOEsC-Y6hSkXgQS5-oc442JQGkyP8M=@smp17.simplex.im,ogtwfxyi3h2h5weftjjpjmxclhb5ugufa5rcyrmg7j4xlch7qsr5nuqd.onion",
"smp://PtsqghzQKU83kYTlQ1VKg996dW4Cw4x_bvpKmiv8uns=@smp18.simplex.im,lyqpnwbs2zqfr45jqkncwpywpbtq7jrhxnib5qddtr6npjyezuwd3nqd.onion",
"smp://N_McQS3F9TGoh4ER0QstUf55kGnNSd-wXfNPZ7HukcM=@smp19.simplex.im,i53bbtoqhlc365k6kxzwdp5w3cdt433s7bwh3y32rcbml2vztiyyz5id.onion"
]
<> map
(presetServer False)
[ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion",
"smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion",
"smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion"
]
fluxSMPServers :: [NewUserServer 'PSMP]
fluxSMPServers =
map
(presetServer True)
[ "smp://xQW_ufMkGE20UrTlBl8QqceG1tbuylXhr9VOLPyRJmw=@smp1.simplexonflux.com,qb4yoanyl4p7o33yrknv4rs6qo7ugeb2tu2zo66sbebezs4cpyosarid.onion",
"smp://LDnWZVlAUInmjmdpQQoIo6FUinRXGe0q3zi5okXDE4s=@smp2.simplexonflux.com,yiqtuh3q4x7hgovkomafsod52wvfjucdljqbbipg5sdssnklgongxbqd.onion",
"smp://1jne379u7IDJSxAvXbWb_JgoE7iabcslX0LBF22Rej0=@smp3.simplexonflux.com,a5lm4k7ufei66cdck6fy63r4lmkqy3dekmmb7jkfdm5ivi6kfaojshad.onion",
"smp://xmAmqj75I9mWrUihLUlI0ZuNLXlIwFIlHRq5Pb6cHAU=@smp4.simplexonflux.com,qpcz2axyy66u26hfdd2e23uohcf3y6c36mn7dcuilcgnwjasnrvnxjqd.onion",
"smp://rWvBYyTamuRCBYb_KAn-nsejg879ndhiTg5Sq3k0xWA=@smp5.simplexonflux.com,4ao347qwiuluyd45xunmii4skjigzuuox53hpdsgbwxqafd4yrticead.onion",
"smp://PN7-uqLBToqlf1NxHEaiL35lV2vBpXq8Nj8BW11bU48=@smp6.simplexonflux.com,hury6ot3ymebbr2535mlp7gcxzrjpc6oujhtfxcfh2m4fal4xw5fq6qd.onion"
]
fluxXFTPServers :: [NewUserServer 'PXFTP]
fluxXFTPServers =
map
(presetServer True)
[ "xftp://92Sctlc09vHl_nAqF2min88zKyjdYJ9mgxRCJns5K2U=@xftp1.simplexonflux.com,apl3pumq3emwqtrztykyyoomdx4dg6ysql5zek2bi3rgznz7ai3odkid.onion",
"xftp://YBXy4f5zU1CEhnbbCzVWTNVNsaETcAGmYqGNxHntiE8=@xftp2.simplexonflux.com,c5jjecisncnngysah3cz2mppediutfelco4asx65mi75d44njvua3xid.onion",
"xftp://ARQO74ZSvv2OrulRF3CdgwPz_AMy27r0phtLSq5b664=@xftp3.simplexonflux.com,dc4mohiubvbnsdfqqn7xhlhpqs5u4tjzp7xpz6v6corwvzvqjtaqqiqd.onion",
"xftp://ub2jmAa9U0uQCy90O-fSUNaYCj6sdhl49Jh3VpNXP58=@xftp4.simplexonflux.com,4qq5pzier3i4yhpuhcrhfbl6j25udc4czoyascrj4yswhodhfwev3nyd.onion",
"xftp://Rh19D5e4Eez37DEE9hAlXDB3gZa1BdFYJTPgJWPO9OI=@xftp5.simplexonflux.com,q7itltdn32hjmgcqwhow4tay5ijetng3ur32bolssw32fvc5jrwvozad.onion",
"xftp://0AznwoyfX8Od9T_acp1QeeKtxUi676IBIiQjXVwbdyU=@xftp6.simplexonflux.com,upvzf23ou6nrmaf3qgnhd6cn3d74tvivlmz3p7wdfwq6fhthjrjiiqid.onion"
]
logCfg :: LogConfig logCfg :: LogConfig
logCfg = LogConfig {lc_file = Nothing, lc_stderr = True} logCfg = LogConfig {lc_file = Nothing, lc_stderr = True}

View file

@ -15,6 +15,7 @@ import qualified Data.ByteString.Char8 as B
import Data.List.NonEmpty (NonEmpty) import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as L import qualified Data.List.NonEmpty as L
import qualified Data.Map.Strict as M import qualified Data.Map.Strict as M
import Data.Maybe (isJust)
import Data.Text (Text) import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import Simplex.Chat.Controller import Simplex.Chat.Controller
@ -24,6 +25,7 @@ import Simplex.Chat.Messages.CIContent
import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Protocol (MsgContent (..))
import Simplex.Chat.Store import Simplex.Chat.Store
import Simplex.Chat.Types (Contact (..), ContactId, IsContact (..), User (..)) import Simplex.Chat.Types (Contact (..), ContactId, IsContact (..), User (..))
import Simplex.Messaging.Agent.Protocol (CreatedConnLink (..))
import Simplex.Messaging.Encoding.String (strEncode) import Simplex.Messaging.Encoding.String (strEncode)
import System.Exit (exitFailure) import System.Exit (exitFailure)
@ -49,16 +51,18 @@ initializeBotAddress = initializeBotAddress' True
initializeBotAddress' :: Bool -> ChatController -> IO () initializeBotAddress' :: Bool -> ChatController -> IO ()
initializeBotAddress' logAddress cc = do initializeBotAddress' logAddress cc = do
sendChatCmd cc ShowMyAddress >>= \case sendChatCmd cc ShowMyAddress >>= \case
CRUserContactLink _ UserContactLink {connReqContact} -> showBotAddress connReqContact CRUserContactLink _ UserContactLink {connLinkContact} -> showBotAddress connLinkContact
CRChatCmdError _ (ChatErrorStore SEUserContactLinkNotFound) -> do CRChatCmdError _ (ChatErrorStore SEUserContactLinkNotFound) -> do
when logAddress $ putStrLn "No bot address, creating..." when logAddress $ putStrLn "No bot address, creating..."
sendChatCmd cc CreateMyAddress >>= \case -- TODO [short links] create short link by default
CRUserContactLinkCreated _ uri -> showBotAddress uri sendChatCmd cc (CreateMyAddress False) >>= \case
CRUserContactLinkCreated _ ccLink -> showBotAddress ccLink
_ -> putStrLn "can't create bot address" >> exitFailure _ -> putStrLn "can't create bot address" >> exitFailure
_ -> putStrLn "unexpected response" >> exitFailure _ -> putStrLn "unexpected response" >> exitFailure
where where
showBotAddress uri = do showBotAddress (CCLink uri shortUri) = do
when logAddress $ putStrLn $ "Bot's contact address is: " <> B.unpack (strEncode uri) when logAddress $ putStrLn $ "Bot's contact address is: " <> B.unpack (maybe (strEncode uri) strEncode shortUri)
when (isJust shortUri) $ putStrLn $ "Full contact address for old clients: " <> B.unpack (strEncode uri)
void $ sendChatCmd cc $ AddressAutoAccept $ Just AutoAccept {businessAddress = False, acceptIncognito = False, autoReply = Nothing} void $ sendChatCmd cc $ AddressAutoAccept $ Just AutoAccept {businessAddress = False, acceptIncognito = False, autoReply = Nothing}
sendMessage :: ChatController -> Contact -> Text -> IO () sendMessage :: ChatController -> Contact -> Text -> IO ()

View file

@ -138,6 +138,7 @@ data ChatConfig = ChatConfig
chatVRange :: VersionRangeChat, chatVRange :: VersionRangeChat,
confirmMigrations :: MigrationConfirmation, confirmMigrations :: MigrationConfirmation,
presetServers :: PresetServers, presetServers :: PresetServers,
shortLinkPresetServers :: NonEmpty SMPServer,
tbqSize :: Natural, tbqSize :: Natural,
fileChunkSize :: Integer, fileChunkSize :: Integer,
xftpDescrPartSize :: Int, xftpDescrPartSize :: Int,
@ -364,7 +365,7 @@ data ChatCommand
| APILeaveGroup GroupId | APILeaveGroup GroupId
| APIListMembers GroupId | APIListMembers GroupId
| APIUpdateGroupProfile GroupId GroupProfile | APIUpdateGroupProfile GroupId GroupProfile
| APICreateGroupLink GroupId GroupMemberRole | APICreateGroupLink GroupId GroupMemberRole CreateShortLink
| APIGroupLinkMemberRole GroupId GroupMemberRole | APIGroupLinkMemberRole GroupId GroupMemberRole
| APIDeleteGroupLink GroupId | APIDeleteGroupLink GroupId
| APIGetGroupLink GroupId | APIGetGroupLink GroupId
@ -437,21 +438,21 @@ data ChatCommand
| EnableGroupMember GroupName ContactName | EnableGroupMember GroupName ContactName
| ChatHelp HelpSection | ChatHelp HelpSection
| Welcome | Welcome
| APIAddContact UserId IncognitoEnabled | APIAddContact UserId CreateShortLink IncognitoEnabled
| AddContact IncognitoEnabled | AddContact CreateShortLink IncognitoEnabled
| APISetConnectionIncognito Int64 IncognitoEnabled | APISetConnectionIncognito Int64 IncognitoEnabled
| APIChangeConnectionUser Int64 UserId -- new user id to switch connection to | APIChangeConnectionUser Int64 UserId -- new user id to switch connection to
| APIConnectPlan UserId AConnectionRequestUri | APIConnectPlan UserId AConnectionLink
| APIConnect UserId IncognitoEnabled (Maybe AConnectionRequestUri) | APIConnect UserId IncognitoEnabled (Maybe ACreatedConnLink)
| Connect IncognitoEnabled (Maybe AConnectionRequestUri) | Connect IncognitoEnabled (Maybe AConnectionLink)
| APIConnectContactViaAddress UserId IncognitoEnabled ContactId | APIConnectContactViaAddress UserId IncognitoEnabled ContactId
| ConnectSimplex IncognitoEnabled -- UserId (not used in UI) | ConnectSimplex IncognitoEnabled -- UserId (not used in UI)
| DeleteContact ContactName ChatDeleteMode | DeleteContact ContactName ChatDeleteMode
| ClearContact ContactName | ClearContact ContactName
| APIListContacts UserId | APIListContacts UserId
| ListContacts | ListContacts
| APICreateMyAddress UserId | APICreateMyAddress UserId CreateShortLink
| CreateMyAddress | CreateMyAddress CreateShortLink
| APIDeleteMyAddress UserId | APIDeleteMyAddress UserId
| DeleteMyAddress | DeleteMyAddress
| APIShowMyAddress UserId | APIShowMyAddress UserId
@ -492,7 +493,7 @@ data ChatCommand
| ShowGroupProfile GroupName | ShowGroupProfile GroupName
| UpdateGroupDescription GroupName (Maybe Text) | UpdateGroupDescription GroupName (Maybe Text)
| ShowGroupDescription GroupName | ShowGroupDescription GroupName
| CreateGroupLink GroupName GroupMemberRole | CreateGroupLink GroupName GroupMemberRole CreateShortLink
| GroupLinkMemberRole GroupName GroupMemberRole | GroupLinkMemberRole GroupName GroupMemberRole
| DeleteGroupLink GroupName | DeleteGroupLink GroupName
| ShowGroupLink GroupName | ShowGroupLink GroupName
@ -674,10 +675,10 @@ data ChatResponse
| CRUserProfileNoChange {user :: User} | CRUserProfileNoChange {user :: User}
| CRUserPrivacy {user :: User, updatedUser :: User} | CRUserPrivacy {user :: User, updatedUser :: User}
| CRVersionInfo {versionInfo :: CoreVersionInfo, chatMigrations :: [UpMigration], agentMigrations :: [UpMigration]} | CRVersionInfo {versionInfo :: CoreVersionInfo, chatMigrations :: [UpMigration], agentMigrations :: [UpMigration]}
| CRInvitation {user :: User, connReqInvitation :: ConnReqInvitation, connection :: PendingContactConnection} | CRInvitation {user :: User, connLinkInvitation :: CreatedLinkInvitation, connection :: PendingContactConnection}
| CRConnectionIncognitoUpdated {user :: User, toConnection :: PendingContactConnection} | CRConnectionIncognitoUpdated {user :: User, toConnection :: PendingContactConnection}
| CRConnectionUserChanged {user :: User, fromConnection :: PendingContactConnection, toConnection :: PendingContactConnection, newUser :: User} | CRConnectionUserChanged {user :: User, fromConnection :: PendingContactConnection, toConnection :: PendingContactConnection, newUser :: User}
| CRConnectionPlan {user :: User, connectionPlan :: ConnectionPlan} | CRConnectionPlan {user :: User, connLink :: ACreatedConnLink, connectionPlan :: ConnectionPlan}
| CRSentConfirmation {user :: User, connection :: PendingContactConnection} | CRSentConfirmation {user :: User, connection :: PendingContactConnection}
| CRSentInvitation {user :: User, connection :: PendingContactConnection, customUserProfile :: Maybe Profile} | CRSentInvitation {user :: User, connection :: PendingContactConnection, customUserProfile :: Maybe Profile}
| CRSentInvitationToContact {user :: User, contact :: Contact, customUserProfile :: Maybe Profile} | CRSentInvitationToContact {user :: User, contact :: Contact, customUserProfile :: Maybe Profile}
@ -687,7 +688,7 @@ data ChatResponse
| CRContactDeleted {user :: User, contact :: Contact} | CRContactDeleted {user :: User, contact :: Contact}
| CRContactDeletedByContact {user :: User, contact :: Contact} | CRContactDeletedByContact {user :: User, contact :: Contact}
| CRChatCleared {user :: User, chatInfo :: AChatInfo} | CRChatCleared {user :: User, chatInfo :: AChatInfo}
| CRUserContactLinkCreated {user :: User, connReqContact :: ConnReqContact} | CRUserContactLinkCreated {user :: User, connLinkContact :: CreatedLinkContact}
| CRUserContactLinkDeleted {user :: User} | CRUserContactLinkDeleted {user :: User}
| CRReceivedContactRequest {user :: User, contactRequest :: UserContactRequest} | CRReceivedContactRequest {user :: User, contactRequest :: UserContactRequest}
| CRAcceptingContactRequest {user :: User, contact :: Contact} | CRAcceptingContactRequest {user :: User, contact :: Contact}
@ -765,8 +766,8 @@ data ChatResponse
| CRGroupUpdated {user :: User, fromGroup :: GroupInfo, toGroup :: GroupInfo, member_ :: Maybe GroupMember} | CRGroupUpdated {user :: User, fromGroup :: GroupInfo, toGroup :: GroupInfo, member_ :: Maybe GroupMember}
| CRGroupProfile {user :: User, groupInfo :: GroupInfo} | CRGroupProfile {user :: User, groupInfo :: GroupInfo}
| CRGroupDescription {user :: User, groupInfo :: GroupInfo} -- only used in CLI | CRGroupDescription {user :: User, groupInfo :: GroupInfo} -- only used in CLI
| CRGroupLinkCreated {user :: User, groupInfo :: GroupInfo, connReqContact :: ConnReqContact, memberRole :: GroupMemberRole} | CRGroupLinkCreated {user :: User, groupInfo :: GroupInfo, connLinkContact :: CreatedLinkContact, memberRole :: GroupMemberRole}
| CRGroupLink {user :: User, groupInfo :: GroupInfo, connReqContact :: ConnReqContact, memberRole :: GroupMemberRole} | CRGroupLink {user :: User, groupInfo :: GroupInfo, connLinkContact :: CreatedLinkContact, memberRole :: GroupMemberRole}
| CRGroupLinkDeleted {user :: User, groupInfo :: GroupInfo} | CRGroupLinkDeleted {user :: User, groupInfo :: GroupInfo}
| CRAcceptingGroupJoinRequestMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember} | CRAcceptingGroupJoinRequestMember {user :: User, groupInfo :: GroupInfo, member :: GroupMember}
| CRNoMemberContactCreating {user :: User, groupInfo :: GroupInfo, member :: GroupMember} -- only used in CLI | CRNoMemberContactCreating {user :: User, groupInfo :: GroupInfo, member :: GroupMember} -- only used in CLI
@ -941,6 +942,7 @@ data ConnectionPlan
= CPInvitationLink {invitationLinkPlan :: InvitationLinkPlan} = CPInvitationLink {invitationLinkPlan :: InvitationLinkPlan}
| CPContactAddress {contactAddressPlan :: ContactAddressPlan} | CPContactAddress {contactAddressPlan :: ContactAddressPlan}
| CPGroupLink {groupLinkPlan :: GroupLinkPlan} | CPGroupLink {groupLinkPlan :: GroupLinkPlan}
| CPError {chatError :: ChatError}
deriving (Show) deriving (Show)
data InvitationLinkPlan data InvitationLinkPlan
@ -984,6 +986,7 @@ connectionPlanProceed = \case
GLPOwnLink _ -> True GLPOwnLink _ -> True
GLPConnectingConfirmReconnect -> True GLPConnectingConfirmReconnect -> True
_ -> False _ -> False
CPError _ -> True
data ForwardConfirmation data ForwardConfirmation
= FCFilesNotAccepted {fileIds :: [FileTransferId]} = FCFilesNotAccepted {fileIds :: [FileTransferId]}
@ -1247,8 +1250,8 @@ data ChatErrorType
| CEChatNotStarted | CEChatNotStarted
| CEChatNotStopped | CEChatNotStopped
| CEChatStoreChanged | CEChatStoreChanged
| CEConnectionPlan {connectionPlan :: ConnectionPlan}
| CEInvalidConnReq | CEInvalidConnReq
| CEUnsupportedConnReq
| CEInvalidChatMessage {connection :: Connection, msgMeta :: Maybe MsgMetaJSON, messageData :: Text, message :: String} | CEInvalidChatMessage {connection :: Connection, msgMeta :: Maybe MsgMetaJSON, messageData :: Text, message :: String}
| CEContactNotFound {contactName :: ContactName, suspectedMember :: Maybe (GroupInfo, GroupMember)} | CEContactNotFound {contactName :: ContactName, suspectedMember :: Maybe (GroupInfo, GroupMember)}
| CEContactNotReady {contact :: Contact} | CEContactNotReady {contact :: Contact}
@ -1583,8 +1586,6 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CAP") ''ContactAddressPlan)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GLP") ''GroupLinkPlan) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GLP") ''GroupLinkPlan)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CP") ''ConnectionPlan)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "FC") ''ForwardConfirmation) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "FC") ''ForwardConfirmation)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CE") ''ChatErrorType) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CE") ''ChatErrorType)
@ -1599,6 +1600,8 @@ $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "DB") ''DatabaseError)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "Chat") ''ChatError) $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "Chat") ''ChatError)
$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "CP") ''ConnectionPlan)
$(JQ.deriveJSON defaultJSON ''AppFilePathsConfig) $(JQ.deriveJSON defaultJSON ''AppFilePathsConfig)
$(JQ.deriveJSON defaultJSON ''ContactSubStatus) $(JQ.deriveJSON defaultJSON ''ContactSubStatus)

View file

@ -1647,16 +1647,18 @@ processChatCommand' vr = \case
EnableGroupMember gName mName -> withMemberName gName mName $ \gId mId -> APIEnableGroupMember gId mId EnableGroupMember gName mName -> withMemberName gName mName $ \gId mId -> APIEnableGroupMember gId mId
ChatHelp section -> pure $ CRChatHelp section ChatHelp section -> pure $ CRChatHelp section
Welcome -> withUser $ pure . CRWelcome Welcome -> withUser $ pure . CRWelcome
APIAddContact userId incognito -> withUserId userId $ \user -> procCmd $ do APIAddContact userId short incognito -> withUserId userId $ \user -> procCmd $ do
-- [incognito] generate profile for connection -- [incognito] generate profile for connection
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
subMode <- chatReadVar subscriptionMode subMode <- chatReadVar subscriptionMode
(connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing IKPQOn subMode let userData = shortLinkUserData short
(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 -- TODO PQ pass minVersion from the current range
conn <- withFastStore' $ \db -> createDirectConnection db user connId cReq ConnNew incognitoProfile subMode initialChatVersion PQSupportOn conn <- withFastStore' $ \db -> createDirectConnection db user connId ccLink' ConnNew incognitoProfile subMode initialChatVersion PQSupportOn
pure $ CRInvitation user cReq conn pure $ CRInvitation user ccLink' conn
AddContact incognito -> withUser $ \User {userId} -> AddContact short incognito -> withUser $ \User {userId} ->
processChatCommand $ APIAddContact userId incognito processChatCommand $ APIAddContact userId short incognito
APISetConnectionIncognito connId incognito -> withUser $ \user@User {userId} -> do APISetConnectionIncognito connId incognito -> withUser $ \user@User {userId} -> do
conn'_ <- withFastStore $ \db -> do conn'_ <- withFastStore $ \db -> do
conn@PendingContactConnection {pccConnStatus, customUserProfileId} <- getPendingContactConnection db userId connId conn@PendingContactConnection {pccConnStatus, customUserProfileId} <- getPendingContactConnection db userId connId
@ -1674,9 +1676,9 @@ processChatCommand' vr = \case
Nothing -> throwChatError CEConnectionIncognitoChangeProhibited Nothing -> throwChatError CEConnectionIncognitoChangeProhibited
APIChangeConnectionUser connId newUserId -> withUser $ \user@User {userId} -> do APIChangeConnectionUser connId newUserId -> withUser $ \user@User {userId} -> do
conn <- withFastStore $ \db -> getPendingContactConnection db userId connId conn <- withFastStore $ \db -> getPendingContactConnection db userId connId
let PendingContactConnection {pccConnStatus, connReqInv} = conn let PendingContactConnection {pccConnStatus, connLinkInv} = conn
case (pccConnStatus, connReqInv) of case (pccConnStatus, connLinkInv) of
(ConnNew, Just cReqInv) -> do (ConnNew, Just (CCLink cReqInv _)) -> do
newUser <- privateGetUser newUserId newUser <- privateGetUser newUserId
conn' <- ifM (canKeepLink cReqInv newUser) (updateConnRecord user conn newUser) (recreateConn user conn newUser) conn' <- ifM (canKeepLink cReqInv newUser) (updateConnRecord user conn newUser) (recreateConn user conn newUser)
pure $ CRConnectionUserChanged user conn conn' newUser pure $ CRConnectionUserChanged user conn conn' newUser
@ -1697,19 +1699,21 @@ processChatCommand' vr = \case
forM_ customUserProfileId $ \profileId -> forM_ customUserProfileId $ \profileId ->
deletePCCIncognitoProfile db user profileId deletePCCIncognitoProfile db user profileId
pure conn' pure conn'
recreateConn user conn@PendingContactConnection {customUserProfileId} newUser = do recreateConn user conn@PendingContactConnection {customUserProfileId, connLinkInv} newUser = do
subMode <- chatReadVar subscriptionMode subMode <- chatReadVar subscriptionMode
(agConnId, cReq) <- withAgent $ \a -> createConnection a (aUserId newUser) True SCMInvitation Nothing IKPQOn subMode let userData = shortLinkUserData $ isJust $ connShortLink =<< connLinkInv
(agConnId, ccLink) <- withAgent $ \a -> createConnection a (aUserId newUser) True SCMInvitation userData Nothing IKPQOn subMode
ccLink' <- shortenCreatedLink ccLink
conn' <- withFastStore' $ \db -> do conn' <- withFastStore' $ \db -> do
deleteConnectionRecord db user connId deleteConnectionRecord db user connId
forM_ customUserProfileId $ \profileId -> forM_ customUserProfileId $ \profileId ->
deletePCCIncognitoProfile db user profileId deletePCCIncognitoProfile db user profileId
createDirectConnection db newUser agConnId cReq ConnNew Nothing subMode initialChatVersion PQSupportOn createDirectConnection db newUser agConnId ccLink' ConnNew Nothing subMode initialChatVersion PQSupportOn
deleteAgentConnectionAsync user (aConnId' conn) deleteAgentConnectionAsync user (aConnId' conn)
pure conn' pure conn'
APIConnectPlan userId cReqUri -> withUserId userId $ \user -> APIConnectPlan userId cLink -> withUserId userId $ \user ->
CRConnectionPlan user <$> connectPlan user cReqUri uncurry (CRConnectionPlan user) <$> connectPlan user cLink
APIConnect userId incognito (Just (ACR SCMInvitation cReq@(CRInvitationUri crData e2e))) -> withUserId userId $ \user -> withInvitationLock "connect" (strEncode cReq) . procCmd $ do APIConnect userId incognito (Just (ACCL SCMInvitation (CCLink cReq@(CRInvitationUri crData e2e) sLnk_))) -> withUserId userId $ \user -> withInvitationLock "connect" (strEncode cReq) . procCmd $ do
subMode <- chatReadVar subscriptionMode subMode <- chatReadVar subscriptionMode
-- [incognito] generate profile to send -- [incognito] generate profile to send
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
@ -1732,7 +1736,8 @@ processChatCommand' vr = \case
where where
joinNewConn chatV dm = do joinNewConn chatV dm = do
connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup' connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup'
pcc <- withFastStore' $ \db -> createDirectConnection db user connId cReq ConnPrepared (incognitoProfile $> profileToSend) subMode chatV 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 dm
joinPreparedConn connId pcc@PendingContactConnection {pccConnId} dm = do joinPreparedConn connId pcc@PendingContactConnection {pccConnId} dm = do
void $ withAgent $ \a -> joinConnection a (aUserId user) connId True cReq dm pqSup' subMode void $ withAgent $ \a -> joinConnection a (aUserId user) connId True cReq dm pqSup' subMode
@ -1742,43 +1747,40 @@ processChatCommand' vr = \case
( CRInvitationUri crData {crScheme = SSSimplex} e2e, ( CRInvitationUri crData {crScheme = SSSimplex} e2e,
CRInvitationUri crData {crScheme = simplexChat} e2e CRInvitationUri crData {crScheme = simplexChat} e2e
) )
APIConnect userId incognito (Just (ACR SCMContact cReq)) -> withUserId userId $ \user -> connectViaContact user incognito cReq APIConnect userId incognito (Just (ACCL SCMContact ccLink)) -> withUserId userId $ \user -> connectViaContact user incognito ccLink
APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq
Connect incognito aCReqUri@(Just cReqUri) -> withUser $ \user@User {userId} -> do Connect incognito (Just cLink@(ACL m cLink')) -> withUser $ \user -> do
plan <- connectPlan user cReqUri `catchChatError` const (pure $ CPInvitationLink ILPOk) (ccLink, plan) <- connectPlan user cLink `catchChatError` \e -> case cLink' of CLFull cReq -> pure (ACCL m (CCLink cReq Nothing), CPInvitationLink ILPOk); _ -> throwError e
unless (connectionPlanProceed plan) $ throwChatError (CEConnectionPlan plan) connectWithPlan user incognito ccLink plan
case plan of
CPContactAddress (CAPContactViaAddress Contact {contactId}) ->
processChatCommand $ APIConnectContactViaAddress userId incognito contactId
_ -> processChatCommand $ APIConnect userId incognito aCReqUri
Connect _ Nothing -> throwChatError CEInvalidConnReq Connect _ Nothing -> throwChatError CEInvalidConnReq
APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do
ct@Contact {activeConn, profile = LocalProfile {contactLink}} <- withFastStore $ \db -> getContact db vr user contactId ct@Contact {activeConn, profile = LocalProfile {contactLink}} <- withFastStore $ \db -> getContact db vr user contactId
when (isJust activeConn) $ throwChatError (CECommandError "contact already has connection") when (isJust activeConn) $ throwChatError (CECommandError "contact already has connection")
case contactLink of ccLink <- case contactLink of
Just cReq -> connectContactViaAddress user incognito ct cReq Just (CLFull cReq) -> pure $ CCLink cReq Nothing
Just (CLShort sLnk) -> do
cReq <- getShortLinkConnReq user sLnk
pure $ CCLink cReq $ Just sLnk
Nothing -> throwChatError (CECommandError "no address in contact profile") Nothing -> throwChatError (CECommandError "no address in contact profile")
ConnectSimplex incognito -> withUser $ \user@User {userId} -> do connectContactViaAddress user incognito ct ccLink
let cReqUri = ACR SCMContact adminContactReq ConnectSimplex incognito -> withUser $ \user -> do
plan <- connectPlan user cReqUri `catchChatError` const (pure $ CPInvitationLink ILPOk) plan <- contactRequestPlan user adminContactReq `catchChatError` const (pure $ CPContactAddress CAPOk)
unless (connectionPlanProceed plan) $ throwChatError (CEConnectionPlan plan) connectWithPlan user incognito (ACCL SCMContact (CCLink adminContactReq Nothing)) plan
case plan of
CPContactAddress (CAPContactViaAddress Contact {contactId}) ->
processChatCommand $ APIConnectContactViaAddress userId incognito contactId
_ -> processChatCommand $ APIConnect userId incognito (Just cReqUri)
DeleteContact cName cdm -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId) cdm DeleteContact cName cdm -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId) cdm
ClearContact cName -> withContactName cName $ APIClearChat . ChatRef CTDirect ClearContact cName -> withContactName cName $ APIClearChat . ChatRef CTDirect
APIListContacts userId -> withUserId userId $ \user -> APIListContacts userId -> withUserId userId $ \user ->
CRContactsList user <$> withFastStore' (\db -> getUserContacts db vr user) CRContactsList user <$> withFastStore' (\db -> getUserContacts db vr user)
ListContacts -> withUser $ \User {userId} -> ListContacts -> withUser $ \User {userId} ->
processChatCommand $ APIListContacts userId processChatCommand $ APIListContacts userId
APICreateMyAddress userId -> withUserId userId $ \user -> procCmd $ do APICreateMyAddress userId short -> withUserId userId $ \user -> procCmd $ do
subMode <- chatReadVar subscriptionMode subMode <- chatReadVar subscriptionMode
(connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact Nothing IKPQOn subMode let userData = shortLinkUserData short
withFastStore $ \db -> createUserContactLink db user connId cReq subMode (connId, ccLink) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact userData Nothing IKPQOn subMode
pure $ CRUserContactLinkCreated user cReq ccLink' <- shortenCreatedLink ccLink
CreateMyAddress -> withUser $ \User {userId} -> withFastStore $ \db -> createUserContactLink db user connId ccLink' subMode
processChatCommand $ APICreateMyAddress userId pure $ CRUserContactLinkCreated user ccLink'
CreateMyAddress short -> withUser $ \User {userId} ->
processChatCommand $ APICreateMyAddress userId short
APIDeleteMyAddress userId -> withUserId userId $ \user@User {profile = p} -> do APIDeleteMyAddress userId -> withUserId userId $ \user@User {profile = p} -> do
conns <- withFastStore $ \db -> getUserAddressConnections db vr user conns <- withFastStore $ \db -> getUserAddressConnections db vr user
withChatLock "deleteMyAddress" $ do withChatLock "deleteMyAddress" $ do
@ -1800,8 +1802,9 @@ processChatCommand' vr = \case
let p' = (fromLocalProfile p :: Profile) {contactLink = Nothing} let p' = (fromLocalProfile p :: Profile) {contactLink = Nothing}
updateProfile_ user p' $ withFastStore' $ \db -> setUserProfileContactLink db user Nothing updateProfile_ user p' $ withFastStore' $ \db -> setUserProfileContactLink db user Nothing
APISetProfileAddress userId True -> withUserId userId $ \user@User {profile = p} -> do APISetProfileAddress userId True -> withUserId userId $ \user@User {profile = p} -> do
ucl@UserContactLink {connReqContact} <- withFastStore (`getUserAddress` user) ucl@UserContactLink {connLinkContact = CCLink cReq _} <- withFastStore (`getUserAddress` user)
let p' = (fromLocalProfile p :: Profile) {contactLink = Just connReqContact} -- TODO [short links] replace with short links
let p' = (fromLocalProfile p :: Profile) {contactLink = Just $ CLFull cReq}
updateProfile_ user p' $ withFastStore' $ \db -> setUserProfileContactLink db user $ Just ucl updateProfile_ user p' $ withFastStore' $ \db -> setUserProfileContactLink db user $ Just ucl
SetProfileAddress onOff -> withUser $ \User {userId} -> SetProfileAddress onOff -> withUser $ \User {userId} ->
processChatCommand $ APISetProfileAddress userId onOff processChatCommand $ APISetProfileAddress userId onOff
@ -1979,7 +1982,7 @@ processChatCommand' vr = \case
Nothing -> do Nothing -> do
gVar <- asks random gVar <- asks random
subMode <- chatReadVar subscriptionMode subMode <- chatReadVar subscriptionMode
(agentConnId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing IKPQOff subMode (agentConnId, CCLink cReq _) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing Nothing IKPQOff subMode
member <- withFastStore $ \db -> createNewContactMember db gVar user gInfo contact memRole agentConnId cReq subMode member <- withFastStore $ \db -> createNewContactMember db gVar user gInfo contact memRole agentConnId cReq subMode
sendInvitation member cReq sendInvitation member cReq
pure $ CRSentGroupInvitation user gInfo contact member pure $ CRSentGroupInvitation user gInfo contact member
@ -2276,16 +2279,18 @@ processChatCommand' vr = \case
updateGroupProfileByName gName $ \p -> p {description} updateGroupProfileByName gName $ \p -> p {description}
ShowGroupDescription gName -> withUser $ \user -> ShowGroupDescription gName -> withUser $ \user ->
CRGroupDescription user <$> withFastStore (\db -> getGroupInfoByName db vr user gName) CRGroupDescription user <$> withFastStore (\db -> getGroupInfoByName db vr user gName)
APICreateGroupLink groupId mRole -> withUser $ \user -> withGroupLock "createGroupLink" groupId $ do APICreateGroupLink groupId mRole short -> withUser $ \user -> withGroupLock "createGroupLink" groupId $ do
gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId
assertUserGroupRole gInfo GRAdmin assertUserGroupRole gInfo GRAdmin
when (mRole > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole when (mRole > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole
groupLinkId <- GroupLinkId <$> drgRandomBytes 16 groupLinkId <- GroupLinkId <$> drgRandomBytes 16
subMode <- chatReadVar subscriptionMode subMode <- chatReadVar subscriptionMode
let crClientData = encodeJSON $ CRDataGroup groupLinkId let crClientData = encodeJSON $ CRDataGroup groupLinkId
(connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact (Just crClientData) IKPQOff subMode userData = shortLinkUserData short
withFastStore $ \db -> createGroupLink db user gInfo connId cReq groupLinkId mRole subMode (connId, ccLink) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact userData (Just crClientData) IKPQOff subMode
pure $ CRGroupLinkCreated user gInfo cReq mRole ccLink' <- createdGroupLink <$> shortenCreatedLink ccLink
withFastStore $ \db -> createGroupLink db user gInfo connId ccLink' groupLinkId mRole subMode
pure $ CRGroupLinkCreated user gInfo ccLink' mRole
APIGroupLinkMemberRole groupId mRole' -> withUser $ \user -> withGroupLock "groupLinkMemberRole" groupId $ do APIGroupLinkMemberRole groupId mRole' -> withUser $ \user -> withGroupLock "groupLinkMemberRole" groupId $ do
gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId
(groupLinkId, groupLink, mRole) <- withFastStore $ \db -> getGroupLink db user gInfo (groupLinkId, groupLink, mRole) <- withFastStore $ \db -> getGroupLink db user gInfo
@ -2311,7 +2316,7 @@ processChatCommand' vr = \case
when (isJust $ memberContactId m) $ throwChatError $ CECommandError "member contact already exists" when (isJust $ memberContactId m) $ throwChatError $ CECommandError "member contact already exists"
subMode <- chatReadVar subscriptionMode subMode <- chatReadVar subscriptionMode
-- TODO PQ should negotitate contact connection with PQSupportOn? -- TODO PQ should negotitate contact connection with PQSupportOn?
(connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing IKPQOff subMode (connId, CCLink cReq _) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing Nothing IKPQOff subMode
-- [incognito] reuse membership incognito profile -- [incognito] reuse membership incognito profile
ct <- withFastStore' $ \db -> createMemberContact db user connId cReq g m mConn subMode ct <- withFastStore' $ \db -> createMemberContact db user connId cReq g m mConn subMode
-- TODO not sure it is correct to set connections status here? -- TODO not sure it is correct to set connections status here?
@ -2332,9 +2337,9 @@ processChatCommand' vr = \case
toView $ CRNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct') ci] toView $ CRNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct') ci]
pure $ CRNewMemberContactSentInv user ct' g m pure $ CRNewMemberContactSentInv user ct' g m
_ -> throwChatError CEGroupMemberNotActive _ -> throwChatError CEGroupMemberNotActive
CreateGroupLink gName mRole -> withUser $ \user -> do CreateGroupLink gName mRole short -> withUser $ \user -> do
groupId <- withFastStore $ \db -> getGroupIdByName db user gName groupId <- withFastStore $ \db -> getGroupIdByName db user gName
processChatCommand $ APICreateGroupLink groupId mRole processChatCommand $ APICreateGroupLink groupId mRole short
GroupLinkMemberRole gName mRole -> withUser $ \user -> do GroupLinkMemberRole gName mRole -> withUser $ \user -> do
groupId <- withFastStore $ \db -> getGroupIdByName db user gName groupId <- withFastStore $ \db -> getGroupIdByName db user gName
processChatCommand $ APIGroupLinkMemberRole groupId mRole processChatCommand $ APIGroupLinkMemberRole groupId mRole
@ -2671,8 +2676,8 @@ processChatCommand' vr = \case
CTGroup -> withFastStore $ \db -> getGroupChatItemIdByText' db user cId msg CTGroup -> withFastStore $ \db -> getGroupChatItemIdByText' db user cId msg
CTLocal -> withFastStore $ \db -> getLocalChatItemIdByText' db user cId msg CTLocal -> withFastStore $ \db -> getLocalChatItemIdByText' db user cId msg
_ -> throwChatError $ CECommandError "not supported" _ -> throwChatError $ CECommandError "not supported"
connectViaContact :: User -> IncognitoEnabled -> ConnectionRequestUri 'CMContact -> CM ChatResponse connectViaContact :: User -> IncognitoEnabled -> CreatedLinkContact -> CM ChatResponse
connectViaContact user@User {userId} incognito cReq@(CRContactUri ConnReqUriData {crClientData}) = withInvitationLock "connectViaContact" (strEncode cReq) $ do connectViaContact user@User {userId} incognito (CCLink cReq@(CRContactUri ConnReqUriData {crClientData}) sLnk) = withInvitationLock "connectViaContact" (strEncode cReq) $ do
let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli let groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli
cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq
case groupLinkId of case groupLinkId of
@ -2702,11 +2707,12 @@ processChatCommand' vr = \case
-- [incognito] generate profile to send -- [incognito] generate profile to send
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
subMode <- chatReadVar subscriptionMode subMode <- chatReadVar subscriptionMode
conn@PendingContactConnection {pccConnId} <- withFastStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId subMode chatV pqSup let sLnk' = serverShortLink <$> sLnk
conn@PendingContactConnection {pccConnId} <- withFastStore' $ \db -> createConnReqConnection db userId connId cReqHash sLnk' xContactId incognitoProfile groupLinkId subMode chatV pqSup
joinContact user pccConnId connId cReq incognitoProfile xContactId inGroup pqSup chatV joinContact user pccConnId connId cReq incognitoProfile xContactId inGroup pqSup chatV
pure $ CRSentInvitation user conn incognitoProfile pure $ CRSentInvitation user conn incognitoProfile
connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> ConnectionRequestUri 'CMContact -> CM ChatResponse connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> CreatedLinkContact -> CM ChatResponse
connectContactViaAddress user incognito ct cReq = connectContactViaAddress user incognito ct (CCLink cReq shortLink) =
withInvitationLock "connectContactViaAddress" (strEncode cReq) $ do withInvitationLock "connectContactViaAddress" (strEncode cReq) $ do
newXContactId <- XContactId <$> drgRandomBytes 16 newXContactId <- XContactId <$> drgRandomBytes 16
let pqSup = PQSupportOn let pqSup = PQSupportOn
@ -2715,10 +2721,10 @@ processChatCommand' vr = \case
-- [incognito] generate profile to send -- [incognito] generate profile to send
incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing
subMode <- chatReadVar subscriptionMode subMode <- chatReadVar subscriptionMode
(pccConnId, ct') <- withFastStore $ \db -> createAddressContactConnection db vr user ct connId cReqHash newXContactId incognitoProfile subMode chatV pqSup (pccConnId, ct') <- withFastStore $ \db -> createAddressContactConnection db vr user ct connId cReqHash shortLink newXContactId incognitoProfile subMode chatV pqSup
joinContact user pccConnId connId cReq incognitoProfile newXContactId False pqSup chatV joinContact user pccConnId connId cReq incognitoProfile newXContactId False pqSup chatV
pure $ CRSentInvitationToContact user ct' incognitoProfile pure $ CRSentInvitationToContact user ct' incognitoProfile
prepareContact :: User -> ConnectionRequestUri 'CMContact -> PQSupport -> CM (ConnId, VersionChat) prepareContact :: User -> ConnReqContact -> PQSupport -> CM (ConnId, VersionChat)
prepareContact user cReq pqSup = do prepareContact user cReq pqSup = do
-- 0) toggle disabled - PQSupportOff -- 0) toggle disabled - PQSupportOff
-- 1) toggle enabled, address supports PQ (connRequestPQSupport returns Just True) - PQSupportOn, enable support with compression -- 1) toggle enabled, address supports PQ (connRequestPQSupport returns Just True) - PQSupportOn, enable support with compression
@ -2729,7 +2735,7 @@ processChatCommand' vr = \case
let chatV = agentToChatVersion agentV let chatV = agentToChatVersion agentV
connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup
pure (connId, chatV) pure (connId, chatV)
joinContact :: User -> Int64 -> ConnId -> ConnectionRequestUri 'CMContact -> Maybe Profile -> XContactId -> Bool -> PQSupport -> VersionChat -> CM () joinContact :: User -> Int64 -> ConnId -> ConnReqContact -> Maybe Profile -> XContactId -> Bool -> PQSupport -> VersionChat -> CM ()
joinContact user pccConnId connId cReq incognitoProfile xContactId inGroup pqSup chatV = do joinContact user pccConnId connId cReq incognitoProfile xContactId inGroup pqSup chatV = do
let profileToSend = userProfileToSend user incognitoProfile Nothing inGroup let profileToSend = userProfileToSend user incognitoProfile Nothing inGroup
dm <- encodeConnInfoPQ pqSup chatV (XContact profileToSend $ Just xContactId) dm <- encodeConnInfoPQ pqSup chatV (XContact profileToSend $ Just xContactId)
@ -3034,13 +3040,62 @@ processChatCommand' vr = \case
pure (gId, chatSettings) pure (gId, chatSettings)
_ -> throwChatError $ CECommandError "not supported" _ -> throwChatError $ CECommandError "not supported"
processChatCommand $ APISetChatSettings (ChatRef cType chatId) $ updateSettings chatSettings processChatCommand $ APISetChatSettings (ChatRef cType chatId) $ updateSettings chatSettings
connectPlan :: User -> AConnectionRequestUri -> CM ConnectionPlan connectPlan :: User -> AConnectionLink -> CM (ACreatedConnLink, ConnectionPlan)
connectPlan user (ACR SCMInvitation (CRInvitationUri crData e2e)) = do connectPlan user (ACL SCMInvitation cLink) = case cLink of
withFastStore' (\db -> getConnectionEntityByConnReq db vr user cReqSchemas) >>= \case CLFull cReq -> invitationReqAndPlan cReq Nothing
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))
Nothing -> getShortLinkConnReq user l' >>= (`invitationReqAndPlan` Just l')
where
invitationReqAndPlan cReq sLnk_ = do
plan <- inviationRequestPlan user cReq `catchChatError` (pure . CPError)
pure (ACCL SCMInvitation (CCLink cReq sLnk_), plan)
connectPlan user (ACL SCMContact cLink) = case cLink of
CLFull cReq -> contactReqAndPlan cReq Nothing
CLShort l@(CSLContact _ ct _ _) -> do
let l' = serverShortLink l
case ct of
CCTContact ->
withFastStore' (\db -> getUserContactLinkViaShortLink db user l') >>= \case
Just (UserContactLink (CCLink cReq _) _) -> pure (ACCL SCMContact $ CCLink cReq (Just l'), CPContactAddress CAPOwnLink)
Nothing -> getShortLinkConnReq user l' >>= (`contactReqAndPlan` Just l')
CCTGroup ->
withFastStore' (\db -> getGroupInfoViaUserShortLink db vr user l') >>= \case
Just (cReq, g) -> pure (ACCL SCMContact $ CCLink cReq (Just l'), CPGroupLink (GLPOwnLink g))
Nothing -> getShortLinkConnReq user l' >>= (`contactReqAndPlan` Just l')
CCTChannel -> throwChatError $ CECommandError "channel links are not supported in this version"
where
contactReqAndPlan cReq sLnk_ = do
plan <- contactRequestPlan user cReq `catchChatError` (pure . CPError)
pure (ACCL SCMContact $ CCLink cReq sLnk_, plan)
connectWithPlan :: User -> IncognitoEnabled -> ACreatedConnLink -> ConnectionPlan -> CM ChatResponse
connectWithPlan user@User {userId} incognito ccLink plan
| connectionPlanProceed plan = do
case plan of CPError e -> toView $ CRChatError (Just user) e; _ -> pure ()
case plan of
CPContactAddress (CAPContactViaAddress Contact {contactId}) ->
processChatCommand $ APIConnectContactViaAddress userId incognito contactId
_ -> processChatCommand $ APIConnect userId incognito (Just ccLink)
| otherwise = pure $ CRConnectionPlan user ccLink plan
inviationRequestPlan :: User -> ConnReqInvitation -> CM ConnectionPlan
inviationRequestPlan user cReq = do
withFastStore' (\db -> getConnectionEntityByConnReq db vr user $ cReqSchemas cReq) >>= \case
Nothing -> pure $ CPInvitationLink ILPOk Nothing -> pure $ CPInvitationLink ILPOk
Just (RcvDirectMsgConnection Connection {connStatus = ConnPrepared} Nothing) -> Just ent -> invitationEntityPlan ent
where
cReqSchemas :: ConnReqInvitation -> (ConnReqInvitation, ConnReqInvitation)
cReqSchemas (CRInvitationUri crData e2e) =
( CRInvitationUri crData {crScheme = SSSimplex} e2e,
CRInvitationUri crData {crScheme = simplexChat} e2e
)
invitationEntityPlan :: ConnectionEntity -> CM ConnectionPlan
invitationEntityPlan = \case
RcvDirectMsgConnection Connection {connStatus = ConnPrepared} Nothing ->
pure $ CPInvitationLink ILPOk pure $ CPInvitationLink ILPOk
Just (RcvDirectMsgConnection conn ct_) -> do RcvDirectMsgConnection conn ct_ -> do
let Connection {connStatus, contactConnInitiated} = conn let Connection {connStatus, contactConnInitiated} = conn
if if
| connStatus == ConnNew && contactConnInitiated -> | connStatus == ConnNew && contactConnInitiated ->
@ -3050,16 +3105,12 @@ processChatCommand' vr = \case
| otherwise -> case ct_ of | otherwise -> case ct_ of
Just ct -> pure $ CPInvitationLink (ILPKnown ct) Just ct -> pure $ CPInvitationLink (ILPKnown ct)
Nothing -> throwChatError $ CEInternalError "ready RcvDirectMsgConnection connection should have associated contact" Nothing -> throwChatError $ CEInternalError "ready RcvDirectMsgConnection connection should have associated contact"
Just _ -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection" _ -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection"
where contactRequestPlan :: User -> ConnReqContact -> CM ConnectionPlan
cReqSchemas :: (ConnReqInvitation, ConnReqInvitation) contactRequestPlan user (CRContactUri crData) = do
cReqSchemas =
( CRInvitationUri crData {crScheme = SSSimplex} e2e,
CRInvitationUri crData {crScheme = simplexChat} e2e
)
connectPlan user (ACR SCMContact (CRContactUri crData)) = do
let ConnReqUriData {crClientData} = crData let ConnReqUriData {crClientData} = crData
groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli groupLinkId = crClientData >>= decodeJSON >>= \(CRDataGroup gli) -> Just gli
cReqHashes = bimap hash hash cReqSchemas
case groupLinkId of case groupLinkId of
-- contact address -- contact address
Nothing -> Nothing ->
@ -3105,9 +3156,31 @@ processChatCommand' vr = \case
( CRContactUri crData {crScheme = SSSimplex}, ( CRContactUri crData {crScheme = SSSimplex},
CRContactUri crData {crScheme = simplexChat} CRContactUri crData {crScheme = simplexChat}
) )
cReqHashes :: (ConnReqUriHash, ConnReqUriHash) hash :: ConnReqContact -> ConnReqUriHash
cReqHashes = bimap hash hash cReqSchemas
hash = ConnReqUriHash . C.sha256Hash . strEncode hash = ConnReqUriHash . C.sha256Hash . strEncode
getShortLinkConnReq :: User -> ConnShortLink m -> CM (ConnectionRequestUri m)
getShortLinkConnReq User {userId} l = do
l' <- restoreShortLink' l
(cReq, cData) <- withAgent (\a -> getConnShortLink a userId l')
case cData of
ContactLinkData {direct} | not direct -> throwChatError CEUnsupportedConnReq
_ -> pure ()
pure cReq
-- This function is needed, as UI uses simplex:/ schema in message view, so that the links can be handled without browser,
-- and short links are stored with server hostname schema, so they wouldn't match without it.
serverShortLink :: ConnShortLink m -> ConnShortLink m
serverShortLink = \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)
shortLinkUserData short = if short then Just "" else Nothing
shortenCreatedLink :: CreatedConnLink m -> CM (CreatedConnLink m)
shortenCreatedLink (CCLink cReq sLnk) = CCLink cReq <$> mapM (\l -> (`shortenShortLink` l) <$> asks (shortLinkPresetServers . config)) sLnk
createdGroupLink :: CreatedLinkContact -> CreatedLinkContact
createdGroupLink (CCLink cReq shortLink) = CCLink cReq (toGroupLink <$> shortLink)
where
toGroupLink :: ShortLinkContact -> ShortLinkContact
toGroupLink (CSLContact sch _ srv k) = CSLContact sch CCTGroup srv k
updateCIGroupInvitationStatus :: User -> GroupInfo -> CIGroupInvitationStatus -> CM () updateCIGroupInvitationStatus :: User -> GroupInfo -> CIGroupInvitationStatus -> CM ()
updateCIGroupInvitationStatus user GroupInfo {groupId} newStatus = do updateCIGroupInvitationStatus user GroupInfo {groupId} newStatus = do
AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withFastStore $ \db -> getChatItemByGroupId db vr user groupId AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withFastStore $ \db -> getChatItemByGroupId db vr user groupId
@ -3574,7 +3647,7 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do
viaUserContactLink, viaUserContactLink,
groupLinkId, groupLinkId,
customUserProfileId, customUserProfileId,
connReqInv = Nothing, connLinkInv = Nothing,
localAlias, localAlias,
createdAt, createdAt,
updatedAt = createdAt updatedAt = createdAt
@ -4042,11 +4115,11 @@ chatCommandP =
"/set welcome " *> char_ '#' *> (UpdateGroupDescription <$> displayNameP <* A.space <*> (Just <$> msgTextP)), "/set welcome " *> char_ '#' *> (UpdateGroupDescription <$> displayNameP <* A.space <*> (Just <$> msgTextP)),
"/delete welcome " *> char_ '#' *> (UpdateGroupDescription <$> displayNameP <*> pure Nothing), "/delete welcome " *> char_ '#' *> (UpdateGroupDescription <$> displayNameP <*> pure Nothing),
"/show welcome " *> char_ '#' *> (ShowGroupDescription <$> displayNameP), "/show welcome " *> char_ '#' *> (ShowGroupDescription <$> displayNameP),
"/_create link #" *> (APICreateGroupLink <$> A.decimal <*> (memberRole <|> pure GRMember)), "/_create link #" *> (APICreateGroupLink <$> A.decimal <*> (memberRole <|> pure GRMember) <*> shortOnOffP),
"/_set link role #" *> (APIGroupLinkMemberRole <$> A.decimal <*> memberRole), "/_set link role #" *> (APIGroupLinkMemberRole <$> A.decimal <*> memberRole),
"/_delete link #" *> (APIDeleteGroupLink <$> A.decimal), "/_delete link #" *> (APIDeleteGroupLink <$> A.decimal),
"/_get link #" *> (APIGetGroupLink <$> A.decimal), "/_get link #" *> (APIGetGroupLink <$> A.decimal),
"/create link #" *> (CreateGroupLink <$> displayNameP <*> (memberRole <|> pure GRMember)), "/create link #" *> (CreateGroupLink <$> displayNameP <*> (memberRole <|> pure GRMember) <*> shortP),
"/set link role #" *> (GroupLinkMemberRole <$> displayNameP <*> memberRole), "/set link role #" *> (GroupLinkMemberRole <$> displayNameP <*> memberRole),
"/delete link #" *> (DeleteGroupLink <$> displayNameP), "/delete link #" *> (DeleteGroupLink <$> displayNameP),
"/show link #" *> (ShowGroupLink <$> displayNameP), "/show link #" *> (ShowGroupLink <$> displayNameP),
@ -4057,12 +4130,12 @@ chatCommandP =
"/_contacts " *> (APIListContacts <$> A.decimal), "/_contacts " *> (APIListContacts <$> A.decimal),
"/contacts" $> ListContacts, "/contacts" $> ListContacts,
"/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> strP), "/_connect plan " *> (APIConnectPlan <$> A.decimal <* A.space <*> strP),
"/_connect " *> (APIConnect <$> A.decimal <*> incognitoOnOffP <* A.space <*> ((Just <$> strP) <|> A.takeByteString $> Nothing)), "/_connect " *> (APIAddContact <$> A.decimal <*> shortOnOffP <*> incognitoOnOffP),
"/_connect " *> (APIAddContact <$> A.decimal <*> incognitoOnOffP), "/_connect " *> (APIConnect <$> A.decimal <*> incognitoOnOffP <* A.space <*> connLinkP),
"/_set incognito :" *> (APISetConnectionIncognito <$> A.decimal <* A.space <*> onOffP), "/_set incognito :" *> (APISetConnectionIncognito <$> A.decimal <* A.space <*> onOffP),
"/_set conn user :" *> (APIChangeConnectionUser <$> A.decimal <* A.space <*> A.decimal), "/_set conn user :" *> (APIChangeConnectionUser <$> A.decimal <* A.space <*> A.decimal),
("/connect" <|> "/c") *> (AddContact <$> shortP <*> incognitoP),
("/connect" <|> "/c") *> (Connect <$> incognitoP <* A.space <*> ((Just <$> strP) <|> A.takeTill isSpace $> Nothing)), ("/connect" <|> "/c") *> (Connect <$> incognitoP <* A.space <*> ((Just <$> strP) <|> A.takeTill isSpace $> Nothing)),
("/connect" <|> "/c") *> (AddContact <$> incognitoP),
ForwardMessage <$> chatNameP <* " <- @" <*> displayNameP <* A.space <*> msgTextP, ForwardMessage <$> chatNameP <* " <- @" <*> displayNameP <* A.space <*> msgTextP,
ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <* A.space <* A.char '@' <*> (Just <$> displayNameP) <* A.space <*> msgTextP, ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <* A.space <* A.char '@' <*> (Just <$> displayNameP) <* A.space <*> msgTextP,
ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <*> pure Nothing <* A.space <*> msgTextP, ForwardGroupMessage <$> chatNameP <* " <- #" <*> displayNameP <*> pure Nothing <* A.space <*> msgTextP,
@ -4096,8 +4169,8 @@ chatCommandP =
("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal), ("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal),
"/_connect contact " *> (APIConnectContactViaAddress <$> A.decimal <*> incognitoOnOffP <* A.space <*> A.decimal), "/_connect contact " *> (APIConnectContactViaAddress <$> A.decimal <*> incognitoOnOffP <* A.space <*> A.decimal),
"/simplex" *> (ConnectSimplex <$> incognitoP), "/simplex" *> (ConnectSimplex <$> incognitoP),
"/_address " *> (APICreateMyAddress <$> A.decimal), "/_address " *> (APICreateMyAddress <$> A.decimal <*> shortOnOffP),
("/address" <|> "/ad") $> CreateMyAddress, ("/address" <|> "/ad") *> (CreateMyAddress <$> shortP),
"/_delete_address " *> (APIDeleteMyAddress <$> A.decimal), "/_delete_address " *> (APIDeleteMyAddress <$> A.decimal),
("/delete_address" <|> "/da") $> DeleteMyAddress, ("/delete_address" <|> "/da") $> DeleteMyAddress,
"/_show_address " *> (APIShowMyAddress <$> A.decimal), "/_show_address " *> (APIShowMyAddress <$> A.decimal),
@ -4167,7 +4240,12 @@ chatCommandP =
] ]
where where
choice = A.choice . map (\p -> p <* A.takeWhile (== ' ') <* A.endOfInput) choice = A.choice . map (\p -> p <* A.takeWhile (== ' ') <* A.endOfInput)
connLinkP = do
((Just <$> strP) <|> A.takeTill (== ' ') $> Nothing)
>>= mapM (\(ACR m cReq) -> ACCL m . CCLink cReq <$> optional (A.space *> strP))
shortP = (A.space *> ("short" <|> "s")) $> True <|> pure False
incognitoP = (A.space *> ("incognito" <|> "i")) $> True <|> pure False incognitoP = (A.space *> ("incognito" <|> "i")) $> True <|> pure False
shortOnOffP = (A.space *> "short=" *> onOffP) <|> pure False
incognitoOnOffP = (A.space *> "incognito=" *> onOffP) <|> pure False incognitoOnOffP = (A.space *> "incognito=" *> onOffP) <|> pure False
imagePrefix = (<>) <$> "data:" <*> ("image/png;base64," <|> "image/jpg;base64,") imagePrefix = (<>) <$> "data:" <*> ("image/png;base64," <|> "image/jpg;base64,")
imageP = safeDecodeUtf8 <$> ((<>) <$> imagePrefix <*> (B64.encode <$> base64P)) imageP = safeDecodeUtf8 <$> ((<>) <$> imagePrefix <*> (B64.encode <$> base64P))

File diff suppressed because one or more lines are too long

View file

@ -1186,8 +1186,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
CORGroup gInfo -> toView $ CRBusinessRequestAlreadyAccepted user gInfo CORGroup gInfo -> toView $ CRBusinessRequestAlreadyAccepted user gInfo
CORRequest cReq -> do CORRequest cReq -> do
ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId
let (UserContactLink {connReqContact, autoAccept}, gLinkInfo_) = ucl let (UserContactLink {connLinkContact = CCLink connReq _, autoAccept}, gLinkInfo_) = ucl
isSimplexTeam = sameConnReqContact connReqContact adminContactReq isSimplexTeam = sameConnReqContact connReq adminContactReq
v = maxVersion chatVRange v = maxVersion chatVRange
case autoAccept of case autoAccept of
Just AutoAccept {acceptIncognito, businessAddress} Just AutoAccept {acceptIncognito, businessAddress}

View file

@ -29,11 +29,10 @@ import Data.Text (Text)
import qualified Data.Text as T import qualified Data.Text as T
import Data.Text.Encoding (encodeUtf8) import Data.Text.Encoding (encodeUtf8)
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri (..), ConnReqUriData (..), ConnectionRequestUri (..), SMPQueue (..)) import Simplex.Messaging.Agent.Protocol (AConnectionLink (..), ConnReqUriData (..), ConnShortLink (..), ConnectionLink (..), ConnectionRequestUri (..), ContactConnType (..), SMPQueue (..), simplexConnReqUri, simplexShortLink)
import Simplex.Messaging.Encoding.String import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fstToLower, sumTypeJSON) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fstToLower, sumTypeJSON)
import Simplex.Messaging.Protocol (ProtocolServer (..)) import Simplex.Messaging.Protocol (ProtocolServer (..))
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
import Simplex.Messaging.Util (decodeJSON, safeDecodeUtf8) import Simplex.Messaging.Util (decodeJSON, safeDecodeUtf8)
import System.Console.ANSI.Types import System.Console.ANSI.Types
import qualified Text.Email.Validate as Email import qualified Text.Email.Validate as Email
@ -49,7 +48,7 @@ data Format
| Secret | Secret
| Colored {color :: FormatColor} | Colored {color :: FormatColor}
| Uri | Uri
| SimplexLink {linkType :: SimplexLinkType, simplexUri :: AConnectionRequestUri, smpHosts :: NonEmpty Text} | SimplexLink {linkType :: SimplexLinkType, simplexUri :: AConnectionLink, smpHosts :: NonEmpty Text}
| Mention {memberName :: Text} | Mention {memberName :: Text}
| Email | Email
| Phone | Phone
@ -62,7 +61,7 @@ mentionedNames = mapMaybe (\(FormattedText f _) -> mentionedName =<< f)
Mention name -> Just name Mention name -> Just name
_ -> Nothing _ -> Nothing
data SimplexLinkType = XLContact | XLInvitation | XLGroup data SimplexLinkType = XLContact | XLInvitation | XLGroup | XLChannel
deriving (Eq, Show) deriving (Eq, Show)
colored :: Color -> Format colored :: Color -> Format
@ -248,24 +247,34 @@ markdownP = mconcat <$> A.many' fragmentP
')' -> False ')' -> False
c -> isPunctuation c c -> isPunctuation c
uriMarkdown s = case strDecode $ encodeUtf8 s of uriMarkdown s = case strDecode $ encodeUtf8 s of
Right cReq -> markdown (simplexUriFormat cReq) s Right cLink -> markdown (simplexUriFormat cLink) s
_ -> markdown Uri s _ -> markdown Uri s
isUri s = T.length s >= 10 && any (`T.isPrefixOf` s) ["http://", "https://", "simplex:/"] isUri s = T.length s >= 10 && any (`T.isPrefixOf` s) ["http://", "https://", "simplex:/"]
isEmail s = T.any (== '@') s && Email.isValid (encodeUtf8 s) isEmail s = T.any (== '@') s && Email.isValid (encodeUtf8 s)
noFormat = pure . unmarked noFormat = pure . unmarked
simplexUriFormat :: AConnectionRequestUri -> Format simplexUriFormat :: AConnectionLink -> Format
simplexUriFormat = \case simplexUriFormat = \case
ACR m (CRContactUri crData) -> ACL m (CLFull cReq) -> case cReq of
let cReq = ACR m $ CRContactUri crData {crScheme = SSSimplex} CRContactUri crData -> SimplexLink (linkType' crData) cLink $ uriHosts crData
in SimplexLink (linkType' crData) cReq $ uriHosts crData CRInvitationUri crData _ -> SimplexLink XLInvitation cLink $ uriHosts crData
ACR m (CRInvitationUri crData e2e) ->
let cReq = ACR m $ CRInvitationUri crData {crScheme = SSSimplex} e2e
in SimplexLink XLInvitation cReq $ uriHosts crData
where where
uriHosts ConnReqUriData {crSmpQueues} = L.map (safeDecodeUtf8 . strEncode) $ sconcat $ L.map (host . qServer) crSmpQueues cLink = ACL m $ CLFull $ simplexConnReqUri cReq
uriHosts ConnReqUriData {crSmpQueues} = L.map strEncodeText $ sconcat $ L.map (host . qServer) crSmpQueues
linkType' ConnReqUriData {crClientData} = case crClientData >>= decodeJSON of linkType' ConnReqUriData {crClientData} = case crClientData >>= decodeJSON of
Just (CRDataGroup _) -> XLGroup Just (CRDataGroup _) -> XLGroup
Nothing -> XLContact Nothing -> XLContact
ACL m (CLShort sLnk) -> case sLnk of
CSLContact _ ct srv _ -> SimplexLink (linkType' ct) cLink $ uriHosts srv
CSLInvitation _ srv _ _ -> SimplexLink XLInvitation cLink $ uriHosts srv
where
cLink = ACL m $ CLShort $ simplexShortLink sLnk
uriHosts srv = L.map strEncodeText $ host srv
linkType' = \case
CCTGroup -> XLGroup
CCTChannel -> XLChannel
CCTContact -> XLContact
strEncodeText :: StrEncoding a => a -> Text
strEncodeText = safeDecodeUtf8 . strEncode
markdownText :: FormattedText -> Text markdownText :: FormattedText -> Text
markdownText (FormattedText f_ t) = case f_ of markdownText (FormattedText f_ t) = case f_ of

View file

@ -275,6 +275,10 @@ data UserServer' s (p :: ProtocolType) = UserServer
} }
deriving (Show) deriving (Show)
presetServerAddress :: UserServer' s p -> ProtocolServer p
presetServerAddress UserServer {server = ProtoServerWithAuth srv _} = srv
{-# INLINE presetServerAddress #-}
data PresetOperator = PresetOperator data PresetOperator = PresetOperator
{ operator :: Maybe NewServerOperator, { operator :: Maybe NewServerOperator,
smp :: [NewUserServer 'PSMP], smp :: [NewUserServer 'PSMP],
@ -297,6 +301,9 @@ operatorServersToUse p PresetOperator {useSMP, useXFTP} = case p of
SPSMP -> useSMP SPSMP -> useSMP
SPXFTP -> useXFTP SPXFTP -> useXFTP
presetServer' :: Bool -> ProtocolServer p -> NewUserServer p
presetServer' enabled = presetServer enabled . (`ProtoServerWithAuth` Nothing)
presetServer :: Bool -> ProtoServerWithAuth p -> NewUserServer p presetServer :: Bool -> ProtoServerWithAuth p -> NewUserServer p
presetServer = newUserServer_ True presetServer = newUserServer_ True

View file

@ -0,0 +1,117 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
module Simplex.Chat.Operators.Presets where
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as L
import Simplex.Chat.Operators
import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..), allRoles)
import Simplex.Messaging.Protocol (ProtocolType (..), SMPServer)
operatorSimpleXChat :: NewServerOperator
operatorSimpleXChat =
ServerOperator
{ operatorId = DBNewEntity,
operatorTag = Just OTSimplex,
tradeName = "SimpleX Chat",
legalName = Just "SimpleX Chat Ltd",
serverDomains = ["simplex.im"],
conditionsAcceptance = CARequired Nothing,
enabled = True,
smpRoles = allRoles,
xftpRoles = allRoles
}
operatorFlux :: NewServerOperator
operatorFlux =
ServerOperator
{ operatorId = DBNewEntity,
operatorTag = Just OTFlux,
tradeName = "Flux",
legalName = Just "InFlux Technologies Limited",
serverDomains = ["simplexonflux.com"],
conditionsAcceptance = CARequired Nothing,
enabled = False,
smpRoles = ServerRoles {storage = False, proxy = True},
xftpRoles = ServerRoles {storage = False, proxy = True}
}
-- Please note: if any servers are removed from the lists below, they MUST be added here.
-- Otherwise previously created short links won't work.
--
-- !!! Also, if any servers need to be added, shortLinkPresetServers will need to be be split to two,
-- so that option used for restoring links is updated earlier, for backward/forward compatibility.
allPresetServers :: NonEmpty SMPServer
allPresetServers = enabledSimplexChatSMPServers <> disabledSimplexChatSMPServers <> fluxSMPServers_
-- TODO [short links] remove, added for testing
<> ["smp://8Af90NX2TTkKEJAF1RCg69P_Odg2Z-6_J6DOKUqK3rQ=@smp7.simplex.im,dbxqutskmmbkbrs7ofi7pmopeyhgi5cxbjbh4ummgmep4r6bz4cbrcid.onion"]
simplexChatSMPServers :: [NewUserServer 'PSMP]
simplexChatSMPServers =
map (presetServer' True) (L.toList enabledSimplexChatSMPServers)
<> map (presetServer' False) (L.toList disabledSimplexChatSMPServers)
-- Please note: if any servers are removed from this list, they MUST be added to allPresetServers.
-- Otherwise previously created short links won't work.
--
-- !!! Also, if any servers need to be added, shortLinkPresetServers will need to be be split to two,
-- so that option used for restoring links is updated earlier, for backward/forward compatibility.
enabledSimplexChatSMPServers :: NonEmpty SMPServer
enabledSimplexChatSMPServers =
[ "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion",
"smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im,jssqzccmrcws6bhmn77vgmhfjmhwlyr3u7puw4erkyoosywgl67slqqd.onion",
"smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im,rb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion",
"smp://1OwYGt-yqOfe2IyVHhxz3ohqo3aCCMjtB-8wn4X_aoY=@smp11.simplex.im,6ioorbm6i3yxmuoezrhjk6f6qgkc4syabh7m3so74xunb5nzr4pwgfqd.onion",
"smp://UkMFNAXLXeAAe0beCa4w6X_zp18PwxSaSjY17BKUGXQ=@smp12.simplex.im,ie42b5weq7zdkghocs3mgxdjeuycheeqqmksntj57rmejagmg4eor5yd.onion",
"smp://enEkec4hlR3UtKx2NMpOUK_K4ZuDxjWBO1d9Y4YXVaA=@smp14.simplex.im,aspkyu2sopsnizbyfabtsicikr2s4r3ti35jogbcekhm3fsoeyjvgrid.onion",
"smp://h--vW7ZSkXPeOUpfxlFGgauQmXNFOzGoizak7Ult7cw=@smp15.simplex.im,oauu4bgijybyhczbnxtlggo6hiubahmeutaqineuyy23aojpih3dajad.onion",
"smp://hejn2gVIqNU6xjtGM3OwQeuk8ZEbDXVJXAlnSBJBWUA=@smp16.simplex.im,p3ktngodzi6qrf7w64mmde3syuzrv57y55hxabqcq3l5p6oi7yzze6qd.onion",
"smp://ZKe4uxF4Z_aLJJOEsC-Y6hSkXgQS5-oc442JQGkyP8M=@smp17.simplex.im,ogtwfxyi3h2h5weftjjpjmxclhb5ugufa5rcyrmg7j4xlch7qsr5nuqd.onion",
"smp://PtsqghzQKU83kYTlQ1VKg996dW4Cw4x_bvpKmiv8uns=@smp18.simplex.im,lyqpnwbs2zqfr45jqkncwpywpbtq7jrhxnib5qddtr6npjyezuwd3nqd.onion",
"smp://N_McQS3F9TGoh4ER0QstUf55kGnNSd-wXfNPZ7HukcM=@smp19.simplex.im,i53bbtoqhlc365k6kxzwdp5w3cdt433s7bwh3y32rcbml2vztiyyz5id.onion"
]
-- Please note: if any servers are removed from this list, they MUST be added to allPresetServers.
-- Otherwise previously created short links won't work.
--
-- !!! Also, if any servers need to be added, shortLinkPresetServers will need to be be split to two,
-- so that option used for restoring links is updated earlier, for backward/forward compatibility.
disabledSimplexChatSMPServers :: NonEmpty SMPServer
disabledSimplexChatSMPServers =
[ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion",
"smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion",
"smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion"
]
fluxSMPServers :: [NewUserServer 'PSMP]
fluxSMPServers = map (presetServer' True) $ L.toList fluxSMPServers_
-- Please note: if any servers are removed from this list, they MUST be added to allPresetServers.
-- Otherwise previously created short links won't work.
--
-- !!! Also, if any servers need to be added, shortLinkPresetServers will need to be be split to two,
-- so that option used for restoring links is updated earlier, for backward/forward compatibility.
fluxSMPServers_ :: NonEmpty SMPServer
fluxSMPServers_ =
[ "smp://xQW_ufMkGE20UrTlBl8QqceG1tbuylXhr9VOLPyRJmw=@smp1.simplexonflux.com,qb4yoanyl4p7o33yrknv4rs6qo7ugeb2tu2zo66sbebezs4cpyosarid.onion",
"smp://LDnWZVlAUInmjmdpQQoIo6FUinRXGe0q3zi5okXDE4s=@smp2.simplexonflux.com,yiqtuh3q4x7hgovkomafsod52wvfjucdljqbbipg5sdssnklgongxbqd.onion",
"smp://1jne379u7IDJSxAvXbWb_JgoE7iabcslX0LBF22Rej0=@smp3.simplexonflux.com,a5lm4k7ufei66cdck6fy63r4lmkqy3dekmmb7jkfdm5ivi6kfaojshad.onion",
"smp://xmAmqj75I9mWrUihLUlI0ZuNLXlIwFIlHRq5Pb6cHAU=@smp4.simplexonflux.com,qpcz2axyy66u26hfdd2e23uohcf3y6c36mn7dcuilcgnwjasnrvnxjqd.onion",
"smp://rWvBYyTamuRCBYb_KAn-nsejg879ndhiTg5Sq3k0xWA=@smp5.simplexonflux.com,4ao347qwiuluyd45xunmii4skjigzuuox53hpdsgbwxqafd4yrticead.onion",
"smp://PN7-uqLBToqlf1NxHEaiL35lV2vBpXq8Nj8BW11bU48=@smp6.simplexonflux.com,hury6ot3ymebbr2535mlp7gcxzrjpc6oujhtfxcfh2m4fal4xw5fq6qd.onion"
]
fluxXFTPServers :: [NewUserServer 'PXFTP]
fluxXFTPServers =
map
(presetServer True)
[ "xftp://92Sctlc09vHl_nAqF2min88zKyjdYJ9mgxRCJns5K2U=@xftp1.simplexonflux.com,apl3pumq3emwqtrztykyyoomdx4dg6ysql5zek2bi3rgznz7ai3odkid.onion",
"xftp://YBXy4f5zU1CEhnbbCzVWTNVNsaETcAGmYqGNxHntiE8=@xftp2.simplexonflux.com,c5jjecisncnngysah3cz2mppediutfelco4asx65mi75d44njvua3xid.onion",
"xftp://ARQO74ZSvv2OrulRF3CdgwPz_AMy27r0phtLSq5b664=@xftp3.simplexonflux.com,dc4mohiubvbnsdfqqn7xhlhpqs5u4tjzp7xpz6v6corwvzvqjtaqqiqd.onion",
"xftp://ub2jmAa9U0uQCy90O-fSUNaYCj6sdhl49Jh3VpNXP58=@xftp4.simplexonflux.com,4qq5pzier3i4yhpuhcrhfbl6j25udc4czoyascrj4yswhodhfwev3nyd.onion",
"xftp://Rh19D5e4Eez37DEE9hAlXDB3gZa1BdFYJTPgJWPO9OI=@xftp5.simplexonflux.com,q7itltdn32hjmgcqwhow4tay5ijetng3ur32bolssw32fvc5jrwvozad.onion",
"xftp://0AznwoyfX8Od9T_acp1QeeKtxUi676IBIiQjXVwbdyU=@xftp6.simplexonflux.com,upvzf23ou6nrmaf3qgnhd6cn3d74tvivlmz3p7wdfwq6fhthjrjiiqid.onion"
]

View file

@ -1,4 +1,5 @@
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
@ -12,6 +13,7 @@ module Simplex.Chat.Store.Connections
( getChatLockEntity, ( getChatLockEntity,
getConnectionEntity, getConnectionEntity,
getConnectionEntityByConnReq, getConnectionEntityByConnReq,
getConnectionEntityViaShortLink,
getContactConnEntityByConnReqHash, getContactConnEntityByConnReqHash,
getConnectionsToSubscribe, getConnectionsToSubscribe,
unsetConnectionToSubscribe, unsetConnectionToSubscribe,
@ -33,7 +35,7 @@ import Simplex.Chat.Store.Groups
import Simplex.Chat.Store.Profiles import Simplex.Chat.Store.Profiles
import Simplex.Chat.Store.Shared import Simplex.Chat.Store.Shared
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Messaging.Agent.Protocol (ConnId) import Simplex.Messaging.Agent.Protocol (ConnId, ConnShortLink, ConnectionMode (..))
import Simplex.Messaging.Agent.Store.AgentStore (firstRow, firstRow', maybeFirstRow) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, firstRow', maybeFirstRow)
import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) import Simplex.Messaging.Agent.Store.DB (BoolInt (..))
import qualified Simplex.Messaging.Agent.Store.DB as DB import qualified Simplex.Messaging.Agent.Store.DB as DB
@ -204,6 +206,26 @@ getConnectionEntityByConnReq db vr user@User {userId} (cReqSchema1, cReqSchema2)
DB.query db "SELECT agent_conn_id FROM connections WHERE user_id = ? AND conn_req_inv IN (?,?) LIMIT 1" (userId, cReqSchema1, cReqSchema2) DB.query db "SELECT agent_conn_id FROM connections WHERE user_id = ? AND conn_req_inv IN (?,?) LIMIT 1" (userId, cReqSchema1, cReqSchema2)
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db vr user) connId_ maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db vr user) connId_
getConnectionEntityViaShortLink :: DB.Connection -> VersionRangeChat -> User -> ConnShortLink 'CMInvitation -> IO (Maybe (ConnReqInvitation, ConnectionEntity))
getConnectionEntityViaShortLink db vr user@User {userId} shortLink = fmap eitherToMaybe $ runExceptT $ do
(cReq, connId) <- ExceptT getConnReqConnId
(cReq,) <$> getConnectionEntity db vr user connId
where
getConnReqConnId =
firstRow' toConnReqConnId (SEInternalError "connection not found") $
DB.query
db
[sql|
SELECT conn_req_inv, agent_conn_id
FROM connections
WHERE user_id = ? AND short_link_inv = ? LIMIT 1
|]
(userId, shortLink)
-- cReq is Maybe - it is removed when connection is established
toConnReqConnId = \case
(Just cReq, connId) -> Right (cReq, connId)
_ -> Left $ SEInternalError "no connection request"
-- search connection for connection plan: -- search connection for connection plan:
-- multiple connections can have same via_contact_uri_hash if request was repeated; -- multiple connections can have same via_contact_uri_hash if request was repeated;
-- this function searches for latest connection with contact so that "known contact" plan would be chosen; -- this function searches for latest connection with contact so that "known contact" plan would be chosen;

View file

@ -100,7 +100,7 @@ import Simplex.Chat.Store.Shared
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.UITheme import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Agent.Protocol (ConnId, InvitationId, UserId) import Simplex.Messaging.Agent.Protocol (ConnId, CreatedConnLink (..), InvitationId, UserId)
import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow)
import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..)) import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..))
import qualified Simplex.Messaging.Agent.Store.DB as DB import qualified Simplex.Messaging.Agent.Store.DB as DB
@ -122,7 +122,7 @@ getPendingContactConnection db userId connId = do
DB.query DB.query
db db
[sql| [sql|
SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at
FROM connections FROM connections
WHERE user_id = ? WHERE user_id = ?
AND connection_id = ? AND connection_id = ?
@ -148,14 +148,14 @@ deletePendingContactConnection db userId connId =
|] |]
(userId, connId, ConnContact) (userId, connId, ConnContact)
createAddressContactConnection :: DB.Connection -> VersionRangeChat -> User -> Contact -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> ExceptT StoreError IO (Int64, Contact) 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 xContactId incognitoProfile subMode chatV pqSup = do 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 xContactId incognitoProfile Nothing subMode chatV pqSup 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) liftIO $ DB.execute db "UPDATE connections SET contact_id = ? WHERE connection_id = ?" (contactId, pccConnId)
(pccConnId,) <$> getContact db vr user contactId (pccConnId,) <$> getContact db vr user contactId
createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> Maybe ShortLinkContact -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection
createConnReqConnection db userId acId cReqHash xContactId incognitoProfile groupLinkId subMode chatV pqSup = do createConnReqConnection db userId acId cReqHash sLnk xContactId incognitoProfile groupLinkId subMode chatV pqSup = do
createdAt <- getCurrentTime createdAt <- getCurrentTime
customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile
let pccConnStatus = ConnJoined let pccConnStatus = ConnJoined
@ -164,16 +164,16 @@ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile grou
[sql| [sql|
INSERT INTO connections ( INSERT INTO connections (
user_id, agent_conn_id, conn_status, conn_type, contact_conn_initiated, user_id, agent_conn_id, conn_status, conn_type, contact_conn_initiated,
via_contact_uri_hash, xcontact_id, custom_user_profile_id, via_group_link, group_link_id, via_contact_uri_hash, via_short_link_contact, 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 created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|] |]
( (userId, acId, pccConnStatus, ConnContact, BI True, cReqHash, xContactId) ( (userId, acId, pccConnStatus, ConnContact, BI True, cReqHash, sLnk, xContactId)
:. (customUserProfileId, BI (isJust groupLinkId), groupLinkId) :. (customUserProfileId, BI (isJust groupLinkId), groupLinkId)
:. (createdAt, createdAt, BI (subMode == SMOnlyCreate), chatV, pqSup, pqSup) :. (createdAt, createdAt, BI (subMode == SMOnlyCreate), chatV, pqSup, pqSup)
) )
pccConnId <- insertedRowId db pccConnId <- insertedRowId db
pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, groupLinkId, customUserProfileId, connReqInv = Nothing, localAlias = "", createdAt, updatedAt = createdAt} pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = True, viaUserContactLink = Nothing, groupLinkId, customUserProfileId, connLinkInv = Nothing, localAlias = "", createdAt, updatedAt = createdAt}
getConnReqContactXContactId :: DB.Connection -> VersionRangeChat -> User -> ConnReqUriHash -> IO (Maybe Contact, Maybe XContactId) getConnReqContactXContactId :: DB.Connection -> VersionRangeChat -> User -> ConnReqUriHash -> IO (Maybe Contact, Maybe XContactId)
getConnReqContactXContactId db vr user@User {userId} cReqHash = do getConnReqContactXContactId db vr user@User {userId} cReqHash = do
@ -214,8 +214,8 @@ getContactByConnReqHash db vr user@User {userId} cReqHash = do
(userId, cReqHash, CSActive) (userId, cReqHash, CSActive)
mapM (addDirectChatTags db) ct_ mapM (addDirectChatTags db) ct_
createDirectConnection :: DB.Connection -> User -> ConnId -> ConnReqInvitation -> ConnStatus -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection createDirectConnection :: DB.Connection -> User -> ConnId -> CreatedLinkInvitation -> ConnStatus -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection
createDirectConnection db User {userId} acId cReq pccConnStatus incognitoProfile subMode chatV pqSup = do createDirectConnection db User {userId} acId ccLink@(CCLink cReq shortLinkInv) pccConnStatus incognitoProfile subMode chatV pqSup = do
createdAt <- getCurrentTime createdAt <- getCurrentTime
customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile customUserProfileId <- mapM (createIncognitoProfile_ db userId createdAt) incognitoProfile
let contactConnInitiated = pccConnStatus == ConnNew let contactConnInitiated = pccConnStatus == ConnNew
@ -223,15 +223,15 @@ createDirectConnection db User {userId} acId cReq pccConnStatus incognitoProfile
db db
[sql| [sql|
INSERT INTO connections INSERT INTO connections
(user_id, agent_conn_id, conn_req_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_conn_initiated, custom_user_profile_id,
created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption) created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|] |]
( (userId, acId, cReq, pccConnStatus, ConnContact, BI contactConnInitiated, customUserProfileId) ( (userId, acId, cReq, shortLinkInv, pccConnStatus, ConnContact, BI contactConnInitiated, customUserProfileId)
:. (createdAt, createdAt, BI (subMode == SMOnlyCreate), chatV, pqSup, pqSup) :. (createdAt, createdAt, BI (subMode == SMOnlyCreate), chatV, pqSup, pqSup)
) )
pccConnId <- insertedRowId db pccConnId <- insertedRowId db
pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, viaUserContactLink = Nothing, groupLinkId = Nothing, customUserProfileId, connReqInv = Just cReq, localAlias = "", createdAt, updatedAt = createdAt} pure PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = False, viaUserContactLink = Nothing, groupLinkId = Nothing, customUserProfileId, connLinkInv = Just ccLink, localAlias = "", createdAt, updatedAt = createdAt}
createIncognitoProfile :: DB.Connection -> User -> Profile -> IO Int64 createIncognitoProfile :: DB.Connection -> User -> Profile -> IO Int64
createIncognitoProfile db User {userId} p = do createIncognitoProfile db User {userId} p = do
@ -904,7 +904,7 @@ getPendingContactConnections db User {userId} = do
<$> DB.query <$> DB.query
db db
[sql| [sql|
SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at
FROM connections FROM connections
WHERE user_id = ? WHERE user_id = ?
AND conn_type = ? AND conn_type = ?
@ -989,7 +989,7 @@ updateConnectionStatus_ :: DB.Connection -> Int64 -> ConnStatus -> IO ()
updateConnectionStatus_ db connId connStatus = do updateConnectionStatus_ db connId connStatus = do
currentTs <- getCurrentTime currentTs <- getCurrentTime
if connStatus == ConnReady if connStatus == ConnReady
then DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ?, conn_req_inv = NULL WHERE connection_id = ?" (connStatus, currentTs, connId) then DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ?, conn_req_inv = NULL, short_link_inv = NULL WHERE connection_id = ?" (connStatus, currentTs, connId)
else DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ? WHERE connection_id = ?" (connStatus, currentTs, connId) else DB.execute db "UPDATE connections SET conn_status = ?, updated_at = ? WHERE connection_id = ?" (connStatus, currentTs, connId)
updateContactSettings :: DB.Connection -> User -> Int64 -> ChatSettings -> IO () updateContactSettings :: DB.Connection -> User -> Int64 -> ChatSettings -> IO ()

View file

@ -39,6 +39,7 @@ module Simplex.Chat.Store.Groups
getGroup, getGroup,
getGroupInfo, getGroupInfo,
getGroupInfoByUserContactLinkConnReq, getGroupInfoByUserContactLinkConnReq,
getGroupInfoViaUserShortLink,
getGroupInfoByGroupLinkHash, getGroupInfoByGroupLinkHash,
updateGroupProfile, updateGroupProfile,
updateGroupPreferences, updateGroupPreferences,
@ -157,14 +158,14 @@ import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.UITheme import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Agent.Protocol (ConnId, UserId) import Simplex.Messaging.Agent.Protocol (ConnId, CreatedConnLink (..), UserId)
import Simplex.Messaging.Agent.Store.AgentStore (firstRow, fromOnlyBI, maybeFirstRow) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, fromOnlyBI, maybeFirstRow)
import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..)) import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..))
import qualified Simplex.Messaging.Agent.Store.DB as DB import qualified Simplex.Messaging.Agent.Store.DB as DB
import qualified Simplex.Messaging.Crypto as C import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.Ratchet (pattern PQEncOff, pattern PQSupportOff) import Simplex.Messaging.Crypto.Ratchet (pattern PQEncOff, pattern PQSupportOff)
import Simplex.Messaging.Protocol (SubscriptionMode (..)) import Simplex.Messaging.Protocol (SubscriptionMode (..))
import Simplex.Messaging.Util (eitherToMaybe, ($>>=), (<$$>)) import Simplex.Messaging.Util (eitherToMaybe, firstRow', ($>>=), (<$$>))
import Simplex.Messaging.Version import Simplex.Messaging.Version
import UnliftIO.STM import UnliftIO.STM
#if defined(dbPostgres) #if defined(dbPostgres)
@ -175,21 +176,21 @@ import Database.SQLite.Simple (Only (..), Query, (:.) (..))
import Database.SQLite.Simple.QQ (sql) import Database.SQLite.Simple.QQ (sql)
#endif #endif
type MaybeGroupMemberRow = (Maybe Int64, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnReqContact, Maybe LocalAlias, Maybe Preferences) :. (Maybe UTCTime, Maybe UTCTime) type MaybeGroupMemberRow = (Maybe Int64, Maybe Int64, Maybe MemberId, Maybe VersionChat, Maybe VersionChat, Maybe GroupMemberRole, Maybe GroupMemberCategory, Maybe GroupMemberStatus, Maybe BoolInt, Maybe MemberRestrictionStatus) :. (Maybe Int64, Maybe GroupMemberId, Maybe ContactName, Maybe ContactId, Maybe ProfileId, Maybe ProfileId, Maybe ContactName, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe LocalAlias, Maybe Preferences) :. (Maybe UTCTime, Maybe UTCTime)
toMaybeGroupMember :: Int64 -> MaybeGroupMemberRow -> Maybe GroupMember toMaybeGroupMember :: Int64 -> MaybeGroupMemberRow -> Maybe GroupMember
toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked) :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId, Just profileId, Just displayName, Just fullName, image, contactLink, Just localAlias, contactPreferences) :. (Just createdAt, Just updatedAt)) = toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just memberId, Just minVer, Just maxVer, Just memberRole, Just memberCategory, Just memberStatus, Just showMessages, memberBlocked) :. (invitedById, invitedByGroupMemberId, Just localDisplayName, memberContactId, Just memberContactProfileId, Just profileId, Just displayName, Just fullName, image, contactLink, Just localAlias, contactPreferences) :. (Just createdAt, Just updatedAt)) =
Just $ toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, contactPreferences) :. (createdAt, updatedAt)) Just $ toGroupMember userContactId ((groupMemberId, groupId, memberId, minVer, maxVer, memberRole, memberCategory, memberStatus, showMessages, memberBlocked) :. (invitedById, invitedByGroupMemberId, localDisplayName, memberContactId, memberContactProfileId, profileId, displayName, fullName, image, contactLink, localAlias, contactPreferences) :. (createdAt, updatedAt))
toMaybeGroupMember _ _ = Nothing toMaybeGroupMember _ _ = Nothing
createGroupLink :: DB.Connection -> User -> GroupInfo -> ConnId -> ConnReqContact -> GroupLinkId -> GroupMemberRole -> SubscriptionMode -> ExceptT StoreError IO () createGroupLink :: DB.Connection -> User -> GroupInfo -> ConnId -> CreatedLinkContact -> GroupLinkId -> GroupMemberRole -> SubscriptionMode -> ExceptT StoreError IO ()
createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName} agentConnId cReq groupLinkId memberRole subMode = createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName} agentConnId (CCLink cReq shortLink) groupLinkId memberRole subMode =
checkConstraint (SEDuplicateGroupLink groupInfo) . liftIO $ do checkConstraint (SEDuplicateGroupLink groupInfo) . liftIO $ do
currentTs <- getCurrentTime currentTs <- getCurrentTime
DB.execute DB.execute
db db
"INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" "INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, short_link_contact, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)"
(userId, groupId, groupLinkId, "group_link_" <> localDisplayName, cReq, memberRole, BI True, currentTs, currentTs) (userId, groupId, groupLinkId, "group_link_" <> localDisplayName, cReq, shortLink, memberRole, BI True, currentTs, currentTs)
userContactLinkId <- insertedRowId db userContactLinkId <- insertedRowId db
void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId ConnNew initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode PQSupportOff void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId ConnNew initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode PQSupportOff
@ -250,12 +251,12 @@ deleteGroupLink db User {userId} GroupInfo {groupId} = do
(userId, groupId) (userId, groupId)
DB.execute db "DELETE FROM user_contact_links WHERE user_id = ? AND group_id = ?" (userId, groupId) DB.execute db "DELETE FROM user_contact_links WHERE user_id = ? AND group_id = ?" (userId, groupId)
getGroupLink :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO (Int64, ConnReqContact, GroupMemberRole) getGroupLink :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO (Int64, CreatedLinkContact, GroupMemberRole)
getGroupLink db User {userId} gInfo@GroupInfo {groupId} = getGroupLink db User {userId} gInfo@GroupInfo {groupId} =
ExceptT . firstRow groupLink (SEGroupLinkNotFound gInfo) $ ExceptT . firstRow groupLink (SEGroupLinkNotFound gInfo) $
DB.query db "SELECT user_contact_link_id, conn_req_contact, group_link_member_role FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1" (userId, groupId) DB.query db "SELECT user_contact_link_id, conn_req_contact, short_link_contact, group_link_member_role FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1" (userId, groupId)
where where
groupLink (linkId, cReq, mRole_) = (linkId, cReq, fromMaybe GRMember mRole_) groupLink (linkId, cReq, shortLink, mRole_) = (linkId, CCLink cReq shortLink, fromMaybe GRMember mRole_)
getGroupLinkId :: DB.Connection -> User -> GroupInfo -> IO (Maybe GroupLinkId) getGroupLinkId :: DB.Connection -> User -> GroupInfo -> IO (Maybe GroupLinkId)
getGroupLinkId db User {userId} GroupInfo {groupId} = getGroupLinkId db User {userId} GroupInfo {groupId} =
@ -1683,8 +1684,9 @@ getGroupInfo db vr User {userId, userContactId} groupId = ExceptT $ do
getGroupInfoByUserContactLinkConnReq :: DB.Connection -> VersionRangeChat -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo) getGroupInfoByUserContactLinkConnReq :: DB.Connection -> VersionRangeChat -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo)
getGroupInfoByUserContactLinkConnReq db vr user@User {userId} (cReqSchema1, cReqSchema2) = do getGroupInfoByUserContactLinkConnReq db vr user@User {userId} (cReqSchema1, cReqSchema2) = do
-- fmap join is to support group_id = NULL if non-group contact request is sent to this function (e.g., if client data is appended).
groupId_ <- groupId_ <-
maybeFirstRow fromOnly $ fmap join . maybeFirstRow fromOnly $
DB.query DB.query
db db
[sql| [sql|
@ -1695,6 +1697,26 @@ getGroupInfoByUserContactLinkConnReq db vr user@User {userId} (cReqSchema1, cReq
(userId, cReqSchema1, cReqSchema2) (userId, cReqSchema1, cReqSchema2)
maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db vr user) groupId_ maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db vr user) groupId_
getGroupInfoViaUserShortLink :: DB.Connection -> VersionRangeChat -> User -> ShortLinkContact -> IO (Maybe (ConnReqContact, GroupInfo))
getGroupInfoViaUserShortLink db vr user@User {userId} shortLink = fmap eitherToMaybe $ runExceptT $ do
(cReq, groupId) <- ExceptT getConnReqGroup
(cReq,) <$> getGroupInfo db vr user groupId
where
getConnReqGroup =
firstRow' toConnReqGroupId (SEInternalError "group link not found") $
DB.query
db
[sql|
SELECT conn_req_contact, group_id
FROM user_contact_links
WHERE user_id = ? AND short_link_contact = ?
|]
(userId, shortLink)
toConnReqGroupId = \case
-- cReq is "not null", group_id is nullable
(cReq, Just groupId) -> Right (cReq, groupId)
_ -> Left $ SEInternalError "no conn req or group ID"
getGroupInfoByGroupLinkHash :: DB.Connection -> VersionRangeChat -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe GroupInfo) getGroupInfoByGroupLinkHash :: DB.Connection -> VersionRangeChat -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe GroupInfo)
getGroupInfoByGroupLinkHash db vr user@User {userId, userContactId} (groupLinkHash1, groupLinkHash2) = do getGroupInfoByGroupLinkHash db vr user@User {userId, userContactId} (groupLinkHash1, groupLinkHash2) = do
groupId_ <- groupId_ <-

View file

@ -162,7 +162,7 @@ import Simplex.Chat.Store.NoteFolders
import Simplex.Chat.Store.Shared import Simplex.Chat.Store.Shared
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Shared
import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, MsgMeta (..), UserId) import Simplex.Messaging.Agent.Protocol (AgentMsgId, ConnId, ConnShortLink, ConnectionMode (..), MsgMeta (..), UserId)
import Simplex.Messaging.Agent.Store.AgentStore (firstRow, firstRow', maybeFirstRow) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, firstRow', maybeFirstRow)
import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) import Simplex.Messaging.Agent.Store.DB (BoolInt (..))
import qualified Simplex.Messaging.Agent.Store.DB as DB import qualified Simplex.Messaging.Agent.Store.DB as DB
@ -966,7 +966,7 @@ getContactConnectionChatPreviews_ db User {userId} pagination clq = case clq of
[sql| [sql|
SELECT SELECT
connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id,
custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at
FROM connections FROM connections
WHERE user_id = ? WHERE user_id = ?
AND conn_type = ? AND conn_type = ?
@ -982,7 +982,7 @@ getContactConnectionChatPreviews_ db User {userId} pagination clq = case clq of
PTLast count -> DB.query db (query <> " ORDER BY updated_at DESC LIMIT ?") (params search :. Only count) PTLast count -> DB.query db (query <> " ORDER BY updated_at DESC LIMIT ?") (params search :. Only count)
PTAfter ts count -> DB.query db (query <> " AND updated_at > ? ORDER BY updated_at ASC LIMIT ?") (params search :. (ts, count)) PTAfter ts count -> DB.query db (query <> " AND updated_at > ? ORDER BY updated_at ASC LIMIT ?") (params search :. (ts, count))
PTBefore ts count -> DB.query db (query <> " AND updated_at < ? ORDER BY updated_at DESC LIMIT ?") (params search :. (ts, count)) PTBefore ts count -> DB.query db (query <> " AND updated_at < ? ORDER BY updated_at DESC LIMIT ?") (params search :. (ts, count))
toPreview :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe GroupLinkId, Maybe Int64, Maybe ConnReqInvitation, LocalAlias, UTCTime, UTCTime) -> AChatPreviewData toPreview :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe GroupLinkId, Maybe Int64, Maybe ConnReqInvitation, Maybe (ConnShortLink 'CMInvitation), LocalAlias, UTCTime, UTCTime) -> AChatPreviewData
toPreview connRow = toPreview connRow =
let conn@PendingContactConnection {updatedAt} = toPendingContactConnection connRow let conn@PendingContactConnection {updatedAt} = toPendingContactConnection connRow
aChat = AChat SCTContactConnection $ Chat (ContactConnection conn) [] emptyChatStats aChat = AChat SCTContactConnection $ Chat (ContactConnection conn) [] emptyChatStats

View file

@ -5,11 +5,13 @@ module Simplex.Chat.Store.Postgres.Migrations (migrations) where
import Data.List (sortOn) import Data.List (sortOn)
import Data.Text (Text) import Data.Text (Text)
import Simplex.Chat.Store.Postgres.Migrations.M20241220_initial import Simplex.Chat.Store.Postgres.Migrations.M20241220_initial
import Simplex.Chat.Store.Postgres.Migrations.M20250402_short_links
import Simplex.Messaging.Agent.Store.Shared (Migration (..)) import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Text, Maybe Text)] schemaMigrations :: [(String, Text, Maybe Text)]
schemaMigrations = schemaMigrations =
[ ("20241220_initial", m20241220_initial, Nothing) [ ("20241220_initial", m20241220_initial, Nothing),
("20250402_short_links", m20250402_short_links, Just down_m20250402_short_links)
] ]
-- | The list of migrations in ascending order by date -- | The list of migrations in ascending order by date

View file

@ -0,0 +1,23 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.Postgres.Migrations.M20250402_short_links where
import Data.Text (Text)
import qualified Data.Text as T
import Text.RawString.QQ (r)
m20250402_short_links :: Text
m20250402_short_links =
T.pack
[r|
ALTER TABLE user_contact_links ADD COLUMN short_link_contact BYTEA;
ALTER TABLE connections ADD COLUMN short_link_inv BYTEA;
|]
down_m20250402_short_links :: Text
down_m20250402_short_links =
T.pack
[r|
ALTER TABLE user_contact_links DROP COLUMN short_link_contact;
ALTER TABLE connections DROP COLUMN short_link_inv;
|]

View file

@ -50,6 +50,7 @@ module Simplex.Chat.Store.Profiles
getUserContactLinkById, getUserContactLinkById,
getGroupLinkInfo, getGroupLinkInfo,
getUserContactLinkByConnReq, getUserContactLinkByConnReq,
getUserContactLinkViaShortLink,
getContactWithoutConnViaAddress, getContactWithoutConnViaAddress,
updateUserAddressAutoAccept, updateUserAddressAutoAccept,
getProtocolServers, getProtocolServers,
@ -100,7 +101,7 @@ import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.UITheme import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..)) import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..))
import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, UserId) import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, ConnectionLink (..), CreatedConnLink (..), UserId)
import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow)
import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) import Simplex.Messaging.Agent.Store.DB (BoolInt (..))
import qualified Simplex.Messaging.Agent.Store.DB as DB import qualified Simplex.Messaging.Agent.Store.DB as DB
@ -326,11 +327,13 @@ setUserProfileContactLink db user@User {userId, profile = p@LocalProfile {profil
SET contact_link = ?, updated_at = ? SET contact_link = ?, updated_at = ?
WHERE user_id = ? AND contact_profile_id = ? WHERE user_id = ? AND contact_profile_id = ?
|] |]
(connReqContact_, ts, userId, profileId) (contactLink, ts, userId, profileId)
pure (user :: User) {profile = p {contactLink = connReqContact_}} pure (user :: User) {profile = p {contactLink}}
where where
connReqContact_ = case ucl_ of -- TODO [short links] this should be replaced with short links once they are supported by all clients.
Just UserContactLink {connReqContact} -> Just connReqContact -- Or, maybe, we want to allow both, when both are optional.
contactLink = case ucl_ of
Just UserContactLink {connLinkContact = CCLink cReq _} -> Just $ CLFull cReq
_ -> Nothing _ -> Nothing
-- only used in tests -- only used in tests
@ -346,17 +349,17 @@ getUserContactProfiles db User {userId} =
|] |]
(Only userId) (Only userId)
where where
toContactProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnReqContact, Maybe Preferences) -> Profile toContactProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, Maybe Preferences) -> Profile
toContactProfile (displayName, fullName, image, contactLink, preferences) = Profile {displayName, fullName, image, contactLink, preferences} toContactProfile (displayName, fullName, image, contactLink, preferences) = Profile {displayName, fullName, image, contactLink, preferences}
createUserContactLink :: DB.Connection -> User -> ConnId -> ConnReqContact -> SubscriptionMode -> ExceptT StoreError IO () createUserContactLink :: DB.Connection -> User -> ConnId -> CreatedLinkContact -> SubscriptionMode -> ExceptT StoreError IO ()
createUserContactLink db User {userId} agentConnId cReq subMode = createUserContactLink db User {userId} agentConnId (CCLink cReq shortLink) subMode =
checkConstraint SEDuplicateContactLink . liftIO $ do checkConstraint SEDuplicateContactLink . liftIO $ do
currentTs <- getCurrentTime currentTs <- getCurrentTime
DB.execute DB.execute
db db
"INSERT INTO user_contact_links (user_id, conn_req_contact, created_at, updated_at) VALUES (?,?,?,?)" "INSERT INTO user_contact_links (user_id, conn_req_contact, short_link_contact, created_at, updated_at) VALUES (?,?,?,?,?)"
(userId, cReq, currentTs, currentTs) (userId, cReq, shortLink, currentTs, currentTs)
userContactLinkId <- insertedRowId db userContactLinkId <- insertedRowId db
void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId ConnNew initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId ConnNew initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff
@ -450,7 +453,7 @@ data UserMsgReceiptSettings = UserMsgReceiptSettings
deriving (Show) deriving (Show)
data UserContactLink = UserContactLink data UserContactLink = UserContactLink
{ connReqContact :: ConnReqContact, { connLinkContact :: CreatedLinkContact,
autoAccept :: Maybe AutoAccept autoAccept :: Maybe AutoAccept
} }
deriving (Show) deriving (Show)
@ -472,22 +475,15 @@ $(J.deriveJSON defaultJSON ''AutoAccept)
$(J.deriveJSON defaultJSON ''UserContactLink) $(J.deriveJSON defaultJSON ''UserContactLink)
toUserContactLink :: (ConnReqContact, BoolInt, BoolInt, BoolInt, Maybe MsgContent) -> UserContactLink toUserContactLink :: (ConnReqContact, Maybe ShortLinkContact, BoolInt, BoolInt, BoolInt, Maybe MsgContent) -> UserContactLink
toUserContactLink (connReq, BI autoAccept, BI businessAddress, BI acceptIncognito, autoReply) = toUserContactLink (connReq, shortLink, BI autoAccept, BI businessAddress, BI acceptIncognito, autoReply) =
UserContactLink connReq $ UserContactLink (CCLink connReq shortLink) $
if autoAccept then Just AutoAccept {businessAddress, acceptIncognito, autoReply} else Nothing if autoAccept then Just AutoAccept {businessAddress, acceptIncognito, autoReply} else Nothing
getUserAddress :: DB.Connection -> User -> ExceptT StoreError IO UserContactLink getUserAddress :: DB.Connection -> User -> ExceptT StoreError IO UserContactLink
getUserAddress db User {userId} = getUserAddress db User {userId} =
ExceptT . firstRow toUserContactLink SEUserContactLinkNotFound $ ExceptT . firstRow toUserContactLink SEUserContactLinkNotFound $
DB.query DB.query db (userContactLinkQuery <> " WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL") (Only userId)
db
[sql|
SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content
FROM user_contact_links
WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL
|]
(Only userId)
getUserContactLinkById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO (UserContactLink, Maybe GroupLinkInfo) getUserContactLinkById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO (UserContactLink, Maybe GroupLinkInfo)
getUserContactLinkById db userId userContactLinkId = getUserContactLinkById db userId userContactLinkId =
@ -495,7 +491,7 @@ getUserContactLinkById db userId userContactLinkId =
DB.query DB.query
db db
[sql| [sql|
SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role SELECT conn_req_contact, short_link_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role
FROM user_contact_links FROM user_contact_links
WHERE user_id = ? AND user_contact_link_id = ? WHERE user_id = ? AND user_contact_link_id = ?
|] |]
@ -521,14 +517,19 @@ getGroupLinkInfo db userId groupId =
getUserContactLinkByConnReq :: DB.Connection -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe UserContactLink) getUserContactLinkByConnReq :: DB.Connection -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe UserContactLink)
getUserContactLinkByConnReq db User {userId} (cReqSchema1, cReqSchema2) = getUserContactLinkByConnReq db User {userId} (cReqSchema1, cReqSchema2) =
maybeFirstRow toUserContactLink $ maybeFirstRow toUserContactLink $
DB.query DB.query db (userContactLinkQuery <> " WHERE user_id = ? AND conn_req_contact IN (?,?)") (userId, cReqSchema1, cReqSchema2)
db
getUserContactLinkViaShortLink :: DB.Connection -> User -> ShortLinkContact -> IO (Maybe UserContactLink)
getUserContactLinkViaShortLink db User {userId} shortLink =
maybeFirstRow toUserContactLink $
DB.query db (userContactLinkQuery <> " WHERE user_id = ? AND short_link_contact = ?") (userId, shortLink)
userContactLinkQuery :: Query
userContactLinkQuery =
[sql| [sql|
SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content SELECT conn_req_contact, short_link_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content
FROM user_contact_links FROM user_contact_links
WHERE user_id = ? AND conn_req_contact IN (?,?)
|] |]
(userId, cReqSchema1, cReqSchema2)
getContactWithoutConnViaAddress :: DB.Connection -> VersionRangeChat -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe Contact) getContactWithoutConnViaAddress :: DB.Connection -> VersionRangeChat -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe Contact)
getContactWithoutConnViaAddress db vr user@User {userId} (cReqSchema1, cReqSchema2) = do getContactWithoutConnViaAddress db vr user@User {userId} (cReqSchema1, cReqSchema2) = do

View file

@ -128,6 +128,7 @@ import Simplex.Chat.Store.SQLite.Migrations.M20250122_chat_items_include_in_hist
import Simplex.Chat.Store.SQLite.Migrations.M20250126_mentions import Simplex.Chat.Store.SQLite.Migrations.M20250126_mentions
import Simplex.Chat.Store.SQLite.Migrations.M20250129_delete_unused_contacts import Simplex.Chat.Store.SQLite.Migrations.M20250129_delete_unused_contacts
import Simplex.Chat.Store.SQLite.Migrations.M20250130_indexes import Simplex.Chat.Store.SQLite.Migrations.M20250130_indexes
import Simplex.Chat.Store.SQLite.Migrations.M20250402_short_links
import Simplex.Messaging.Agent.Store.Shared (Migration (..)) import Simplex.Messaging.Agent.Store.Shared (Migration (..))
schemaMigrations :: [(String, Query, Maybe Query)] schemaMigrations :: [(String, Query, Maybe Query)]
@ -255,7 +256,8 @@ schemaMigrations =
("20250122_chat_items_include_in_history", m20250122_chat_items_include_in_history, Just down_m20250122_chat_items_include_in_history), ("20250122_chat_items_include_in_history", m20250122_chat_items_include_in_history, Just down_m20250122_chat_items_include_in_history),
("20250126_mentions", m20250126_mentions, Just down_m20250126_mentions), ("20250126_mentions", m20250126_mentions, Just down_m20250126_mentions),
("20250129_delete_unused_contacts", m20250129_delete_unused_contacts, Just down_m20250129_delete_unused_contacts), ("20250129_delete_unused_contacts", m20250129_delete_unused_contacts, Just down_m20250129_delete_unused_contacts),
("20250130_indexes", m20250130_indexes, Just down_m20250130_indexes) ("20250130_indexes", m20250130_indexes, Just down_m20250130_indexes),
("20250402_short_links", m20250402_short_links, Just down_m20250402_short_links)
] ]
-- | The list of migrations in ascending order by date -- | The list of migrations in ascending order by date

View file

@ -0,0 +1,23 @@
{-# LANGUAGE QuasiQuotes #-}
module Simplex.Chat.Store.SQLite.Migrations.M20250402_short_links where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
m20250402_short_links :: Query
m20250402_short_links =
[sql|
ALTER TABLE user_contact_links ADD COLUMN short_link_contact BLOB;
ALTER TABLE connections ADD COLUMN short_link_inv BLOB;
ALTER TABLE connections ADD COLUMN via_short_link_contact BLOB;
|]
down_m20250402_short_links :: Query
down_m20250402_short_links =
[sql|
ALTER TABLE user_contact_links DROP COLUMN short_link_contact;
ALTER TABLE connections DROP COLUMN short_link_inv;
ALTER TABLE connections DROP COLUMN via_short_link_contact;
|]

View file

@ -443,6 +443,22 @@ Query:
Plan: Plan:
SEARCH connections USING PRIMARY KEY (conn_id=?) SEARCH connections USING PRIMARY KEY (conn_id=?)
Query:
SELECT link_id, snd_private_key
FROM inv_short_links
WHERE host = ? AND port = ? AND snd_id = ?
Plan:
SEARCH inv_short_links USING INDEX idx_inv_short_links_link_id (host=? AND port=?)
Query:
SELECT link_key, snd_private_key, snd_id
FROM inv_short_links
WHERE host = ? AND port = ? AND link_id = ?
Plan:
SEARCH inv_short_links USING INDEX idx_inv_short_links_link_id (host=? AND port=? AND link_id=?)
Query: Query:
SELECT s.internal_id, m.msg_type, s.internal_hash, s.rcpt_internal_id, s.rcpt_status SELECT s.internal_id, m.msg_type, s.internal_hash, s.rcpt_internal_id, s.rcpt_status
FROM snd_messages s FROM snd_messages s
@ -466,6 +482,19 @@ Query:
Plan: Plan:
Query:
INSERT INTO inv_short_links
(host, port, server_key_hash, link_id, link_key, snd_private_key, snd_id)
VALUES (?,?,?,?,?,?,?)
ON CONFLICT (host, port, link_id)
DO UPDATE SET
server_key_hash = EXCLUDED.server_key_hash,
link_key = EXCLUDED.link_key,
snd_private_key = EXCLUDED.snd_private_key,
snd_id = EXCLUDED.snd_id
Plan:
Query: Query:
INSERT INTO messages INSERT INTO messages
(conn_id, internal_id, internal_ts, internal_rcv_id, internal_snd_id, msg_type, msg_flags, msg_body, pq_encryption) (conn_id, internal_id, internal_ts, internal_rcv_id, internal_snd_id, msg_type, msg_flags, msg_body, pq_encryption)
@ -524,7 +553,10 @@ SEARCH messages USING COVERING INDEX idx_messages_conn_id_internal_rcv_id (conn_
Query: Query:
INSERT INTO rcv_queues INSERT INTO rcv_queues
(host, port, rcv_id, conn_id, rcv_private_key, rcv_dh_secret, e2e_priv_key, e2e_dh_secret, snd_id, snd_secure, status, rcv_queue_id, rcv_primary, replace_rcv_queue_id, smp_client_version, server_key_hash) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?); ( host, port, rcv_id, conn_id, rcv_private_key, rcv_dh_secret, e2e_priv_key, e2e_dh_secret,
snd_id, queue_mode, status, rcv_queue_id, rcv_primary, replace_rcv_queue_id, smp_client_version, server_key_hash,
link_id, link_key, link_priv_sig_key, link_enc_fixed_data
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);
Plan: Plan:
@ -546,14 +578,14 @@ SEARCH messages USING COVERING INDEX idx_messages_conn_id_internal_snd_id (conn_
Query: Query:
INSERT INTO snd_queues INSERT INTO snd_queues
(host, port, snd_id, snd_secure, conn_id, snd_public_key, snd_private_key, e2e_pub_key, e2e_dh_secret, (host, port, snd_id, queue_mode, conn_id, snd_public_key, snd_private_key, e2e_pub_key, e2e_dh_secret,
status, snd_queue_id, snd_primary, replace_snd_queue_id, smp_client_version, server_key_hash) status, snd_queue_id, snd_primary, replace_snd_queue_id, smp_client_version, server_key_hash)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
ON CONFLICT (host, port, snd_id) DO UPDATE SET ON CONFLICT (host, port, snd_id) DO UPDATE SET
host=EXCLUDED.host, host=EXCLUDED.host,
port=EXCLUDED.port, port=EXCLUDED.port,
snd_id=EXCLUDED.snd_id, snd_id=EXCLUDED.snd_id,
snd_secure=EXCLUDED.snd_secure, queue_mode=EXCLUDED.queue_mode,
conn_id=EXCLUDED.conn_id, conn_id=EXCLUDED.conn_id,
snd_public_key=EXCLUDED.snd_public_key, snd_public_key=EXCLUDED.snd_public_key,
snd_private_key=EXCLUDED.snd_private_key, snd_private_key=EXCLUDED.snd_private_key,
@ -631,6 +663,14 @@ Query:
Plan: Plan:
SEARCH connections USING PRIMARY KEY (conn_id=?) SEARCH connections USING PRIMARY KEY (conn_id=?)
Query:
UPDATE inv_short_links
SET snd_id = ?
WHERE host = ? AND port = ? AND link_id = ?
Plan:
SEARCH inv_short_links USING INDEX idx_inv_short_links_link_id (host=? AND port=? AND link_id=?)
Query: Query:
UPDATE ratchets UPDATE ratchets
SET x3dh_priv_key_1 = ?, x3dh_priv_key_2 = ?, pq_priv_kem = ? SET x3dh_priv_key_1 = ?, x3dh_priv_key_2 = ?, pq_priv_kem = ?
@ -691,7 +731,7 @@ SEARCH snd_queues USING PRIMARY KEY (host=? AND port=? AND snd_id=?)
Query: Query:
SELECT SELECT
c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.snd_id, q.snd_secure, c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.snd_id, q.queue_mode,
q.snd_public_key, q.snd_private_key, q.e2e_pub_key, q.e2e_dh_secret, q.status, q.snd_public_key, q.snd_private_key, q.e2e_pub_key, q.e2e_dh_secret, q.status,
q.snd_queue_id, q.snd_primary, q.replace_snd_queue_id, q.switch_status, q.smp_client_version q.snd_queue_id, q.snd_primary, q.replace_snd_queue_id, q.switch_status, q.smp_client_version
FROM snd_queues q FROM snd_queues q
@ -705,9 +745,10 @@ SEARCH s USING PRIMARY KEY (host=? AND port=?)
Query: Query:
SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret,
q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.snd_secure, q.status, q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status,
q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors,
q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret,
q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data
FROM rcv_queues q FROM rcv_queues q
JOIN servers s ON q.host = s.host AND q.port = s.port JOIN servers s ON q.host = s.host AND q.port = s.port
JOIN connections c ON q.conn_id = c.conn_id JOIN connections c ON q.conn_id = c.conn_id
@ -719,9 +760,10 @@ SEARCH s USING PRIMARY KEY (host=? AND port=?)
Query: Query:
SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret,
q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.snd_secure, q.status, q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status,
q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors,
q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret,
q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data
FROM rcv_queues q FROM rcv_queues q
JOIN servers s ON q.host = s.host AND q.port = s.port JOIN servers s ON q.host = s.host AND q.port = s.port
JOIN connections c ON q.conn_id = c.conn_id JOIN connections c ON q.conn_id = c.conn_id
@ -733,9 +775,10 @@ SEARCH c USING PRIMARY KEY (conn_id=?)
Query: Query:
SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret,
q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.snd_secure, q.status, q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status,
q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors,
q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret,
q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data
FROM rcv_queues q FROM rcv_queues q
JOIN servers s ON q.host = s.host AND q.port = s.port JOIN servers s ON q.host = s.host AND q.port = s.port
JOIN connections c ON q.conn_id = c.conn_id JOIN connections c ON q.conn_id = c.conn_id
@ -747,9 +790,10 @@ SEARCH c USING PRIMARY KEY (conn_id=?)
Query: Query:
SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret,
q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.snd_secure, q.status, q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status,
q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors,
q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret,
q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data
FROM rcv_queues q FROM rcv_queues q
JOIN servers s ON q.host = s.host AND q.port = s.port JOIN servers s ON q.host = s.host AND q.port = s.port
JOIN connections c ON q.conn_id = c.conn_id JOIN connections c ON q.conn_id = c.conn_id
@ -761,9 +805,10 @@ SEARCH s USING PRIMARY KEY (host=? AND port=?)
Query: Query:
SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret, SELECT c.user_id, COALESCE(q.server_key_hash, s.key_hash), q.conn_id, q.host, q.port, q.rcv_id, q.rcv_private_key, q.rcv_dh_secret,
q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.snd_secure, q.status, q.e2e_priv_key, q.e2e_dh_secret, q.snd_id, q.queue_mode, q.status,
q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors, q.rcv_queue_id, q.rcv_primary, q.replace_rcv_queue_id, q.switch_status, q.smp_client_version, q.delete_errors,
q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret q.ntf_public_key, q.ntf_private_key, q.ntf_id, q.rcv_ntf_dh_secret,
q.link_id, q.link_key, q.link_priv_sig_key, q.link_enc_fixed_data
FROM rcv_queues q FROM rcv_queues q
JOIN servers s ON q.host = s.host AND q.port = s.port JOIN servers s ON q.host = s.host AND q.port = s.port
JOIN connections c ON q.conn_id = c.conn_id JOIN connections c ON q.conn_id = c.conn_id
@ -799,6 +844,10 @@ Query: DELETE FROM deleted_snd_chunk_replicas WHERE deleted_snd_chunk_replica_id
Plan: Plan:
SEARCH deleted_snd_chunk_replicas USING INTEGER PRIMARY KEY (rowid=?) SEARCH deleted_snd_chunk_replicas USING INTEGER PRIMARY KEY (rowid=?)
Query: DELETE FROM inv_short_links WHERE host = ? AND port = ? AND link_id = ?
Plan:
SEARCH inv_short_links USING INDEX idx_inv_short_links_link_id (host=? AND port=? AND link_id=?)
Query: DELETE FROM messages WHERE conn_id = ? AND internal_id = ?; Query: DELETE FROM messages WHERE conn_id = ? AND internal_id = ?;
Plan: Plan:
SEARCH messages USING PRIMARY KEY (conn_id=? AND internal_id=?) SEARCH messages USING PRIMARY KEY (conn_id=? AND internal_id=?)

View file

@ -497,6 +497,14 @@ Plan:
SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=?) SEARCH chat_items USING INDEX idx_chat_items_groups_user_mention (user_id=?)
USE TEMP B-TREE FOR ORDER BY USE TEMP B-TREE FOR ORDER BY
Query:
SELECT conn_req_contact, group_id
FROM user_contact_links
WHERE user_id = ? AND short_link_contact = ?
Plan:
SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?)
Query: Query:
SELECT conn_req_contact, group_id SELECT conn_req_contact, group_id
FROM user_contact_links FROM user_contact_links
@ -505,6 +513,14 @@ Query:
Plan: Plan:
SEARCH user_contact_links USING INTEGER PRIMARY KEY (rowid=?) SEARCH user_contact_links USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT conn_req_inv, agent_conn_id
FROM connections
WHERE user_id = ? AND short_link_inv = ? LIMIT 1
Plan:
SEARCH connections USING INDEX idx_connections_updated_at (user_id=?)
Query: Query:
SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id,
conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id,
@ -1347,7 +1363,7 @@ SCAN cc
Query: Query:
SELECT SELECT
connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id,
custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at
FROM connections FROM connections
WHERE user_id = ? WHERE user_id = ?
AND conn_type = ? AND conn_type = ?
@ -1364,7 +1380,7 @@ SEARCH connections USING INDEX idx_connections_updated_at (user_id=? AND updated
Query: Query:
SELECT SELECT
connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id,
custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at
FROM connections FROM connections
WHERE user_id = ? WHERE user_id = ?
AND conn_type = ? AND conn_type = ?
@ -1381,7 +1397,7 @@ SEARCH connections USING INDEX idx_connections_updated_at (user_id=? AND updated
Query: Query:
SELECT SELECT
connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id,
custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at
FROM connections FROM connections
WHERE user_id = ? WHERE user_id = ?
AND conn_type = ? AND conn_type = ?
@ -2935,23 +2951,7 @@ Plan:
SEARCH commands USING INTEGER PRIMARY KEY (rowid=?) SEARCH commands USING INTEGER PRIMARY KEY (rowid=?)
Query: Query:
SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content SELECT conn_req_contact, short_link_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role
FROM user_contact_links
WHERE user_id = ? AND conn_req_contact IN (?,?)
Plan:
SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?)
Query:
SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content
FROM user_contact_links
WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL
Plan:
SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?)
Query:
SELECT conn_req_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content, group_id, group_link_member_role
FROM user_contact_links FROM user_contact_links
WHERE user_id = ? AND user_contact_link_id = ? WHERE user_id = ? AND user_contact_link_id = ?
@ -2970,7 +2970,7 @@ Plan:
SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) SEARCH connections USING INTEGER PRIMARY KEY (rowid=?)
Query: Query:
SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at
FROM connections FROM connections
WHERE user_id = ? WHERE user_id = ?
AND conn_type = ? AND conn_type = ?
@ -2980,7 +2980,7 @@ Plan:
SEARCH connections USING INDEX idx_connections_updated_at (user_id=?) SEARCH connections USING INDEX idx_connections_updated_at (user_id=?)
Query: Query:
SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, local_alias, created_at, updated_at SELECT connection_id, agent_conn_id, conn_status, via_contact_uri_hash, via_user_contact_link, group_link_id, custom_user_profile_id, conn_req_inv, short_link_inv, local_alias, created_at, updated_at
FROM connections FROM connections
WHERE user_id = ? WHERE user_id = ?
AND connection_id = ? AND connection_id = ?
@ -3944,9 +3944,9 @@ Plan:
Query: Query:
INSERT INTO connections INSERT INTO connections
(user_id, agent_conn_id, conn_req_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_conn_initiated, custom_user_profile_id,
created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption) created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
Plan: Plan:
@ -3962,9 +3962,9 @@ Plan:
Query: Query:
INSERT INTO connections ( INSERT INTO connections (
user_id, agent_conn_id, conn_status, conn_type, contact_conn_initiated, user_id, agent_conn_id, conn_status, conn_type, contact_conn_initiated,
via_contact_uri_hash, xcontact_id, custom_user_profile_id, via_group_link, group_link_id, via_contact_uri_hash, via_short_link_contact, 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 created_at, updated_at, to_subscribe, conn_chat_version, pq_support, pq_encryption
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
Plan: Plan:
@ -4650,6 +4650,27 @@ SEARCH c USING INTEGER PRIMARY KEY (rowid=?) LEFT-JOIN
CORRELATED SCALAR SUBQUERY 1 CORRELATED SCALAR SUBQUERY 1
SEARCH cc USING COVERING INDEX idx_connections_group_member (user_id=? AND group_member_id=?) SEARCH cc USING COVERING INDEX idx_connections_group_member (user_id=? AND group_member_id=?)
Query:
SELECT conn_req_contact, short_link_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content
FROM user_contact_links
WHERE user_id = ? AND conn_req_contact IN (?,?)
Plan:
SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?)
Query:
SELECT conn_req_contact, short_link_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content
FROM user_contact_links
WHERE user_id = ? AND local_display_name = '' AND group_id IS NULL
Plan:
SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=? AND local_display_name=?)
Query:
SELECT conn_req_contact, short_link_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content
FROM user_contact_links
WHERE user_id = ? AND short_link_contact = ?
Plan:
SEARCH user_contact_links USING INDEX sqlite_autoindex_user_contact_links_1 (user_id=?)
Query: Query:
SELECT f.file_id, f.ci_file_status, f.file_path SELECT f.file_id, f.ci_file_status, f.file_path
FROM chat_items i FROM chat_items i
@ -5409,10 +5430,10 @@ SEARCH connections USING INTEGER PRIMARY KEY (rowid=?)
Query: INSERT INTO temp_conn_ids (conn_id) VALUES (?) Query: INSERT INTO temp_conn_ids (conn_id) VALUES (?)
Plan: Plan:
Query: INSERT INTO user_contact_links (user_id, conn_req_contact, created_at, updated_at) VALUES (?,?,?,?) Query: INSERT INTO user_contact_links (user_id, conn_req_contact, short_link_contact, created_at, updated_at) VALUES (?,?,?,?,?)
Plan: Plan:
Query: INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?) Query: INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, short_link_contact, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)
Plan: Plan:
Query: INSERT INTO users (agent_user_id, local_display_name, active_user, active_order, contact_id, show_ntfs, send_rcpts_contacts, send_rcpts_small_groups, created_at, updated_at) VALUES (?,?,?,?,0,?,?,?,?,?) Query: INSERT INTO users (agent_user_id, local_display_name, active_user, active_order, contact_id, show_ntfs, send_rcpts_contacts, send_rcpts_small_groups, created_at, updated_at) VALUES (?,?,?,?,0,?,?,?,?,?)
@ -5637,7 +5658,7 @@ Query: SELECT user_contact_link_id FROM contact_requests WHERE contact_request_i
Plan: Plan:
SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?) SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?)
Query: SELECT user_contact_link_id, conn_req_contact, group_link_member_role FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1 Query: SELECT user_contact_link_id, conn_req_contact, short_link_contact, group_link_member_role FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1
Plan: Plan:
SEARCH user_contact_links USING INDEX idx_user_contact_links_group_id (group_id=?) SEARCH user_contact_links USING INDEX idx_user_contact_links_group_id (group_id=?)
@ -5685,7 +5706,7 @@ Query: UPDATE connections SET conn_status = ?, updated_at = ? WHERE connection_i
Plan: Plan:
SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) SEARCH connections USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE connections SET conn_status = ?, updated_at = ?, conn_req_inv = NULL WHERE connection_id = ? Query: UPDATE connections SET conn_status = ?, updated_at = ?, conn_req_inv = NULL, short_link_inv = NULL WHERE connection_id = ?
Plan: Plan:
SEARCH connections USING INTEGER PRIMARY KEY (rowid=?) SEARCH connections USING INTEGER PRIMARY KEY (rowid=?)

View file

@ -297,6 +297,8 @@ CREATE TABLE connections(
pq_snd_enabled INTEGER, pq_snd_enabled INTEGER,
pq_rcv_enabled INTEGER, pq_rcv_enabled INTEGER,
quota_err_counter INTEGER NOT NULL DEFAULT 0, quota_err_counter INTEGER NOT NULL DEFAULT 0,
short_link_inv BLOB,
via_short_link_contact BLOB,
FOREIGN KEY(snd_file_id, connection_id) FOREIGN KEY(snd_file_id, connection_id)
REFERENCES snd_files(file_id, connection_id) REFERENCES snd_files(file_id, connection_id)
ON DELETE CASCADE ON DELETE CASCADE
@ -316,6 +318,7 @@ CREATE TABLE user_contact_links(
group_link_id BLOB, group_link_id BLOB,
group_link_member_role TEXT NULL, group_link_member_role TEXT NULL,
business_address INTEGER DEFAULT 0, business_address INTEGER DEFAULT 0,
short_link_contact BLOB,
UNIQUE(user_id, local_display_name) UNIQUE(user_id, local_display_name)
); );
CREATE TABLE contact_requests( CREATE TABLE contact_requests(

View file

@ -1,4 +1,5 @@
{-# LANGUAGE CPP #-} {-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
@ -35,7 +36,7 @@ import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Shared
import Simplex.Chat.Types.UITheme import Simplex.Chat.Types.UITheme
import Simplex.Messaging.Agent.Protocol (ConnId, UserId) import Simplex.Messaging.Agent.Protocol (ConnId, ConnShortLink, ConnectionMode (..), CreatedConnLink (..), UserId)
import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow) import Simplex.Messaging.Agent.Store.AgentStore (firstRow, maybeFirstRow)
import Simplex.Messaging.Agent.Store.DB (BoolInt (..)) import Simplex.Messaging.Agent.Store.DB (BoolInt (..))
import qualified Simplex.Messaging.Agent.Store.DB as DB import qualified Simplex.Messaging.Agent.Store.DB as DB
@ -416,7 +417,7 @@ deleteUnusedIncognitoProfileById_ db User {userId} profileId =
|] |]
(userId, profileId, userId, profileId, userId, profileId) (userId, profileId, userId, profileId, userId, profileId)
type ContactRow' = (ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. (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 GroupMemberId, BoolInt, Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64)
type ContactRow = Only ContactId :. ContactRow' type ContactRow = Only ContactId :. ContactRow'
@ -441,10 +442,10 @@ getProfileById db userId profileId =
|] |]
(userId, profileId) (userId, profileId)
where where
toProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Maybe Preferences) -> LocalProfile toProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, Maybe Preferences) -> LocalProfile
toProfile (displayName, fullName, image, contactLink, localAlias, preferences) = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} toProfile (displayName, fullName, image, contactLink, localAlias, preferences) = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Int64, AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact) :. (Maybe XContactId, PQSupport, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat) type ContactRequestRow = (Int64, ContactName, AgentInvId, Maybe ContactId, Int64, AgentConnId, Int64, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact) :. (Maybe XContactId, PQSupport, Maybe Preferences, UTCTime, UTCTime, VersionChat, VersionChat)
toContactRequest :: ContactRequestRow -> UserContactRequest toContactRequest :: ContactRequestRow -> UserContactRequest
toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, contactId_, userContactLinkId, agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, pqSupport, preferences, createdAt, updatedAt, minVer, maxVer)) = do toContactRequest ((contactRequestId, localDisplayName, agentInvitationId, contactId_, userContactLinkId, agentContactConnId, profileId, displayName, fullName, image, contactLink) :. (xContactId, pqSupport, preferences, createdAt, updatedAt, minVer, maxVer)) = do
@ -462,7 +463,7 @@ userQuery =
JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id
|] |]
toUser :: (UserId, UserId, ContactId, ProfileId, BoolInt, Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, Maybe Preferences) :. (BoolInt, BoolInt, BoolInt, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime, Maybe UIThemeEntityOverrides) -> User toUser :: (UserId, UserId, ContactId, ProfileId, BoolInt, Int64, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, Maybe Preferences) :. (BoolInt, BoolInt, BoolInt, Maybe B64UrlByteString, Maybe B64UrlByteString, Maybe UTCTime, Maybe UIThemeEntityOverrides) -> User
toUser ((userId, auId, userContactId, profileId, BI activeUser, activeOrder, displayName, fullName, image, contactLink, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt, uiThemes)) = toUser ((userId, auId, userContactId, profileId, BI activeUser, activeOrder, displayName, fullName, image, contactLink, userPreferences) :. (BI showNtfs, BI sendRcptsContacts, BI sendRcptsSmallGroups, viewPwdHash_, viewPwdSalt_, userMemberProfileUpdatedAt, uiThemes)) =
User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, activeOrder, fullPreferences, showNtfs, sendRcptsContacts, sendRcptsSmallGroups, viewPwdHash, userMemberProfileUpdatedAt, uiThemes} User {userId, agentUserId = AgentUserId auId, userContactId, localDisplayName = displayName, profile, activeUser, activeOrder, fullPreferences, showNtfs, sendRcptsContacts, sendRcptsSmallGroups, viewPwdHash, userMemberProfileUpdatedAt, uiThemes}
where where
@ -470,9 +471,10 @@ toUser ((userId, auId, userContactId, profileId, BI activeUser, activeOrder, dis
fullPreferences = mergePreferences Nothing userPreferences fullPreferences = mergePreferences Nothing userPreferences
viewPwdHash = UserPwdHash <$> viewPwdHash_ <*> viewPwdSalt_ viewPwdHash = UserPwdHash <$> viewPwdHash_ <*> viewPwdSalt_
toPendingContactConnection :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe GroupLinkId, Maybe Int64, Maybe ConnReqInvitation, LocalAlias, UTCTime, UTCTime) -> PendingContactConnection toPendingContactConnection :: (Int64, ConnId, ConnStatus, Maybe ByteString, Maybe Int64, Maybe GroupLinkId, Maybe Int64, Maybe ConnReqInvitation, Maybe (ConnShortLink 'CMInvitation), LocalAlias, UTCTime, UTCTime) -> PendingContactConnection
toPendingContactConnection (pccConnId, acId, pccConnStatus, connReqHash, viaUserContactLink, groupLinkId, customUserProfileId, connReqInv, localAlias, createdAt, updatedAt) = toPendingContactConnection (pccConnId, acId, pccConnStatus, connReqHash, viaUserContactLink, groupLinkId, customUserProfileId, connReqInv, shortLinkInv, localAlias, createdAt, updatedAt) =
PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = isJust connReqHash, viaUserContactLink, groupLinkId, customUserProfileId, connReqInv, localAlias, createdAt, updatedAt} let connLinkInv = (`CCLink` shortLinkInv) <$> connReqInv
in PendingContactConnection {pccConnId, pccAgentConnId = AgentConnId acId, pccConnStatus, viaContactUri = isJust connReqHash, viaUserContactLink, groupLinkId, customUserProfileId, connLinkInv, localAlias, createdAt, updatedAt}
getConnReqInv :: DB.Connection -> Int64 -> ExceptT StoreError IO ConnReqInvitation getConnReqInv :: DB.Connection -> Int64 -> ExceptT StoreError IO ConnReqInvitation
getConnReqInv db connId = getConnReqInv db connId =
@ -579,7 +581,7 @@ type BusinessChatInfoRow = (Maybe BusinessChatType, Maybe MemberId, Maybe Member
type GroupInfoRow = (Int64, GroupName, GroupName, Text, Text, Maybe Text, Maybe ImageData, Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. BusinessChatInfoRow :. (Maybe UIThemeEntityOverrides, Maybe CustomData, Maybe Int64) :. GroupMemberRow type GroupInfoRow = (Int64, GroupName, GroupName, Text, Text, Maybe Text, Maybe ImageData, Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe GroupPreferences) :. (UTCTime, UTCTime, Maybe UTCTime, Maybe UTCTime) :. BusinessChatInfoRow :. (Maybe UIThemeEntityOverrides, Maybe CustomData, Maybe Int64) :. 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 ConnReqContact, LocalAlias, Maybe Preferences) :. (UTCTime, UTCTime) 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)
toGroupInfo :: VersionRangeChat -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo toGroupInfo :: VersionRangeChat -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo
toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, localAlias, description, image, enableNtfs_, sendRcpts, BI favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. businessRow :. (uiThemes, customData, chatItemTTL) :. userMemberRow) = toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, localAlias, description, image, enableNtfs_, sendRcpts, BI favorite, groupPreferences) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. businessRow :. (uiThemes, customData, chatItemTTL) :. userMemberRow) =

View file

@ -9,12 +9,13 @@ module Simplex.Chat.Terminal where
import Control.Monad import Control.Monad
import qualified Data.List.NonEmpty as L import qualified Data.List.NonEmpty as L
import Simplex.Chat (defaultChatConfig, operatorSimpleXChat) import Simplex.Chat (defaultChatConfig)
import Simplex.Chat.Controller import Simplex.Chat.Controller
import Simplex.Chat.Core import Simplex.Chat.Core
import Simplex.Chat.Help (chatWelcome) import Simplex.Chat.Help (chatWelcome)
import Simplex.Chat.Library.Commands (_defaultNtfServers) import Simplex.Chat.Library.Commands (_defaultNtfServers)
import Simplex.Chat.Operators import Simplex.Chat.Operators
import Simplex.Chat.Operators.Presets (operatorSimpleXChat)
import Simplex.Chat.Options import Simplex.Chat.Options
import Simplex.Chat.Terminal.Input import Simplex.Chat.Terminal.Input
import Simplex.Chat.Terminal.Output import Simplex.Chat.Terminal.Output

View file

@ -51,7 +51,7 @@ import Simplex.Chat.Types.UITheme
import Simplex.Chat.Types.Util import Simplex.Chat.Types.Util
import Simplex.FileTransfer.Description (FileDigest) import Simplex.FileTransfer.Description (FileDigest)
import Simplex.FileTransfer.Types (RcvFileId, SndFileId) import Simplex.FileTransfer.Types (RcvFileId, SndFileId)
import Simplex.Messaging.Agent.Protocol (ACorrId, AEventTag (..), AEvtTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, SAEntity (..), UserId) import Simplex.Messaging.Agent.Protocol (ACorrId, AEventTag (..), AEvtTag (..), ConnId, ConnShortLink, ConnectionLink, ConnectionMode (..), ConnectionRequestUri, CreatedConnLink, InvitationId, SAEntity (..), UserId)
import Simplex.Messaging.Agent.Store.DB (Binary (..), blobFieldDecoder, fromTextField_) import Simplex.Messaging.Agent.Store.DB (Binary (..), blobFieldDecoder, fromTextField_)
import Simplex.Messaging.Crypto.File (CryptoFileArgs (..)) import Simplex.Messaging.Crypto.File (CryptoFileArgs (..))
import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff) import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff)
@ -220,6 +220,8 @@ contactConnId c = aConnId <$> contactConn c
type IncognitoEnabled = Bool type IncognitoEnabled = Bool
type CreateShortLink = Bool
contactConnIncognito :: Contact -> IncognitoEnabled contactConnIncognito :: Contact -> IncognitoEnabled
contactConnIncognito = maybe False connIncognito . contactConn contactConnIncognito = maybe False connIncognito . contactConn
@ -559,7 +561,7 @@ data Profile = Profile
{ displayName :: ContactName, { displayName :: ContactName,
fullName :: Text, fullName :: Text,
image :: Maybe ImageData, image :: Maybe ImageData,
contactLink :: Maybe ConnReqContact, contactLink :: Maybe ConnLinkContact,
preferences :: Maybe Preferences preferences :: Maybe Preferences
-- fields that should not be read into this data type to prevent sending them as part of profile to contacts: -- fields that should not be read into this data type to prevent sending them as part of profile to contacts:
-- - contact_profile_id -- - contact_profile_id
@ -592,7 +594,7 @@ data LocalProfile = LocalProfile
displayName :: ContactName, displayName :: ContactName,
fullName :: Text, fullName :: Text,
image :: Maybe ImageData, image :: Maybe ImageData,
contactLink :: Maybe ConnReqContact, contactLink :: Maybe ConnLinkContact,
preferences :: Maybe Preferences, preferences :: Maybe Preferences,
localAlias :: LocalAlias localAlias :: LocalAlias
} }
@ -1407,6 +1409,14 @@ type ConnReqInvitation = ConnectionRequestUri 'CMInvitation
type ConnReqContact = ConnectionRequestUri 'CMContact type ConnReqContact = ConnectionRequestUri 'CMContact
type CreatedLinkInvitation = CreatedConnLink 'CMInvitation
type CreatedLinkContact = CreatedConnLink 'CMContact
type ConnLinkContact = ConnectionLink 'CMContact
type ShortLinkContact = ConnShortLink 'CMContact
data Connection = Connection data Connection = Connection
{ connId :: Int64, { connId :: Int64,
agentConnId :: AgentConnId, agentConnId :: AgentConnId,
@ -1484,7 +1494,7 @@ data PendingContactConnection = PendingContactConnection
viaUserContactLink :: Maybe Int64, viaUserContactLink :: Maybe Int64,
groupLinkId :: Maybe GroupLinkId, groupLinkId :: Maybe GroupLinkId,
customUserProfileId :: Maybe Int64, customUserProfileId :: Maybe Int64,
connReqInv :: Maybe ConnReqInvitation, connLinkInv :: Maybe CreatedLinkInvitation,
localAlias :: Text, localAlias :: Text,
createdAt :: UTCTime, createdAt :: UTCTime,
updatedAt :: UTCTime updatedAt :: UTCTime

View file

@ -90,7 +90,7 @@ serializeChatResponse :: (Maybe RemoteHostId, Maybe User) -> CurrentTime -> Time
serializeChatResponse user_ ts tz remoteHost_ = unlines . map unStyle . responseToView user_ defaultChatConfig False ts tz remoteHost_ serializeChatResponse user_ ts tz remoteHost_ = unlines . map unStyle . responseToView user_ defaultChatConfig False ts tz remoteHost_
responseToView :: (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Bool -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatResponse -> [StyledString] responseToView :: (Maybe RemoteHostId, Maybe User) -> ChatConfig -> Bool -> CurrentTime -> TimeZone -> Maybe RemoteHostId -> ChatResponse -> [StyledString]
responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showReceipts, testView} liveItems ts tz outputRH = \case responseToView hu@(currentRH, user_) cfg@ChatConfig {logLevel, showReactions, showReceipts, testView} liveItems ts tz outputRH = \case
CRActiveUser User {profile, uiThemes} -> viewUserProfile (fromLocalProfile profile) <> viewUITheme uiThemes CRActiveUser User {profile, uiThemes} -> viewUserProfile (fromLocalProfile profile) <> viewUITheme uiThemes
CRUsersList users -> viewUsersList users CRUsersList users -> viewUsersList users
CRChatStarted -> ["chat started"] CRChatStarted -> ["chat started"]
@ -108,7 +108,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRUserServersValidation {} -> [] CRUserServersValidation {} -> []
CRUsageConditions current _ accepted_ -> viewUsageConditions current accepted_ CRUsageConditions current _ accepted_ -> viewUsageConditions current accepted_
CRChatItemTTL u ttl -> ttyUser u $ viewChatItemTTL ttl CRChatItemTTL u ttl -> ttyUser u $ viewChatItemTTL ttl
CRNetworkConfig cfg -> viewNetworkConfig cfg CRNetworkConfig netCfg -> viewNetworkConfig netCfg
CRContactInfo u ct cStats customUserProfile -> ttyUser u $ viewContactInfo ct cStats customUserProfile CRContactInfo u ct cStats customUserProfile -> ttyUser u $ viewContactInfo ct cStats customUserProfile
CRGroupInfo u g s -> ttyUser u $ viewGroupInfo g s CRGroupInfo u g s -> ttyUser u $ viewGroupInfo g s
CRGroupMemberInfo u g m cStats -> ttyUser u $ viewGroupMemberInfo g m cStats CRGroupMemberInfo u g m cStats -> ttyUser u $ viewGroupMemberInfo g m cStats
@ -181,7 +181,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
HSDatabase -> databaseHelpInfo HSDatabase -> databaseHelpInfo
CRWelcome user -> chatWelcome user CRWelcome user -> chatWelcome user
CRContactsList u cs -> ttyUser u $ viewContactsList cs CRContactsList u cs -> ttyUser u $ viewContactsList cs
CRUserContactLink u UserContactLink {connReqContact, autoAccept} -> ttyUser u $ connReqContact_ "Your chat address:" connReqContact <> autoAcceptStatus_ autoAccept CRUserContactLink u UserContactLink {connLinkContact, autoAccept} -> ttyUser u $ connReqContact_ "Your chat address:" connLinkContact <> autoAcceptStatus_ autoAccept
CRUserContactLinkUpdated u UserContactLink {autoAccept} -> ttyUser u $ autoAcceptStatus_ autoAccept CRUserContactLinkUpdated u UserContactLink {autoAccept} -> ttyUser u $ autoAcceptStatus_ autoAccept
CRContactRequestRejected u UserContactRequest {localDisplayName = c} -> ttyUser u [ttyContact c <> ": contact request rejected"] CRContactRequestRejected u UserContactRequest {localDisplayName = c} -> ttyUser u [ttyContact c <> ": contact request rejected"]
CRGroupCreated u g -> ttyUser u $ viewGroupCreated g testView CRGroupCreated u g -> ttyUser u $ viewGroupCreated g testView
@ -200,10 +200,10 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRUserProfileNoChange u -> ttyUser u ["user profile did not change"] CRUserProfileNoChange u -> ttyUser u ["user profile did not change"]
CRUserPrivacy u u' -> ttyUserPrefix u $ viewUserPrivacy u u' CRUserPrivacy u u' -> ttyUserPrefix u $ viewUserPrivacy u u'
CRVersionInfo info _ _ -> viewVersionInfo logLevel info CRVersionInfo info _ _ -> viewVersionInfo logLevel info
CRInvitation u cReq _ -> ttyUser u $ viewConnReqInvitation cReq CRInvitation u ccLink _ -> ttyUser u $ viewConnReqInvitation ccLink
CRConnectionIncognitoUpdated u c -> ttyUser u $ viewConnectionIncognitoUpdated c CRConnectionIncognitoUpdated u c -> ttyUser u $ viewConnectionIncognitoUpdated c
CRConnectionUserChanged u c c' nu -> ttyUser u $ viewConnectionUserChanged u c nu c' CRConnectionUserChanged u c c' nu -> ttyUser u $ viewConnectionUserChanged u c nu c'
CRConnectionPlan u connectionPlan -> ttyUser u $ viewConnectionPlan connectionPlan CRConnectionPlan u _ connectionPlan -> ttyUser u $ viewConnectionPlan cfg connectionPlan
CRSentConfirmation u _ -> ttyUser u ["confirmation sent!"] CRSentConfirmation u _ -> ttyUser u ["confirmation sent!"]
CRSentInvitation u _ customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView CRSentInvitation u _ customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView
CRSentInvitationToContact u _c customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView CRSentInvitationToContact u _c customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView
@ -215,7 +215,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRContactAlreadyExists u c -> ttyUser u [ttyFullContact c <> ": contact already exists"] CRContactAlreadyExists u c -> ttyUser u [ttyFullContact c <> ": contact already exists"]
CRContactRequestAlreadyAccepted u c -> ttyUser u [ttyFullContact c <> ": sent you a duplicate contact request, but you are already connected, no action needed"] CRContactRequestAlreadyAccepted u c -> ttyUser u [ttyFullContact c <> ": sent you a duplicate contact request, but you are already connected, no action needed"]
CRBusinessRequestAlreadyAccepted u g -> ttyUser u [ttyFullGroup g <> ": sent you a duplicate connection request, but you are already connected, no action needed"] CRBusinessRequestAlreadyAccepted u g -> ttyUser u [ttyFullGroup g <> ": sent you a duplicate connection request, but you are already connected, no action needed"]
CRUserContactLinkCreated u cReq -> ttyUser u $ connReqContact_ "Your new chat address is created!" cReq CRUserContactLinkCreated u ccLink -> ttyUser u $ connReqContact_ "Your new chat address is created!" ccLink
CRUserContactLinkDeleted u -> ttyUser u viewUserContactLinkDeleted CRUserContactLinkDeleted u -> ttyUser u viewUserContactLinkDeleted
CRUserAcceptedGroupSent u _g _ -> ttyUser u [] -- [ttyGroup' g <> ": joining the group..."] CRUserAcceptedGroupSent u _g _ -> ttyUser u [] -- [ttyGroup' g <> ": joining the group..."]
CRGroupLinkConnecting u g _ -> ttyUser u [ttyGroup' g <> ": joining the group..."] CRGroupLinkConnecting u g _ -> ttyUser u [ttyGroup' g <> ": joining the group..."]
@ -314,8 +314,8 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe
CRGroupUpdated u g g' m -> ttyUser u $ viewGroupUpdated g g' m CRGroupUpdated u g g' m -> ttyUser u $ viewGroupUpdated g g' m
CRGroupProfile u g -> ttyUser u $ viewGroupProfile g CRGroupProfile u g -> ttyUser u $ viewGroupProfile g
CRGroupDescription u g -> ttyUser u $ viewGroupDescription g CRGroupDescription u g -> ttyUser u $ viewGroupDescription g
CRGroupLinkCreated u g cReq mRole -> ttyUser u $ groupLink_ "Group link is created!" g cReq mRole CRGroupLinkCreated u g ccLink mRole -> ttyUser u $ groupLink_ "Group link is created!" g ccLink mRole
CRGroupLink u g cReq mRole -> ttyUser u $ groupLink_ "Group link:" g cReq mRole CRGroupLink u g ccLink mRole -> ttyUser u $ groupLink_ "Group link:" g ccLink mRole
CRGroupLinkDeleted u g -> ttyUser u $ viewGroupLinkDeleted g CRGroupLinkDeleted u g -> ttyUser u $ viewGroupLinkDeleted g
CRAcceptingGroupJoinRequestMember _ g m -> [ttyFullMember m <> ": accepting request to join group " <> ttyGroup' g <> "..."] CRAcceptingGroupJoinRequestMember _ g m -> [ttyFullMember m <> ": accepting request to join group " <> ttyGroup' g <> "..."]
CRNoMemberContactCreating u g m -> ttyUser u ["member " <> ttyGroup' g <> " " <> ttyMember m <> " does not have direct connection, creating"] CRNoMemberContactCreating u g m -> ttyUser u ["member " <> ttyGroup' g <> " " <> ttyMember m <> " does not have direct connection, creating"]
@ -911,14 +911,17 @@ viewInvalidConnReq =
plain updateStr plain updateStr
] ]
viewConnReqInvitation :: ConnReqInvitation -> [StyledString] viewConnReqInvitation :: CreatedLinkInvitation -> [StyledString]
viewConnReqInvitation cReq = viewConnReqInvitation (CCLink cReq shortLink) =
[ "pass this invitation link to your contact (via another channel): ", [ "pass this invitation link to your contact (via another channel): ",
"", "",
(plain . strEncode) (simplexChatInvitation cReq), plain $ maybe cReqStr strEncode shortLink,
"", "",
"and ask them to connect: " <> highlight' "/c <invitation_link_above>" "and ask them to connect: " <> highlight' "/c <invitation_link_above>"
] ]
<> ["The invitation link for old clients: " <> plain cReqStr | isJust shortLink]
where
cReqStr = strEncode $ simplexChatInvitation cReq
simplexChatInvitation :: ConnReqInvitation -> ConnReqInvitation simplexChatInvitation :: ConnReqInvitation -> ConnReqInvitation
simplexChatInvitation (CRInvitationUri crData e2e) = CRInvitationUri crData {crScheme = simplexChat} e2e simplexChatInvitation (CRInvitationUri crData e2e) = CRInvitationUri crData {crScheme = simplexChat} e2e
@ -973,21 +976,29 @@ viewForwardPlan count itemIds = maybe [forwardCount] $ \fc -> [confirmation fc,
| otherwise = plain $ show len <> " message(s) out of " <> show count <> " can be forwarded" | otherwise = plain $ show len <> " message(s) out of " <> show count <> " can be forwarded"
len = length itemIds len = length itemIds
connReqContact_ :: StyledString -> ConnReqContact -> [StyledString] connReqContact_ :: StyledString -> CreatedLinkContact -> [StyledString]
connReqContact_ intro cReq = connReqContact_ intro (CCLink cReq shortLink) =
[ intro, [ intro,
"", "",
(plain . strEncode) (simplexChatContact cReq), plain $ maybe cReqStr strEncode shortLink,
"", "",
"Anybody can send you contact requests with: " <> highlight' "/c <contact_link_above>", "Anybody can send you contact requests with: " <> highlight' "/c <contact_link_above>",
"to show it again: " <> highlight' "/sa", "to show it again: " <> highlight' "/sa",
"to share with your contacts: " <> highlight' "/profile_address on", "to share with your contacts: " <> highlight' "/profile_address on",
"to delete it: " <> highlight' "/da" <> " (accepted contacts will remain connected)" "to delete it: " <> highlight' "/da" <> " (accepted contacts will remain connected)"
] ]
<> ["The contact link for old clients: " <> plain cReqStr | isJust shortLink]
where
cReqStr = strEncode $ simplexChatContact cReq
simplexChatContact :: ConnReqContact -> ConnReqContact simplexChatContact :: ConnReqContact -> ConnReqContact
simplexChatContact (CRContactUri crData) = CRContactUri crData {crScheme = simplexChat} simplexChatContact (CRContactUri crData) = CRContactUri crData {crScheme = simplexChat}
simplexChatContact' :: ConnLinkContact -> ConnLinkContact
simplexChatContact' = \case
CLFull (CRContactUri crData) -> CLFull $ CRContactUri crData {crScheme = simplexChat}
l@(CLShort _) -> l
autoAcceptStatus_ :: Maybe AutoAccept -> [StyledString] autoAcceptStatus_ :: Maybe AutoAccept -> [StyledString]
autoAcceptStatus_ = \case autoAcceptStatus_ = \case
Just AutoAccept {businessAddress, acceptIncognito, autoReply} -> Just AutoAccept {businessAddress, acceptIncognito, autoReply} ->
@ -1000,16 +1011,19 @@ autoAcceptStatus_ = \case
| otherwise = "" | otherwise = ""
_ -> ["auto_accept off"] _ -> ["auto_accept off"]
groupLink_ :: StyledString -> GroupInfo -> ConnReqContact -> GroupMemberRole -> [StyledString] groupLink_ :: StyledString -> GroupInfo -> CreatedLinkContact -> GroupMemberRole -> [StyledString]
groupLink_ intro g cReq mRole = groupLink_ intro g (CCLink cReq shortLink) mRole =
[ intro, [ intro,
"", "",
(plain . strEncode) (simplexChatContact cReq), plain $ maybe cReqStr strEncode shortLink,
"", "",
"Anybody can connect to you and join group as " <> showRole mRole <> " with: " <> highlight' "/c <group_link_above>", "Anybody can connect to you and join group as " <> showRole mRole <> " with: " <> highlight' "/c <group_link_above>",
"to show it again: " <> highlight ("/show link #" <> viewGroupName g), "to show it again: " <> highlight ("/show link #" <> viewGroupName g),
"to delete it: " <> highlight ("/delete link #" <> viewGroupName g) <> " (joined members will remain connected to you)" "to delete it: " <> highlight ("/delete link #" <> viewGroupName g) <> " (joined members will remain connected to you)"
] ]
<> ["The group link for old clients: " <> plain cReqStr | isJust shortLink]
where
cReqStr = strEncode $ simplexChatContact cReq
viewGroupLinkDeleted :: GroupInfo -> [StyledString] viewGroupLinkDeleted :: GroupInfo -> [StyledString]
viewGroupLinkDeleted g = viewGroupLinkDeleted g =
@ -1405,7 +1419,7 @@ viewContactInfo :: Contact -> Maybe ConnectionStats -> Maybe Profile -> [StyledS
viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn, uiThemes, customData} stats incognitoProfile = viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn, uiThemes, customData} stats incognitoProfile =
["contact ID: " <> sShow contactId] ["contact ID: " <> sShow contactId]
<> maybe [] viewConnectionStats stats <> maybe [] viewConnectionStats stats
<> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact l)]) contactLink <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact' l)]) contactLink
<> maybe <> maybe
["you've shared main profile with this contact"] ["you've shared main profile with this contact"]
(\p -> ["you've shared incognito profile with this contact: " <> incognitoProfile' p]) (\p -> ["you've shared incognito profile with this contact: " <> incognitoProfile' p])
@ -1437,7 +1451,7 @@ viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProf
"member ID: " <> sShow groupMemberId "member ID: " <> sShow groupMemberId
] ]
<> maybe ["member not connected"] viewConnectionStats stats <> maybe ["member not connected"] viewConnectionStats stats
<> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact l)]) contactLink <> maybe [] (\l -> ["contact address: " <> (plain . strEncode) (simplexChatContact' l)]) contactLink
<> ["alias: " <> plain localAlias | localAlias /= ""] <> ["alias: " <> plain localAlias | localAlias /= ""]
<> [viewConnectionVerified (memberSecurityCode m) | isJust stats] <> [viewConnectionVerified (memberSecurityCode m) | isJust stats]
<> maybe [] (\ac -> [viewPeerChatVRange (peerChatVRange ac)]) activeConn <> maybe [] (\ac -> [viewPeerChatVRange (peerChatVRange ac)]) activeConn
@ -1663,21 +1677,24 @@ viewConnectionIncognitoUpdated PendingContactConnection {pccConnId, customUserPr
| otherwise = ["connection " <> sShow pccConnId <> " changed to non incognito"] | otherwise = ["connection " <> sShow pccConnId <> " changed to non incognito"]
viewConnectionUserChanged :: User -> PendingContactConnection -> User -> PendingContactConnection -> [StyledString] viewConnectionUserChanged :: User -> PendingContactConnection -> User -> PendingContactConnection -> [StyledString]
viewConnectionUserChanged User {localDisplayName = n} PendingContactConnection {pccConnId, connReqInv} User {localDisplayName = n'} PendingContactConnection {connReqInv = connReqInv'} = viewConnectionUserChanged User {localDisplayName = n} PendingContactConnection {pccConnId, connLinkInv} User {localDisplayName = n'} PendingContactConnection {connLinkInv = connLinkInv'} =
case (connReqInv, connReqInv') of case (connLinkInv, connLinkInv') of
(Just cReqInv, Just cReqInv') (Just ccLink, Just ccLink')
| cReqInv /= cReqInv' -> [userChangedStr <> ", new link:"] <> newLink cReqInv' | ccLink /= ccLink' -> [userChangedStr <> ", new link:"] <> newLink ccLink'
_ -> [userChangedStr] _ -> [userChangedStr]
where where
userChangedStr = "connection " <> sShow pccConnId <> " changed from user " <> plain n <> " to user " <> plain n' userChangedStr = "connection " <> sShow pccConnId <> " changed from user " <> plain n <> " to user " <> plain n'
newLink cReqInv = newLink (CCLink cReq shortLink) =
[ "", [ "",
(plain . strEncode) (simplexChatInvitation cReqInv), plain $ maybe cReqStr strEncode shortLink,
"" ""
] ]
<> ["The invitation link for old clients: " <> plain cReqStr | isJust shortLink]
where
cReqStr = strEncode $ simplexChatInvitation cReq
viewConnectionPlan :: ConnectionPlan -> [StyledString] viewConnectionPlan :: ChatConfig -> ConnectionPlan -> [StyledString]
viewConnectionPlan = \case viewConnectionPlan ChatConfig {logLevel, testView} = \case
CPInvitationLink ilp -> case ilp of CPInvitationLink ilp -> case ilp of
ILPOk -> [invLink "ok to connect"] ILPOk -> [invLink "ok to connect"]
ILPOwnLink -> [invLink "own link"] ILPOwnLink -> [invLink "own link"]
@ -1716,6 +1733,7 @@ viewConnectionPlan = \case
grpOrBiz GroupInfo {businessChat} = case businessChat of grpOrBiz GroupInfo {businessChat} = case businessChat of
Just _ -> "business" Just _ -> "business"
Nothing -> "group" Nothing -> "group"
CPError e -> viewChatError False logLevel testView e
viewContactUpdated :: Contact -> Contact -> [StyledString] viewContactUpdated :: Contact -> Contact -> [StyledString]
viewContactUpdated viewContactUpdated
@ -2138,8 +2156,8 @@ viewChatError isCmd logLevel testView = \case
CEChatNotStarted -> ["error: chat not started"] CEChatNotStarted -> ["error: chat not started"]
CEChatNotStopped -> ["error: chat not stopped"] CEChatNotStopped -> ["error: chat not stopped"]
CEChatStoreChanged -> ["error: chat store changed, please restart chat"] CEChatStoreChanged -> ["error: chat store changed, please restart chat"]
CEConnectionPlan connectionPlan -> viewConnectionPlan connectionPlan
CEInvalidConnReq -> viewInvalidConnReq CEInvalidConnReq -> viewInvalidConnReq
CEUnsupportedConnReq -> [ "", "Connection link is not supported by the your app version, please ugrade it.", plain updateStr]
CEInvalidChatMessage Connection {connId} msgMeta_ msg e -> CEInvalidChatMessage Connection {connId} msgMeta_ msg e ->
[ plain $ [ plain $
("chat message error: " <> e <> " (" <> T.unpack (T.take 120 msg) <> ")") ("chat message error: " <> e <> " (" <> T.unpack (T.take 120 msg) <> ")")

View file

@ -186,6 +186,7 @@ testCfg =
defaultChatConfig defaultChatConfig
{ agentConfig = testAgentCfg, { agentConfig = testAgentCfg,
showReceipts = False, showReceipts = False,
shortLinkPresetServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=@localhost:7001"],
testView = True, testView = True,
tbqSize = 16 tbqSize = 16
} }
@ -290,8 +291,8 @@ startTestChat_ TestParams {printOutput} db cfg opts@ChatOpts {maintenance} user
ct <- newChatTerminal t opts ct <- newChatTerminal t opts
cc <- newChatController db (Just user) cfg opts False cc <- newChatController db (Just user) cfg opts False
void $ execChatCommand' (SetTempFolder "tests/tmp/tmp") `runReaderT` cc void $ execChatCommand' (SetTempFolder "tests/tmp/tmp") `runReaderT` cc
chatAsync <- async . runSimplexChat opts user cc $ \_u cc' -> runChatTerminal ct cc' opts chatAsync <- async $ runSimplexChat opts user cc $ \_u cc' -> runChatTerminal ct cc' opts
atomically . unless maintenance $ readTVar (agentAsync cc) >>= \a -> when (isNothing a) retry unless maintenance $ atomically $ readTVar (agentAsync cc) >>= \a -> when (isNothing a) retry
termQ <- newTQueueIO termQ <- newTQueueIO
termAsync <- async $ readTerminalOutput t termQ termAsync <- async $ readTerminalOutput t termQ
pure TestCC {chatController = cc, virtualTerminal = t, chatAsync, termAsync, termQ, printOutput} pure TestCC {chatController = cc, virtualTerminal = t, chatAsync, termAsync, termQ, printOutput}
@ -393,14 +394,14 @@ withTmpFiles =
testChatN :: HasCallStack => ChatConfig -> ChatOpts -> [Profile] -> (HasCallStack => [TestCC] -> IO ()) -> TestParams -> IO () testChatN :: HasCallStack => ChatConfig -> ChatOpts -> [Profile] -> (HasCallStack => [TestCC] -> IO ()) -> TestParams -> IO ()
testChatN cfg opts ps test params = testChatN cfg opts ps test params =
bracket (getTestCCs (zip ps [1 ..]) []) entTests test bracket (getTestCCs $ zip ps [1 ..]) endTests test
where where
getTestCCs :: [(Profile, Int)] -> [TestCC] -> IO [TestCC] getTestCCs :: [(Profile, Int)] -> IO [TestCC]
getTestCCs [] tcs = pure tcs getTestCCs [] = pure []
getTestCCs ((p, db) : envs') tcs = (:) <$> createTestChat params cfg opts (show db) p <*> getTestCCs envs' tcs getTestCCs ((p, db) : envs') = (:) <$> createTestChat params cfg opts (show db) p <*> getTestCCs envs'
entTests tcs = do endTests tcs = do
concurrentlyN_ $ map (<// 100000) tcs mapConcurrently_ (<// 100000) tcs
concurrentlyN_ $ map (stopTestChat params) tcs mapConcurrently_ (stopTestChat params) tcs
(<//) :: HasCallStack => TestCC -> Int -> Expectation (<//) :: HasCallStack => TestCC -> Int -> Expectation
(<//) cc t = timeout t (getTermLine cc) `shouldReturn` Nothing (<//) cc t = timeout t (getTermLine cc) `shouldReturn` Nothing
@ -481,7 +482,7 @@ smpServerCfg =
msgQueueQuota = 16, msgQueueQuota = 16,
maxJournalMsgCount = 24, maxJournalMsgCount = 24,
maxJournalStateLines = 4, maxJournalStateLines = 4,
queueIdBytes = 12, queueIdBytes = 24,
msgIdBytes = 6, msgIdBytes = 6,
serverStoreCfg = ASSCfg SQSMemory SMSMemory $ SSCMemory Nothing, serverStoreCfg = ASSCfg SQSMemory SMSMemory $ SSCMemory Nothing,
storeNtfsFile = Nothing, storeNtfsFile = Nothing,

View file

@ -1113,6 +1113,8 @@ testSendMultiManyBatches =
DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdBob) :: IO [[Int]] DB.query db "SELECT count(1) FROM chat_items WHERE chat_item_id > ?" (Only msgIdBob) :: IO [[Int]]
bobItemsCount `shouldBe` [[300]] bobItemsCount `shouldBe` [[300]]
threadDelay 1000000
testGetSetSMPServers :: HasCallStack => TestParams -> IO () testGetSetSMPServers :: HasCallStack => TestParams -> IO ()
testGetSetSMPServers = testGetSetSMPServers =
testChat aliceProfile $ testChat aliceProfile $

View file

@ -101,6 +101,11 @@ chatProfileTests = do
it "files & media" testGroupPrefsFilesForRole it "files & media" testGroupPrefsFilesForRole
it "SimpleX links" testGroupPrefsSimplexLinksForRole it "SimpleX links" testGroupPrefsSimplexLinksForRole
it "set user, contact and group UI theme" testSetUITheme 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 contact address" testShortLinkContactAddress
it "should join group" testShortLinkJoinGroup
testUpdateProfile :: HasCallStack => TestParams -> IO () testUpdateProfile :: HasCallStack => TestParams -> IO ()
testUpdateProfile = testUpdateProfile =
@ -2583,3 +2588,162 @@ testSetUITheme =
groupInfo a = do groupInfo a = do
a <## "group ID: 1" a <## "group ID: 1"
a <## "current members: 1" a <## "current members: 1"
testShortLinkInvitation :: HasCallStack => TestParams -> IO ()
testShortLinkInvitation =
testChat2 aliceProfile bobProfile $ \alice bob -> do
alice ##> "/c short"
inv <- getShortInvitation alice
bob ##> ("/c " <> inv)
bob <## "confirmation sent!"
concurrently_
(alice <## "bob (Bob): contact is connected")
(bob <## "alice (Alice): contact is connected")
alice #> "@bob hi"
bob <# "alice> hi"
bob #> "@alice hey"
alice <# "bob> hey"
testPlanShortLinkInvitation :: HasCallStack => TestParams -> IO ()
testPlanShortLinkInvitation =
testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do
alice ##> "/c short"
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"
-- nobody else can connect
cath ##> ("/_connect plan 1 " <> inv)
cath <##. "error: connection authorization failed"
cath ##> ("/c " <> inv)
cath <##. "error: connection authorization failed"
-- bob can retry "plan"
bob ##> ("/_connect plan 1 " <> inv)
bob <## "invitation link: ok to connect"
-- with simplex: scheme too
bob ##> ("/_connect plan 1 " <> slSimplexScheme inv)
bob <## "invitation link: ok to connect"
bob ##> ("/c " <> inv)
bob <## "confirmation sent!"
concurrently_
(alice <## "bob (Bob): contact is connected")
(bob <## "alice (Alice): contact is connected")
alice #> "@bob hi"
bob <# "alice> hi"
bob #> "@alice hey"
alice <# "bob> hey"
bob ##> ("/_connect plan 1 " <> inv)
bob <##. "error: connection authorization failed"
alice ##> ("/_connect plan 1 " <> inv)
alice <##. "error: connection authorization failed" -- short_link_inv and conn_req_inv are removed after connection
slSimplexScheme :: String -> String
slSimplexScheme sl = T.unpack $ T.replace "https://localhost/" "simplex:/" (T.pack sl) <> "?h=localhost"
testShortLinkContactAddress :: HasCallStack => TestParams -> IO ()
testShortLinkContactAddress =
testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do
alice ##> "/ad short"
(shortLink, fullLink) <- getShortContactLink alice True
alice ##> ("/_connect plan 1 " <> shortLink)
alice <## "contact address: own address"
alice ##> ("/_connect plan 1 " <> slSimplexScheme shortLink)
alice <## "contact address: own address"
alice ##> ("/_connect plan 1 " <> fullLink)
alice <## "contact address: own address"
(alice, bob) `connectVia` shortLink
bob ##> ("/_connect plan 1 " <> slSimplexScheme shortLink)
bob <## "contact address: known contact alice"
bob <## "use @alice <message> to send messages"
(alice, cath) `connectVia` slSimplexScheme shortLink
cath ##> ("/_connect plan 1 " <> shortLink)
cath <## "contact address: known contact alice"
cath <## "use @alice <message> to send messages"
(alice, dan) `connectVia` fullLink
where
(alice, cc) `connectVia` cLink = do
name <- userName cc
sName <- showName cc
cc ##> ("/_connect plan 1 " <> cLink)
cc <## "contact address: ok to connect"
cc ##> ("/c " <> cLink)
alice <#? cc
alice ##> ("/ac " <> name)
alice <## (sName <> ": accepting contact request, you can send messages to contact")
concurrently_
(cc <## "alice (Alice): contact is connected")
(alice <## (sName <> ": contact is connected"))
cc ##> ("/_connect plan 1 " <> cLink)
cc <## "contact address: known contact alice"
cc <## "use @alice <message> to send messages"
testShortLinkJoinGroup :: HasCallStack => TestParams -> IO ()
testShortLinkJoinGroup =
testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do
threadDelay 100000
alice ##> "/ad short" -- create the address to test that it can co-exist with group link
_ <- getShortContactLink alice True
alice ##> "/g team"
alice <## "group #team is created"
alice <## "to add members use /a team <name> or /create link #team"
alice ##> "/create link #team short"
(shortLink, fullLink) <- getShortGroupLink alice "team" GRMember True
alice ##> ("/_connect plan 1 " <> shortLink)
alice <## "group link: own link for group #team"
alice ##> ("/_connect plan 1 " <> slSimplexScheme shortLink)
alice <## "group link: own link for group #team"
alice ##> ("/_connect plan 1 " <> fullLink)
alice <## "group link: own link for group #team"
joinGroup alice bob shortLink
bob ##> ("/_connect plan 1 " <> shortLink)
bob <## "group link: known group #team"
bob <## "use #team <message> to send messages"
bob ##> ("/_connect plan 1 " <> slSimplexScheme shortLink)
bob <## "group link: known group #team"
bob <## "use #team <message> to send messages"
joinGroup alice cath $ slSimplexScheme shortLink
concurrentlyN_
[ do
bob <## "#team: alice added cath (Catherine) to the group (connecting...)"
bob <## "#team: new member cath is connected",
cath <## "#team: member bob (Bob) is connected"
]
cath ##> ("/_connect plan 1 " <> slSimplexScheme shortLink)
cath <## "group link: known group #team"
cath <## "use #team <message> to send messages"
cath ##> ("/_connect plan 1 " <> shortLink)
cath <## "group link: known group #team"
cath <## "use #team <message> to send messages"
joinGroup alice dan fullLink
concurrentlyN_
[ do
bob <## "#team: alice added dan (Daniel) to the group (connecting...)"
bob <## "#team: new member dan is connected",
do
cath <## "#team: alice added dan (Daniel) to the group (connecting...)"
cath <## "#team: new member dan is connected",
do
dan <## "#team: member bob (Bob) is connected"
dan <## "#team: member cath (Catherine) is connected"
]
dan ##> ("/_connect plan 1 " <> fullLink)
dan <## "group link: known group #team"
dan <## "use #team <message> to send messages"
where
joinGroup alice cc link = do
name <- userName cc
sName <- showName cc
cc ##> ("/_connect plan 1 " <> link)
cc <## "group link: ok to connect"
cc ##> ("/c " <> link)
cc <## "connection request sent!"
alice <## (sName <> ": accepting request to join group #team...")
concurrentlyN_
[ alice <## ("#team: " <> name <> " joined the group"),
do
cc <## "#team: joining the group..."
cc <## "#team: you joined the group"
]

View file

@ -11,7 +11,7 @@ module ChatTests.Utils where
import ChatClient import ChatClient
import ChatTests.DBUtils import ChatTests.DBUtils
import Control.Concurrent (threadDelay) import Control.Concurrent (threadDelay)
import Control.Concurrent.Async (concurrently_) import Control.Concurrent.Async (concurrently_, mapConcurrently_)
import Control.Concurrent.STM import Control.Concurrent.STM
import Control.Monad (unless, when) import Control.Monad (unless, when)
import Control.Monad.Except (runExceptT) import Control.Monad.Except (runExceptT)
@ -424,7 +424,7 @@ getInAnyOrder f cc ls = do
cc <# line = (dropTime <$> getTermLine cc) `shouldReturn` line cc <# line = (dropTime <$> getTermLine cc) `shouldReturn` line
(*<#) :: HasCallStack => [TestCC] -> String -> Expectation (*<#) :: HasCallStack => [TestCC] -> String -> Expectation
ccs *<# line = concurrentlyN_ $ map (<# line) ccs ccs *<# line = mapConcurrently_ (<# line) ccs
(?<#) :: HasCallStack => TestCC -> String -> Expectation (?<#) :: HasCallStack => TestCC -> String -> Expectation
cc ?<# line = (dropTime <$> getTermLine cc) `shouldReturn` "i " <> line cc ?<# line = (dropTime <$> getTermLine cc) `shouldReturn` "i " <> line
@ -502,14 +502,27 @@ dropPartialReceipt_ msg = case splitAt 2 msg of
_ -> Nothing _ -> Nothing
getInvitation :: HasCallStack => TestCC -> IO String getInvitation :: HasCallStack => TestCC -> IO String
getInvitation cc = do getInvitation = getInvitation_ False
getShortInvitation :: HasCallStack => TestCC -> IO String
getShortInvitation = getInvitation_ True
getInvitation_ :: HasCallStack => Bool -> TestCC -> IO String
getInvitation_ short cc = do
cc <## "pass this invitation link to your contact (via another channel):" cc <## "pass this invitation link to your contact (via another channel):"
cc <## "" cc <## ""
inv <- getTermLine cc inv <- getTermLine cc
cc <## "" cc <## ""
cc <## "and ask them to connect: /c <invitation_link_above>" cc <## "and ask them to connect: /c <invitation_link_above>"
when short $ cc <##. "The invitation link for old clients: https://simplex.chat/invitation#"
pure inv pure inv
getShortContactLink :: HasCallStack => TestCC -> Bool -> IO (String, String)
getShortContactLink cc created = do
shortLink <- getContactLink cc created
fullLink <- dropLinePrefix "The contact link for old clients: " =<< getTermLine cc
pure (shortLink, fullLink)
getContactLink :: HasCallStack => TestCC -> Bool -> IO String getContactLink :: HasCallStack => TestCC -> Bool -> IO String
getContactLink cc created = do getContactLink cc created = do
cc <## if created then "Your new chat address is created!" else "Your chat address:" cc <## if created then "Your new chat address is created!" else "Your chat address:"
@ -522,6 +535,17 @@ getContactLink cc created = do
cc <## "to delete it: /da (accepted contacts will remain connected)" cc <## "to delete it: /da (accepted contacts will remain connected)"
pure link pure link
dropLinePrefix :: String -> String -> IO String
dropLinePrefix line s
| line `isPrefixOf` s = pure $ drop (length line) s
| otherwise = error $ "expected to start from: " <> line <> ", got: " <> s
getShortGroupLink :: HasCallStack => TestCC -> String -> GroupMemberRole -> Bool -> IO (String, String)
getShortGroupLink cc gName mRole created = do
shortLink <- getGroupLink cc gName mRole created
fullLink <- dropLinePrefix "The group link for old clients: " =<< getTermLine cc
pure (shortLink, fullLink)
getGroupLink :: HasCallStack => TestCC -> String -> GroupMemberRole -> Bool -> IO String getGroupLink :: HasCallStack => TestCC -> String -> GroupMemberRole -> Bool -> IO String
getGroupLink cc gName mRole created = do getGroupLink cc gName mRole created = do
cc <## if created then "Group link is created!" else "Group link:" cc <## if created then "Group link is created!" else "Group link:"

View file

@ -19,6 +19,7 @@ import qualified Data.List.NonEmpty as L
import Simplex.Chat import Simplex.Chat
import Simplex.Chat.Controller (ChatConfig (..), PresetServers (..)) import Simplex.Chat.Controller (ChatConfig (..), PresetServers (..))
import Simplex.Chat.Operators import Simplex.Chat.Operators
import Simplex.Chat.Operators.Presets
import Simplex.Chat.Types import Simplex.Chat.Types
import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) import Simplex.FileTransfer.Client.Presets (defaultXFTPServers)
import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..), allRoles) import Simplex.Messaging.Agent.Env.SQLite (ServerRoles (..), allRoles)

View file

@ -35,7 +35,7 @@ queue =
{ smpServer = srv, { smpServer = srv,
senderId = EntityId "\223\142z\251", senderId = EntityId "\223\142z\251",
dhPublicKey = "MCowBQYDK2VuAyEAjiswwI3O/NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o=", dhPublicKey = "MCowBQYDK2VuAyEAjiswwI3O/NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o=",
sndSecure = False queueMode = Nothing
} }
connReqData :: ConnReqUriData connReqData :: ConnReqUriData
@ -201,7 +201,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"v\":\"1\",\"event\":\"x.msg.deleted\",\"params\":{}}" "{\"v\":\"1\",\"event\":\"x.msg.deleted\",\"params\":{}}"
#==# XMsgDeleted #==# XMsgDeleted
it "x.file" $ it "x.file" $
"{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" "{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
#==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Just testConnReq, fileInline = Nothing, fileDescr = Nothing} #==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Just testConnReq, fileInline = Nothing, fileDescr = Nothing}
it "x.file without file invitation" $ it "x.file without file invitation" $
"{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" "{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}"
@ -210,7 +210,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"v\":\"1\",\"event\":\"x.file.acpt\",\"params\":{\"fileName\":\"photo.jpg\"}}" "{\"v\":\"1\",\"event\":\"x.file.acpt\",\"params\":{\"fileName\":\"photo.jpg\"}}"
#==# XFileAcpt "photo.jpg" #==# XFileAcpt "photo.jpg"
it "x.file.acpt.inv" $ it "x.file.acpt.inv" $
"{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" "{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}"
#==# XFileAcptInv (SharedMsgId "\1\2\3\4") (Just testConnReq) "photo.jpg" #==# XFileAcptInv (SharedMsgId "\1\2\3\4") (Just testConnReq) "photo.jpg"
it "x.file.acpt.inv" $ it "x.file.acpt.inv" $
"{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\"}}" "{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\"}}"
@ -237,10 +237,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" "{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
==# XContact testProfile Nothing ==# XContact testProfile Nothing
it "x.grp.inv" $ it "x.grp.inv" $
"{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}" "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}"
#==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, business = Nothing, groupLinkId = Nothing, groupSize = Nothing} #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, business = Nothing, groupLinkId = Nothing, groupSize = Nothing}
it "x.grp.inv with group link id" $ it "x.grp.inv with group link id" $
"{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}" "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}"
#==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, business = Nothing, groupLinkId = Just $ GroupLinkId "\1\2\3\4", groupSize = Nothing} #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, business = Nothing, groupLinkId = Just $ GroupLinkId "\1\2\3\4", groupSize = Nothing}
it "x.grp.acpt without incognito profile" $ it "x.grp.acpt without incognito profile" $
"{\"v\":\"1\",\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}" "{\"v\":\"1\",\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}"
@ -261,16 +261,16 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberRestrictions\":{\"restriction\":\"blocked\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberRestrictions\":{\"restriction\":\"blocked\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} (Just MemberRestrictions {restriction = MRSBlocked}) #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} (Just MemberRestrictions {restriction = MRSBlocked})
it "x.grp.mem.inv" $ it "x.grp.mem.inv" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}"
#==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq} #==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq}
it "x.grp.mem.inv w/t directConnReq" $ it "x.grp.mem.inv w/t directConnReq" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}"
#==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing} #==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing}
it "x.grp.mem.fwd" $ it "x.grp.mem.fwd" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq} #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq}
it "x.grp.mem.fwd with member chat version range and w/t directConnReq" $ it "x.grp.mem.fwd with member chat version range and w/t directConnReq" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-14\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-14\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing} #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing}
it "x.grp.mem.info" $ it "x.grp.mem.info" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" "{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}"
@ -291,10 +291,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"v\":\"1\",\"event\":\"x.grp.del\",\"params\":{}}" "{\"v\":\"1\",\"event\":\"x.grp.del\",\"params\":{}}"
==# XGrpDel ==# XGrpDel
it "x.grp.direct.inv" $ it "x.grp.direct.inv" $
"{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
#==# XGrpDirectInv testConnReq (Just $ MCText "hello") #==# XGrpDirectInv testConnReq (Just $ MCText "hello")
it "x.grp.direct.inv without content" $ it "x.grp.direct.inv without content" $
"{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-4%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}"
#==# XGrpDirectInv testConnReq Nothing #==# XGrpDirectInv testConnReq Nothing
-- it "x.grp.msg.forward" -- it "x.grp.msg.forward"
-- $ "{\"v\":\"1\",\"event\":\"x.grp.msg.forward\",\"params\":{\"msgForward\":{\"memberId\":\"AQIDBA==\",\"msg\":\"{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}\",\"msgTs\":\"1970-01-01T00:00:01.000000001Z\"}}}" -- $ "{\"v\":\"1\",\"event\":\"x.grp.msg.forward\",\"params\":{\"msgForward\":{\"memberId\":\"AQIDBA==\",\"msg\":\"{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}\",\"msgTs\":\"1970-01-01T00:00:01.000000001Z\"}}}"

View file

@ -17,6 +17,30 @@
}, },
{ {
"/": "/invitation" "/": "/invitation"
},
{
"/": "/a/*"
},
{
"/": "/a"
},
{
"/": "/c/*"
},
{
"/": "/c"
},
{
"/": "/g/*"
},
{
"/": "/g"
},
{
"/": "/i/*"
},
{
"/": "/i"
} }
] ]
} }