core, ios: track "short link data set" state, interact with address accordingly (#5978)

This commit is contained in:
spaced4ndy
2025-06-10 15:12:23 +00:00
committed by GitHub
parent 2bc4836338
commit 7f6bc30894
30 changed files with 330 additions and 255 deletions
+15 -10
View File
@@ -837,8 +837,8 @@ enum ChatResponse1: Decodable, ChatAPIResult {
case let .groupAliasUpdated(u, toGroup): return withUser(u, String(describing: toGroup))
case let .connectionAliasUpdated(u, toConnection): return withUser(u, String(describing: toConnection))
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 .userContactLinkUpdated(u, contactLink): return withUser(u, contactLink.responseDetails)
case let .userContactLink(u, contactLink): return withUser(u, String(describing: contactLink))
case let .userContactLinkUpdated(u, contactLink): return withUser(u, String(describing: contactLink))
case let .userContactLinkCreated(u, connLink): return withUser(u, String(describing: connLink))
case .userContactLinkDeleted: return noDetails
case let .acceptingContactRequest(u, contact): return withUser(u, String(describing: contact))
@@ -888,8 +888,8 @@ enum ChatResponse2: Decodable, ChatAPIResult {
case membersRoleUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], toRole: GroupMemberRole)
case membersBlockedForAllUser(user: UserRef, groupInfo: GroupInfo, members: [GroupMember], blocked: Bool)
case groupUpdated(user: UserRef, toGroup: GroupInfo)
case groupLinkCreated(user: UserRef, groupInfo: GroupInfo, connLinkContact: CreatedConnLink, memberRole: GroupMemberRole)
case groupLink(user: UserRef, groupInfo: GroupInfo, connLinkContact: CreatedConnLink, memberRole: GroupMemberRole)
case groupLinkCreated(user: UserRef, groupInfo: GroupInfo, groupLink: GroupLink)
case groupLink(user: UserRef, groupInfo: GroupInfo, groupLink: GroupLink)
case groupLinkDeleted(user: UserRef, groupInfo: GroupInfo)
case newMemberContact(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember)
case newMemberContactSentInv(user: UserRef, contact: Contact, groupInfo: GroupInfo, member: GroupMember)
@@ -984,8 +984,8 @@ enum ChatResponse2: Decodable, ChatAPIResult {
case let .membersRoleUser(u, groupInfo, members, toRole): return withUser(u, "groupInfo: \(groupInfo)\nmembers: \(members)\ntoRole: \(toRole)")
case let .membersBlockedForAllUser(u, groupInfo, members, blocked): return withUser(u, "groupInfo: \(groupInfo)\nmember: \(members)\nblocked: \(blocked)")
case let .groupUpdated(u, toGroup): return withUser(u, String(describing: toGroup))
case let .groupLinkCreated(u, groupInfo, connLinkContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnLinkContact: \(connLinkContact)\nmemberRole: \(memberRole)")
case let .groupLink(u, groupInfo, connLinkContact, memberRole): return withUser(u, "groupInfo: \(groupInfo)\nconnLinkContact: \(connLinkContact)\nmemberRole: \(memberRole)")
case let .groupLinkCreated(u, groupInfo, groupLink): return withUser(u, "groupInfo: \(groupInfo)\ngroupLink: \(groupLink)")
case let .groupLink(u, groupInfo, groupLink): return withUser(u, "groupInfo: \(groupInfo)\ngroupLink: \(groupLink)")
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 .newMemberContactSentInv(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)")
@@ -1395,11 +1395,8 @@ struct UserMsgReceiptSettings: Codable {
struct UserContactLink: Decodable, Hashable {
var connLinkContact: CreatedConnLink
var shortLinkDataSet: Bool
var autoAccept: AutoAccept?
var responseDetails: String {
"connLinkContact: \(connLinkContact)\nautoAccept: \(AutoAccept.cmdString(autoAccept))"
}
}
struct AutoAccept: Codable, Hashable {
@@ -1420,6 +1417,14 @@ struct AutoAccept: Codable, Hashable {
}
}
struct GroupLink: Decodable, Hashable {
var userContactLinkId: Int64
var connLinkContact: CreatedConnLink
var shortLinkDataSet: Bool
var groupLinkId: String
var acceptMemberRole: GroupMemberRole
}
struct DeviceToken: Decodable {
var pushProvider: PushProvider
var token: String
+4 -2
View File
@@ -404,6 +404,10 @@ final class ChatModel: ObservableObject {
remoteCtrlSession?.active ?? false
}
var addressShortLinkDataSet: Bool {
userAddress?.shortLinkDataSet ?? true
}
func getUser(_ userId: Int64) -> User? {
currentUser?.userId == userId
? currentUser
@@ -1216,9 +1220,7 @@ struct ShowingInvitation {
var connChatUsed: Bool
}
// TODO [short links] incognito if !userAddress.shortLinkDataSet; or remove if no access to chat model here
struct NTFContactRequest {
var incognito: Bool
var chatId: String
}
+3 -11
View File
@@ -12,7 +12,6 @@ import UIKit
import SimpleXChat
let ntfActionAcceptContact = "NTF_ACT_ACCEPT_CONTACT"
let ntfActionAcceptContactIncognito = "NTF_ACT_ACCEPT_CONTACT_INCOGNITO"
let ntfActionAcceptCall = "NTF_ACT_ACCEPT_CALL"
let ntfActionRejectCall = "NTF_ACT_REJECT_CALL"
@@ -59,13 +58,12 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
logger.debug("NtfManager.processNotificationResponse changeActiveUser")
changeActiveUser(userId, viewPwd: nil)
}
if content.categoryIdentifier == ntfCategoryContactRequest && (action == ntfActionAcceptContact || action == ntfActionAcceptContactIncognito),
if content.categoryIdentifier == ntfCategoryContactRequest && action == ntfActionAcceptContact,
let chatId = content.userInfo["chatId"] as? String {
let incognito = action == ntfActionAcceptContactIncognito
if case let .contactRequest(contactRequest) = chatModel.getChat(chatId)?.chatInfo {
Task { await acceptContactRequest(incognito: incognito, contactRequestId: contactRequest.apiId) }
Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) }
} else {
chatModel.ntfContactRequest = NTFContactRequest(incognito: incognito, chatId: chatId)
chatModel.ntfContactRequest = NTFContactRequest(chatId: chatId)
}
} else if let (chatId, ntfAction) = ntfCallAction(content, action) {
if let invitation = chatModel.callInvitations.removeValue(forKey: chatId) {
@@ -161,12 +159,6 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
identifier: ntfActionAcceptContact,
title: NSLocalizedString("Accept", comment: "accept contact request via notification"),
options: .foreground
),
// TODO [short links] if !userAddress.shortLinkDataSet; or remove if no access to chat model here
UNNotificationAction(
identifier: ntfActionAcceptContactIncognito,
title: NSLocalizedString("Accept incognito", comment: "accept contact request via notification"),
options: .foreground
)
],
intentIdentifiers: [],
+9 -9
View File
@@ -1758,16 +1758,16 @@ func apiUpdateGroup(_ groupId: Int64, _ groupProfile: GroupProfile) async throws
throw r.unexpected
}
func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> (CreatedConnLink, GroupMemberRole) {
func apiCreateGroupLink(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> GroupLink {
let short = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS)
let r: ChatResponse2 = try await chatSendCmd(.apiCreateGroupLink(groupId: groupId, memberRole: memberRole, short: short))
if case let .groupLinkCreated(_, _, connLink, memberRole) = r { return (connLink, memberRole) }
if case let .groupLinkCreated(_, _, groupLink) = r { return groupLink }
throw r.unexpected
}
func apiGroupLinkMemberRole(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> (CreatedConnLink, GroupMemberRole) {
func apiGroupLinkMemberRole(_ groupId: Int64, memberRole: GroupMemberRole = .member) async throws -> GroupLink {
let r: ChatResponse2 = try await chatSendCmd(.apiGroupLinkMemberRole(groupId: groupId, memberRole: memberRole))
if case let .groupLink(_, _, connLink, memberRole) = r { return (connLink, memberRole) }
if case let .groupLink(_, _, groupLink) = r { return groupLink }
throw r.unexpected
}
@@ -1777,20 +1777,20 @@ func apiDeleteGroupLink(_ groupId: Int64) async throws {
throw r.unexpected
}
func apiGetGroupLink(_ groupId: Int64) throws -> (CreatedConnLink, GroupMemberRole)? {
func apiGetGroupLink(_ groupId: Int64) throws -> GroupLink? {
let r: APIResult<ChatResponse2> = chatApiSendCmdSync(.apiGetGroupLink(groupId: groupId))
switch r {
case let .result(.groupLink(_, _, connLink, memberRole)):
return (connLink, memberRole)
case let .result(.groupLink(_, _, groupLink)):
return groupLink
case .error(.errorStore(storeError: .groupLinkNotFound)):
return nil
default: throw r.unexpected
}
}
func apiAddGroupShortLink(_ groupId: Int64) async throws -> (CreatedConnLink, GroupMemberRole) {
func apiAddGroupShortLink(_ groupId: Int64) async throws -> GroupLink {
let r: ChatResponse2 = try await chatSendCmd(.apiAddGroupShortLink(groupId: groupId))
if case let .groupLink(_, _, connLink, memberRole) = r { return (connLink, memberRole) }
if case let .groupLink(_, _, groupLink) = r { return groupLink }
throw r.unexpected
}
+1 -1
View File
@@ -164,7 +164,7 @@ struct SimpleXApp: App {
if let ncr = chatModel.ntfContactRequest {
await MainActor.run { chatModel.ntfContactRequest = nil }
if case let .contactRequest(contactRequest) = chatModel.getChat(ncr.chatId)?.chatInfo {
Task { await acceptContactRequest(incognito: ncr.incognito, contactRequestId: contactRequest.apiId) }
Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) }
}
}
} catch let error {
+4 -3
View File
@@ -47,7 +47,7 @@ struct ChatView: View {
@State private var selectedMember: GMember? = nil
// opening GroupLinkView on link button (incognito)
@State private var showGroupLinkSheet: Bool = false
@State private var groupLink: CreatedConnLink?
@State private var groupLink: GroupLink?
@State private var groupLinkMemberRole: GroupMemberRole = .member
@State private var forwardedChatItems: [ChatItem] = []
@State private var selectedChatItems: Set<Int64>? = nil
@@ -1040,8 +1040,9 @@ struct ChatView: View {
if case let .group(gInfo, _) = chat.chatInfo {
Task {
do {
if let link = try apiGetGroupLink(gInfo.groupId) {
(groupLink, groupLinkMemberRole) = link
if let gLink = try apiGetGroupLink(gInfo.groupId) {
groupLink = gLink
groupLinkMemberRole = gLink.acceptMemberRole
}
} catch let error {
logger.error("ChatView apiGetGroupLink: \(responseError(error))")
@@ -56,29 +56,36 @@ func showRejectRequestAlert(_ contactRequestId: Int64) {
}
func showAcceptRequestAlert(_ contactRequestId: Int64) {
showAlert(
NSLocalizedString("Accept contact request", comment: "alert title"),
actions: {[
var actions: [UIAlertAction] = []
actions.append(
UIAlertAction(
title: NSLocalizedString("Accept", comment: "alert action"),
style: .default,
handler: { _ in
Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequestId) }
}
)
)
if !ChatModel.shared.addressShortLinkDataSet {
actions.append(
UIAlertAction(
title: NSLocalizedString("Accept", comment: "alert action"),
title: NSLocalizedString("Accept incognito", comment: "alert action"),
style: .default,
handler: { _ in
Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequestId) }
}
),
// TODO [short links] if !userAddress.shortLinkDataSet; check other places
// UIAlertAction(
// title: NSLocalizedString("Accept incognito", comment: "alert action"),
// style: .default,
// handler: { _ in
// Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) }
// }
// ),
UIAlertAction(
title: NSLocalizedString("Cancel", comment: "alert action"),
style: .default
Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequestId) }
}
)
]}
)
}
actions.append(
UIAlertAction(
title: NSLocalizedString("Cancel", comment: "alert action"),
style: .default
)
)
showAlert(
NSLocalizedString("Accept contact request", comment: "alert title"),
actions: { actions }
)
}
@@ -22,7 +22,7 @@ struct GroupChatInfoView: View {
@State var localAlias: String
@FocusState private var aliasTextFieldFocused: Bool
@State private var alert: GroupChatInfoViewAlert? = nil
@State private var groupLink: CreatedConnLink?
@State private var groupLink: GroupLink?
@State private var groupLinkMemberRole: GroupMemberRole = .member
@State private var groupLinkNavLinkActive: Bool = false
@State private var addMembersNavLinkActive: Bool = false
@@ -221,8 +221,9 @@ struct GroupChatInfoView: View {
}
sendReceipts = SendReceipts.fromBool(groupInfo.chatSettings.sendRcpts, userDefault: sendReceiptsUserDefault)
do {
if let link = try apiGetGroupLink(groupInfo.groupId) {
(groupLink, groupLinkMemberRole) = link
if let gLink = try apiGetGroupLink(groupInfo.groupId) {
groupLink = gLink
groupLinkMemberRole = gLink.acceptMemberRole
}
} catch let error {
logger.error("GroupChatInfoView apiGetGroupLink: \(responseError(error))")
@@ -12,7 +12,7 @@ import SimpleXChat
struct GroupLinkView: View {
@EnvironmentObject var theme: AppTheme
var groupId: Int64
@Binding var groupLink: CreatedConnLink?
@Binding var groupLink: GroupLink?
@Binding var groupLinkMemberRole: GroupMemberRole
var showTitle: Bool = false
var creatingGroup: Bool = false
@@ -78,19 +78,27 @@ struct GroupLinkView: View {
}
}
.frame(height: 36)
SimpleXCreatedLinkQRCode(link: groupLink, short: $showShortLink)
.id("simplex-qrcode-view-for-\(groupLink.simplexChatUri(short: showShortLink))")
SimpleXCreatedLinkQRCode(link: groupLink.connLinkContact, short: $showShortLink)
.id("simplex-qrcode-view-for-\(groupLink.connLinkContact.simplexChatUri(short: showShortLink))")
Button {
showShareSheet(items: [groupLink.simplexChatUri(short: showShortLink)])
showShareSheet(items: [groupLink.connLinkContact.simplexChatUri(short: showShortLink)])
} label: {
Label("Share link", systemImage: "square.and.arrow.up")
}
if (groupLink.connShortLink == nil && UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS)) {
Button {
addShortLink()
} label: {
Label("Add short link", systemImage: "plus")
if UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS) {
if groupLink.connLinkContact.connShortLink == nil {
Button {
addShortLink()
} label: {
Label("Add short link", systemImage: "plus")
}
} else if !groupLink.shortLinkDataSet {
Button {
addShortLink()
} label: {
Label("Share group profile via link", systemImage: "plus")
}
}
}
@@ -106,8 +114,8 @@ struct GroupLinkView: View {
.disabled(creatingLink)
}
} header: {
if let groupLink, groupLink.connShortLink != nil {
ToggleShortLinkHeader(text: Text(""), link: groupLink, short: $showShortLink)
if let groupLink, groupLink.connLinkContact.connShortLink != nil {
ToggleShortLinkHeader(text: Text(""), link: groupLink.connLinkContact, short: $showShortLink)
}
}
.alert(item: $alert) { alert in
@@ -134,7 +142,7 @@ struct GroupLinkView: View {
.onChange(of: groupLinkMemberRole) { _ in
Task {
do {
_ = try await apiGroupLinkMemberRole(groupId, memberRole: groupLinkMemberRole)
groupLink = try await apiGroupLinkMemberRole(groupId, memberRole: groupLinkMemberRole)
} catch let error {
let a = getErrorAlert(error, "Error updating group link")
alert = .error(title: a.title, error: a.message)
@@ -155,10 +163,10 @@ struct GroupLinkView: View {
Task {
do {
creatingLink = true
let link = try await apiCreateGroupLink(groupId)
let gLink = try await apiCreateGroupLink(groupId)
await MainActor.run {
creatingLink = false
(groupLink, groupLinkMemberRole) = link
groupLink = gLink
}
} catch let error {
logger.error("GroupLinkView apiCreateGroupLink: \(responseError(error))")
@@ -175,10 +183,10 @@ struct GroupLinkView: View {
Task {
do {
creatingLink = true
let link = try await apiAddGroupShortLink(groupId)
let gLink = try await apiAddGroupShortLink(groupId)
await MainActor.run {
creatingLink = false
(groupLink, groupLinkMemberRole) = link
groupLink = gLink
}
} catch let error {
logger.error("apiAddGroupShortLink: \(responseError(error))")
@@ -194,8 +202,14 @@ struct GroupLinkView: View {
struct GroupLinkView_Previews: PreviewProvider {
static var previews: some View {
@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: CreatedConnLink? = nil
@State var groupLink: GroupLink? = GroupLink(
userContactLinkId: 1,
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),
shortLinkDataSet: false,
groupLinkId: "abc",
acceptMemberRole: .member
)
@State var noGroupLink: GroupLink? = nil
return Group {
GroupLinkView(groupId: 1, groupLink: $groupLink, groupLinkMemberRole: Binding.constant(.member))
@@ -136,12 +136,14 @@ struct ChatListNavLink: View {
Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequestId) }
} label: { SwipeLabel(NSLocalizedString("Accept", comment: "swipe action"), systemImage: "checkmark", inverted: oneHandUI) }
.tint(theme.colors.primary)
Button {
Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequestId) }
} label: {
SwipeLabel(NSLocalizedString("Accept incognito", comment: "swipe action"), systemImage: "theatermasks.fill", inverted: oneHandUI)
if !ChatModel.shared.addressShortLinkDataSet {
Button {
Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequestId) }
} label: {
SwipeLabel(NSLocalizedString("Accept incognito", comment: "swipe action"), systemImage: "theatermasks.fill", inverted: oneHandUI)
}
.tint(.indigo)
}
.tint(.indigo)
Button {
AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequestId))
} label: {
@@ -461,12 +463,14 @@ struct ChatListNavLink: View {
Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) }
} label: { SwipeLabel(NSLocalizedString("Accept", comment: "swipe action"), systemImage: "checkmark", inverted: oneHandUI) }
.tint(theme.colors.primary)
Button {
Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequest.apiId) }
} label: {
SwipeLabel(NSLocalizedString("Accept incognito", comment: "swipe action"), systemImage: "theatermasks.fill", inverted: oneHandUI)
if !ChatModel.shared.addressShortLinkDataSet {
Button {
Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequest.apiId) }
} label: {
SwipeLabel(NSLocalizedString("Accept incognito", comment: "swipe action"), systemImage: "theatermasks.fill", inverted: oneHandUI)
}
.tint(.indigo)
}
.tint(.indigo)
Button {
AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequest.apiId))
} label: {
@@ -478,7 +482,9 @@ struct ChatListNavLink: View {
.onTapGesture { showContactRequestDialog = true }
.confirmationDialog("Accept connection request?", isPresented: $showContactRequestDialog, titleVisibility: .visible) {
Button("Accept") { Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) } }
Button("Accept incognito") { Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequest.apiId) } }
if !ChatModel.shared.addressShortLinkDataSet {
Button("Accept incognito") { Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequest.apiId) } }
}
Button("Reject (sender NOT notified)", role: .destructive) { Task { await rejectContactRequest(contactRequest.apiId) } }
}
}
@@ -343,6 +343,8 @@ struct ChatPreviewView: View {
if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active {
chatPreviewInfoText("Tap to Connect")
.foregroundColor(theme.colors.primary)
} else if contact.nextAcceptContactRequest {
chatPreviewInfoText("swipe or open to connect")
} else if contact.sendMsgToConnect {
chatPreviewInfoText("send to connect")
} else if !contact.sndReady && contact.activeConn != nil && contact.active {
@@ -94,12 +94,14 @@ struct ContactListNavLink: View {
Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequestId) }
} label: { Label("Accept", systemImage: "checkmark") }
.tint(theme.colors.primary)
Button {
Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequestId) }
} label: {
Label("Accept incognito", systemImage: "theatermasks")
if !ChatModel.shared.addressShortLinkDataSet {
Button {
Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequestId) }
} label: {
Label("Accept incognito", systemImage: "theatermasks")
}
.tint(.indigo)
}
.tint(.indigo)
Button {
alert = SomeAlert(alert: rejectContactRequestAlert(contactRequestId), id: "rejectContactRequestAlert")
} label: {
@@ -258,12 +260,14 @@ struct ContactListNavLink: View {
Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) }
} label: { Label("Accept", systemImage: "checkmark") }
.tint(theme.colors.primary)
Button {
Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequest.apiId) }
} label: {
Label("Accept incognito", systemImage: "theatermasks")
if !ChatModel.shared.addressShortLinkDataSet {
Button {
Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequest.apiId) }
} label: {
Label("Accept incognito", systemImage: "theatermasks")
}
.tint(.indigo)
}
.tint(.indigo)
Button {
alert = SomeAlert(alert: rejectContactRequestAlert(contactRequest.apiId), id: "rejectContactRequestAlert")
} label: {
@@ -273,7 +277,9 @@ struct ContactListNavLink: View {
}
.confirmationDialog("Accept connection request?", isPresented: $showContactRequestDialog, titleVisibility: .visible) {
Button("Accept") { Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) } }
Button("Accept incognito") { Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequest.apiId) } }
if !ChatModel.shared.addressShortLinkDataSet {
Button("Accept incognito") { Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequest.apiId) } }
}
Button("Reject (sender NOT notified)", role: .destructive) { Task { await rejectContactRequest(contactRequest.apiId) } }
}
}
@@ -23,7 +23,7 @@ struct AddGroupView: View {
@State private var showTakePhoto = false
@State private var chosenImage: UIImage? = nil
@State private var showInvalidNameAlert = false
@State private var groupLink: CreatedConnLink?
@State private var groupLink: GroupLink?
@State private var groupLinkMemberRole: GroupMemberRole = .member
var body: some View {
@@ -77,9 +77,10 @@ struct CreateSimpleXAddress: View {
progressIndicator = true
Task {
do {
let connLinkContact = try await apiCreateUserAddress(short: false)
let short = false
let connLinkContact = try await apiCreateUserAddress(short: short)
DispatchQueue.main.async {
m.userAddress = UserContactLink(connLinkContact: connLinkContact)
m.userAddress = UserContactLink(connLinkContact: connLinkContact, shortLinkDataSet: short)
}
await MainActor.run { progressIndicator = false }
} catch let error {
@@ -153,8 +153,12 @@ struct UserAddressView: View {
}
}
addressSettingsButton(userAddress)
if (userAddress.connLinkContact.connShortLink == nil && UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS)) {
addShortLinkButton()
if UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS) {
if userAddress.connLinkContact.connShortLink == nil {
addShortLinkButton()
} else if !userAddress.shortLinkDataSet {
addProfileToShortLinkButton()
}
}
} header: {
ToggleShortLinkHeader(text: Text("For social media"), link: userAddress.connLinkContact, short: $showShortLink)
@@ -199,7 +203,7 @@ struct UserAddressView: View {
let short = UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_SHORT_LINKS)
let connLinkContact = try await apiCreateUserAddress(short: short)
DispatchQueue.main.async {
chatModel.userAddress = UserContactLink(connLinkContact: connLinkContact)
chatModel.userAddress = UserContactLink(connLinkContact: connLinkContact, shortLinkDataSet: short)
alert = .shareOnCreate
progressIndicator = false
}
@@ -214,12 +218,30 @@ struct UserAddressView: View {
private func addShortLinkButton() -> some View {
Button {
addShortLink()
showAddShortLinkAlert()
} label: {
Label("Add short link", systemImage: "plus")
}
}
private func addProfileToShortLinkButton() -> some View {
Button {
showAddShortLinkAlert()
} label: {
Label("Share profile via link", systemImage: "plus")
}
}
private func showAddShortLinkAlert() {
showAlert(
title: NSLocalizedString("Share profile via link", comment: "alert title"),
message: NSLocalizedString("Profile will be shared via the address short link. This change to the address cannot be reversed, other than fully deleting it. Do you wish to update the address?", comment: "alert message"),
buttonTitle: NSLocalizedString("Update (and share profile)", comment: "alert button"),
buttonAction: { addShortLink() },
cancelButton: true
)
}
private func addShortLink() {
progressIndicator = true
Task {
@@ -231,7 +253,7 @@ struct UserAddressView: View {
await MainActor.run { progressIndicator = false }
} catch let error {
logger.error("apiAddMyAddressShortLink: \(responseError(error))")
let a = getErrorAlert(error, "Error creating address")
let a = getErrorAlert(error, "Error adding address short link")
alert = .error(title: a.title, error: a.message)
await MainActor.run { progressIndicator = false }
}
@@ -527,7 +549,7 @@ struct UserAddressSettingsView: View {
private func autoAcceptSection() -> some View {
Section {
if !aas.business {
if !ChatModel.shared.addressShortLinkDataSet && !aas.business {
acceptIncognitoToggle()
}
welcomeMessageEditor()
@@ -594,7 +616,10 @@ private func saveAAS(_ aas: Binding<AutoAcceptState>, _ savedAAS: Binding<AutoAc
struct UserAddressView_Previews: PreviewProvider {
static var previews: some View {
let chatModel = ChatModel()
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))
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),
shortLinkDataSet: false
)
return Group {
@@ -26,7 +26,6 @@ import Control.Logger.Simple
import Control.Monad
import Control.Monad.Except
import Control.Monad.IO.Class
import Data.Int (Int64)
import Data.List (find, intercalate)
import Data.List.NonEmpty (NonEmpty (..))
import qualified Data.Map.Strict as M
@@ -52,6 +51,7 @@ import Simplex.Chat.Markdown (FormattedText (..), Format (..), parseMaybeMarkdow
import Simplex.Chat.Messages
import Simplex.Chat.Options
import Simplex.Chat.Protocol (MsgContent (..))
import Simplex.Chat.Store (GroupLink (..))
import Simplex.Chat.Store.Direct (getContact)
import Simplex.Chat.Store.Groups (getGroupInfo, getGroupLink, getGroupSummary, setGroupCustomData)
import Simplex.Chat.Store.Profiles (GroupLinkInfo (..), getGroupLinkInfo)
@@ -349,7 +349,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
let GroupInfo {groupId, groupProfile = GroupProfile {displayName}} = g
notifyOwner gr $ "Joined the group " <> displayName <> ", creating the link…"
sendChatCmd cc (APICreateGroupLink groupId GRMember False) >>= \case
Right CRGroupLinkCreated {connLinkContact = CCLink gLink _} -> do
Right CRGroupLinkCreated {groupLink = GroupLink {connLinkContact = CCLink gLink _}} -> do
setGroupStatus st gr GRSPendingUpdate
notifyOwner
gr
@@ -446,7 +446,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
groupProfileUpdate = profileUpdate <$> sendChatCmd cc (APIGetGroupLink groupId)
where
profileUpdate = \case
Right CRGroupLink {connLinkContact = CCLink cr sl_} ->
Right CRGroupLink {groupLink = GroupLink {connLinkContact = CCLink cr sl_}} ->
let hadLinkBefore = profileHasGroupLink fromGroup
hasLinkNow = profileHasGroupLink toGroup
profileHasGroupLink GroupInfo {groupProfile = gp} =
@@ -712,11 +712,11 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
let GroupInfo {groupProfile = GroupProfile {displayName = n}} = g
case mRole_ of
Nothing ->
getGroupLinkRole cc user g >>= \case
Just (_, CCLink gLink _, _, mRole) -> do
let anotherRole = case mRole of GRObserver -> GRMember; _ -> GRObserver
getGroupLink' cc user g >>= \case
Just GroupLink {connLinkContact = CCLink gLink _, acceptMemberRole} -> do
let anotherRole = case acceptMemberRole of GRObserver -> GRMember; _ -> GRObserver
sendReply $
initialRole n mRole
initialRole n acceptMemberRole
<> ("Send */role " <> tshow gId <> " " <> strEncodeTxt anotherRole <> "* to change it.\n\n")
<> onlyViaLink gLink
Nothing -> sendReply $ "Error: failed reading the initial member role for the group " <> n
@@ -893,11 +893,11 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName
let groupRef = groupReference' groupId gName
withGroupAndReg sendReply groupId gName $ \_ _ ->
sendChatCmd cc (APIGetGroupLink groupId) >>= \case
Right CRGroupLink {connLinkContact = CCLink cReq _, memberRole} ->
Right CRGroupLink {groupLink = GroupLink {connLinkContact = CCLink cReq _, acceptMemberRole}} ->
sendReply $ T.unlines
[ "The link to join the group " <> groupRef <> ":",
strEncodeTxt $ simplexChatContact cReq,
"New member role: " <> strEncodeTxt memberRole
"New member role: " <> strEncodeTxt acceptMemberRole
]
Left (ChatErrorStore (SEGroupLinkNotFound _)) ->
sendReply $ "The group " <> groupRef <> " has no public link."
@@ -1045,15 +1045,15 @@ vr :: ChatController -> VersionRangeChat
vr ChatController {config = ChatConfig {chatVRange}} = chatVRange
{-# INLINE vr #-}
getGroupLinkRole :: ChatController -> User -> GroupInfo -> IO (Maybe (Int64, CreatedLinkContact, GroupLinkId, GroupMemberRole))
getGroupLinkRole cc user gInfo =
getGroupLink' :: ChatController -> User -> GroupInfo -> IO (Maybe GroupLink)
getGroupLink' cc user gInfo =
withDB "getGroupLink" cc $ \db -> getGroupLink db user gInfo
setGroupLinkRole :: ChatController -> GroupInfo -> GroupMemberRole -> IO (Maybe ConnReqContact)
setGroupLinkRole cc GroupInfo {groupId} mRole = resp <$> sendChatCmd cc (APIGroupLinkMemberRole groupId mRole)
where
resp = \case
Right (CRGroupLink _ _ (CCLink gLink _) _) -> Just gLink
Right (CRGroupLink {groupLink = GroupLink {connLinkContact = CCLink gLink _sLnk}}) -> Just gLink
_ -> Nothing
unexpectedError :: Text -> Text
+3 -3
View File
@@ -62,7 +62,7 @@ import Simplex.Chat.Protocol
import Simplex.Chat.Remote.AppVersion
import Simplex.Chat.Remote.Types
import Simplex.Chat.Stats (PresentedServersSummary)
import Simplex.Chat.Store (AutoAccept, ChatLockEntity, StoreError (..), UserContactLink, GroupLinkInfo, UserMsgReceiptSettings)
import Simplex.Chat.Store (AutoAccept, ChatLockEntity, GroupLink, GroupLinkInfo, StoreError (..), UserContactLink, UserMsgReceiptSettings)
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
import Simplex.Chat.Types.Shared
@@ -723,8 +723,8 @@ data ChatResponse
| CRGroupUpdated {user :: User, fromGroup :: GroupInfo, toGroup :: GroupInfo, member_ :: Maybe GroupMember}
| CRGroupProfile {user :: User, groupInfo :: GroupInfo}
| CRGroupDescription {user :: User, groupInfo :: GroupInfo} -- only used in CLI
| CRGroupLinkCreated {user :: User, groupInfo :: GroupInfo, connLinkContact :: CreatedLinkContact, memberRole :: GroupMemberRole}
| CRGroupLink {user :: User, groupInfo :: GroupInfo, connLinkContact :: CreatedLinkContact, memberRole :: GroupMemberRole}
| CRGroupLinkCreated {user :: User, groupInfo :: GroupInfo, groupLink :: GroupLink}
| CRGroupLink {user :: User, groupInfo :: GroupInfo, groupLink :: GroupLink}
| CRGroupLinkDeleted {user :: User, groupInfo :: GroupInfo}
| CRNewMemberContact {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember}
| CRNewMemberContactSentInv {user :: User, contact :: Contact, groupInfo :: GroupInfo, member :: GroupMember}
+35 -58
View File
@@ -1144,19 +1144,17 @@ processChatCommand' vr = \case
withFastStore' $ \db -> deleteNoteFolderCIs db user nf
pure $ CRChatCleared user (AChatInfo SCTLocal $ LocalChat nf)
_ -> throwCmdError "not supported"
-- TODO [short links] prohibit incognito if short link data is set for address
-- TODO - add user_contact_links.short_link_data_set; UserContactLink.shortLinkDataSet
-- TODO - same for auto-accept:
-- TODO - set incognito to false on setting short link data
-- TODO - ignore on accept (for foolproofing)
-- TODO - hide in UI
APIAcceptContact incognito connReqId -> withUser $ \_ -> do
userContactLinkId <- withFastStore $ \db -> getUserContactLinkIdByCReq db connReqId
withUserContactLock "acceptContact" userContactLinkId $ do
(user@User {userId}, cReq) <- withFastStore $ \db -> getContactRequest' db connReqId
APIAcceptContact incognito connReqId -> withUser $ \user@User {userId} -> do
(uclId, (ucl, gLinkInfo_)) <- withFastStore $ \db -> do
uclId <- getUserContactLinkIdByCReq db connReqId
uclGLinkInfo <- getUserContactLinkById db userId uclId
pure (uclId, uclGLinkInfo)
let UserContactLink {shortLinkDataSet} = ucl
when (shortLinkDataSet && incognito) $ throwCmdError "incognito not allowed for address with short link data"
withUserContactLock "acceptContact" uclId $ do
cReq <- withFastStore $ \db -> getContactRequest db user connReqId
(ct, conn@Connection {connId}, sqSecured) <- acceptContactRequest user cReq incognito
ucl <- withFastStore $ \db -> getUserContactLinkById db userId userContactLinkId
let contactUsed = (\(_, gLinkInfo_) -> isNothing gLinkInfo_) ucl
let contactUsed = isNothing gLinkInfo_
ct' <- withStore' $ \db -> do
deleteContactRequestRec db user cReq
updateContactAccepted db user ct contactUsed
@@ -1764,9 +1762,9 @@ processChatCommand' vr = \case
-- TODO [short links] change prepared entity user
-- TODO - UI would call these APIs before APIConnectPrepared... APIs
-- TODO - UI to transition to new user keeping chat opened
APIChangeContactUser contactId newUserId -> withUser $ \user -> do
APIChangeContactUser _contactId _newUserId -> withUser $ \_user -> do
ok_
APIChangeGroupUser groupId newUserId -> withUser $ \user -> do
APIChangeGroupUser _groupId _newUserId -> withUser $ \_user -> do
ok_
APIConnectPreparedContact contactId incognito msgContent_ -> withUser $ \user -> do
Contact {connLinkToConnect} <- withFastStore $ \db -> getContact db vr user contactId
@@ -1837,37 +1835,13 @@ processChatCommand' vr = \case
processChatCommand $ APIListContacts userId
APICreateMyAddress userId short -> withUserId userId $ \user -> procCmd $ do
subMode <- chatReadVar subscriptionMode
-- TODO [short links] incognito interaction with contact address
-- TODO - problems:
-- TODO 1 now that user profile is advertised in short link, giving an option to
-- TODO share incognito profile on accept doesn't make sense.
-- TODO 2 even advertising a random incognito profile in short link is somewhat broken,
-- TODO as it would be the same for all connecting clients, but then changed on accept (in current implementation),
-- TODO so it would be clear it's incognito profile; it might as well be clearly a placeholder
-- TODO (e.g. "Hidden profile" in profile name), and to avoid distinguishing incognito and main profiles,
-- TODO it can be a setting that can be set for main profile too.
-- TODO 3 changing short link data from main to incognito profile also defeats the purpose of incognito profile,
-- TODO as by scanning the short link in the past, connecting clients may know main profile (even if it's
-- TODO currently set as incognito).
-- TODO - some possibilities:
-- TODO 1 always base choice on autoAccept -> acceptIncognito choice, don't give option on user accept action
-- TODO 2 in this case, replace short link user data on change of this setting? (doesn't solve problem 3)
-- TODO 3 give setting to include "Hidden profile" in short link, even if autoAccept is not set to incognito
-- TODO 4 share same random profile on each accept, this way connecting clients technically
-- TODO wouldn't be able to distinguish incognito profile (placeholder problem);
-- TODO 5 only give choice to accept with main or incognito profile if random profile or placeholder is shared
-- TODO in short link user data; if main profile is shared, don't give choice
-- TODO 6 don't allow to change from main profile in short link to "Hidden profile" (or incognito autoAccept),
-- TODO only allow to change vice versa, in one direction (solves problem 3)
-- TODO 7 remove incognito functionality from address altogether
-- TODO - it seems measures 3, 5, 6 are the most reasonable, or removing incognito functionality for addresses altogether
let userData =
if short
then Just $ encodeShortLinkData (ContactShortLinkData (userProfileToSend user Nothing Nothing False) Nothing)
else Nothing
(connId, ccLink) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact userData Nothing IKPQOn subMode
ccLink' <- shortenCreatedLink ccLink
withFastStore $ \db -> createUserContactLink db user connId ccLink' subMode
withFastStore $ \db -> createUserContactLink db user connId ccLink' short subMode
pure $ CRUserContactLinkCreated user ccLink'
CreateMyAddress short -> withUser $ \User {userId} ->
processChatCommand $ APICreateMyAddress userId short
@@ -1889,10 +1863,8 @@ processChatCommand' vr = \case
ShowMyAddress -> withUser' $ \User {userId} ->
processChatCommand $ APIShowMyAddress userId
APIAddMyAddressShortLink userId -> withUserId' userId $ \user -> do
(ucl@UserContactLink {connLinkContact = CCLink connFullLink sLnk_, autoAccept}, conn) <-
(ucl@UserContactLink {connLinkContact = CCLink connFullLink _sLnk_, autoAccept}, conn) <-
withFastStore $ \db -> (,) <$> getUserAddress db user <*> getUserAddressConnection db vr user
when (isJust sLnk_) $ throwCmdError "address already has short link"
-- TODO [short links] allow to add short link without data if autoAccept was set to incognito?
let shortLinkProfile = userProfileToSend user Nothing Nothing False
shortLinkMsg = autoAccept >>= autoReply >>= (Just . msgContentText)
userData = encodeShortLinkData (ContactShortLinkData shortLinkProfile shortLinkMsg)
@@ -1900,7 +1872,8 @@ processChatCommand' vr = \case
case entityId conn of
Just uclId -> do
withFastStore' $ \db -> setUserContactLinkShortLink db uclId sLnk
let ucl' = (ucl :: UserContactLink) {connLinkContact = CCLink connFullLink (Just sLnk)}
let autoAccept' = autoAccept >>= \aa -> Just aa {acceptIncognito = False}
ucl' = (ucl :: UserContactLink) {connLinkContact = CCLink connFullLink (Just sLnk), shortLinkDataSet = True, autoAccept = autoAccept'}
pure $ CRUserContactLink user ucl'
Nothing -> throwChatError $ CEException "no user contact link id"
APISetProfileAddress userId False -> withUserId userId $ \user@User {profile = p} -> do
@@ -1915,7 +1888,9 @@ processChatCommand' vr = \case
processChatCommand $ APISetProfileAddress userId onOff
APIAddressAutoAccept userId autoAccept_ -> withUserId userId $ \user -> do
-- TODO [short links] update adress short link data if message changed
forM_ autoAccept_ $ \AutoAccept {businessAddress, acceptIncognito} ->
UserContactLink {shortLinkDataSet} <- withFastStore (`getUserAddress` user)
forM_ autoAccept_ $ \AutoAccept {businessAddress, acceptIncognito} -> do
when (shortLinkDataSet && acceptIncognito) $ throwCmdError "incognito not allowed for address with short link data"
when (businessAddress && acceptIncognito) $ throwCmdError "requests to business address cannot be accepted incognito"
contactLink <- withFastStore (\db -> updateUserAddressAutoAccept db user autoAccept_)
pure $ CRUserContactLinkUpdated user contactLink
@@ -2482,37 +2457,39 @@ processChatCommand' vr = \case
crClientData = encodeJSON $ CRDataGroup groupLinkId
(connId, ccLink) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact userData (Just crClientData) IKPQOff subMode
ccLink' <- createdGroupLink <$> shortenCreatedLink ccLink
withFastStore $ \db -> createGroupLink db user gInfo connId ccLink' groupLinkId mRole subMode
pure $ CRGroupLinkCreated user gInfo ccLink' mRole
gLink <- withFastStore $ \db -> createGroupLink db user gInfo connId ccLink' groupLinkId mRole short subMode
pure $ CRGroupLinkCreated user gInfo gLink
APIGroupLinkMemberRole groupId mRole' -> withUser $ \user -> withGroupLock "groupLinkMemberRole" groupId $ do
gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId
(groupLinkId, groupLink, _, mRole) <- withFastStore $ \db -> getGroupLink db user gInfo
gLnk@GroupLink {acceptMemberRole} <- withFastStore $ \db -> getGroupLink db user gInfo
assertUserGroupRole gInfo GRAdmin
when (mRole' > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole'
when (mRole' /= mRole) $ withFastStore' $ \db -> setGroupLinkMemberRole db user groupLinkId mRole'
pure $ CRGroupLink user gInfo groupLink mRole'
gLnk' <-
if mRole' /= acceptMemberRole
then withFastStore' $ \db -> setGroupLinkMemberRole db user gLnk mRole'
else pure gLnk
pure $ CRGroupLink user gInfo gLnk'
APIDeleteGroupLink groupId -> withUser $ \user -> withGroupLock "deleteGroupLink" groupId $ do
gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId
deleteGroupLink' user gInfo
pure $ CRGroupLinkDeleted user gInfo
APIGetGroupLink groupId -> withUser $ \user -> do
gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId
(_, groupLink, _, mRole) <- withFastStore $ \db -> getGroupLink db user gInfo
pure $ CRGroupLink user gInfo groupLink mRole
gLnk <- withFastStore $ \db -> getGroupLink db user gInfo
pure $ CRGroupLink user gInfo gLnk
APIAddGroupShortLink groupId -> withUser $ \user -> do
(gInfo, (uclId, _gLink@(CCLink connFullLink sLnk_), gLinkId, mRole), conn) <- withFastStore $ \db -> do
(gInfo, gLink, conn) <- withFastStore $ \db -> do
gInfo <- getGroupInfo db vr user groupId
gLink <- getGroupLink db user gInfo
conn <- getGroupLinkConnection db vr user gInfo
pure (gInfo, gLink, conn)
when (isJust sLnk_) $ throwCmdError "group link already has short link"
let GroupInfo {groupProfile} = gInfo
userData = encodeShortLinkData (GroupShortLinkData groupProfile)
crClientData = encodeJSON $ CRDataGroup gLinkId
GroupLink {groupLinkId} = gLink
crClientData = encodeJSON $ CRDataGroup groupLinkId
sLnk <- shortenShortLink' . toShortGroupLink =<< withAgent (\a -> setConnShortLink a (aConnId conn) SCMContact userData (Just crClientData))
withFastStore' $ \db -> setUserContactLinkShortLink db uclId sLnk
let groupLink' = CCLink connFullLink (Just sLnk)
pure $ CRGroupLink user gInfo groupLink' mRole
gLink' <- withFastStore' $ \db -> setGroupLinkShortLink db gLink sLnk
pure $ CRGroupLink user gInfo gLink'
APICreateMemberContact gId gMemberId -> withUser $ \user -> do
(g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId
assertUserGroupRole g GRAuthor
@@ -3313,7 +3290,7 @@ processChatCommand' vr = \case
case ct of
CCTContact ->
withFastStore' (\db -> getUserContactLinkViaShortLink db user l') >>= \case
Just (UserContactLink (CCLink cReq _) _) -> pure (ACCL SCMContact $ CCLink cReq (Just l'), CPContactAddress CAPOwnLink)
Just (UserContactLink (CCLink cReq _) _ _) -> pure (ACCL SCMContact $ CCLink cReq (Just l'), CPContactAddress CAPOwnLink)
Nothing -> do
(cReq, cData) <- getShortLinkConnReq user l'
let contactSLinkData_ = decodeShortLinkData $ linkUserData cData
+16 -16
View File
@@ -577,9 +577,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
probeMatchingContactsAndMembers ct' (contactConnIncognito ct') doProbeContacts
withStore' $ \db -> resetContactConnInitiated db user conn'
forM_ viaUserContactLink $ \userContactLinkId -> do
ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId
let (UserContactLink {autoAccept}, gli_) = ucl
when (connChatVersion < batchSend2Version) $ sendAutoReply ct' autoAccept
(ucl, gli_) <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId
when (connChatVersion < batchSend2Version) $ sendAutoReply ucl ct'
-- TODO REMOVE LEGACY vvv
forM_ gli_ $ \GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do
groupInfo <- withStore $ \db -> getGroupInfo db vr user groupId
@@ -645,9 +644,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
lift $ setContactNetworkStatus ct NSConnected
toView $ CEvtContactSndReady user ct
forM_ viaUserContactLink $ \userContactLinkId -> do
ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId
let (UserContactLink {autoAccept}, _) = ucl
when (connChatVersion >= batchSend2Version) $ sendAutoReply ct autoAccept
(ucl, _) <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId
when (connChatVersion >= batchSend2Version) $ sendAutoReply ucl ct
QCONT ->
void $ continueSending connEntity conn
MWARN msgId err -> do
@@ -667,13 +665,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
-- TODO add debugging output
_ -> pure ()
where
-- TODO [short links] don't send auto-reply message if it should have been created by connecting client
-- TODO - based on version + whether address has short link data (shortLinkDataSet)
sendAutoReply ct = \case
Just AutoAccept {autoReply = Just mc} -> do
(msg, _) <- sendDirectContactMessage user ct (XMsgNew $ MCSimple (extMsgContent mc Nothing))
ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc)
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci]
sendAutoReply UserContactLink {shortLinkDataSet, autoAccept} ct = case autoAccept of
Just AutoAccept {autoReply = Just mc}
| not shortLinkDataSet || connChatVersion < shortLinkDataVersion -> do
(msg, _) <- sendDirectContactMessage user ct (XMsgNew $ MCSimple (extMsgContent mc Nothing))
ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc)
toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDSnd (DirectChat ct) ci]
_ -> pure ()
processGroupMessage :: AEvent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> CM ()
@@ -1227,8 +1224,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
where
profileContactRequest :: InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe MsgContent -> PQSupport -> CM ()
profileContactRequest invId chatVRange p@Profile {displayName} xContactId_ mc_ reqPQSup = do
ucl <- withStore $ \db -> getUserContactLinkById db userId uclId
let (UserContactLink {connLinkContact = CCLink connReq _, autoAccept}, gLinkInfo_) = ucl
uclGLinkInfo <- withStore $ \db -> getUserContactLinkById db userId uclId
let (UserContactLink {connLinkContact = CCLink connReq _, shortLinkDataSet, autoAccept}, gLinkInfo_) = uclGLinkInfo
isSimplexTeam = sameConnReqContact connReq adminContactReq
v = maxVersion chatVRange
case autoAccept of
@@ -1265,7 +1262,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
Just ct -> toView $ CEvtContactRequestAlreadyAccepted user ct
Nothing -> do
-- [incognito] generate profile to send, create connection with incognito profile
incognitoProfile <- if acceptIncognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing
incognitoProfile <-
if not shortLinkDataSet && acceptIncognito
then Just . NewIncognito <$> liftIO generateRandomProfile
else pure Nothing
ct <- acceptContactRequestAsync user uclId invId chatVRange p xContactId_ reqPQSup incognitoProfile
forM_ mc_ $ \mc ->
createInternalChatItem user (CDDirectRcv ct) (CIRcvMsgContent mc) Nothing
+6 -1
View File
@@ -78,12 +78,13 @@ import Simplex.Messaging.Version hiding (version)
-- 12 - support sending and receiving content reports (2025-01-03)
-- 14 - support sending and receiving group join rejection (2025-02-24)
-- 15 - support specifying message scopes for group messages (2025-03-12)
-- 16 - support short link data (2025-06-10)
-- This should not be used directly in code, instead use `maxVersion chatVRange` from ChatConfig.
-- This indirection is needed for backward/forward compatibility testing.
-- Testing with real app versions is still needed, as tests use the current code with different version ranges, not the old code.
currentChatVersion :: VersionChat
currentChatVersion = VersionChat 15
currentChatVersion = VersionChat 16
-- This should not be used directly in code, instead use `chatVRange` from ChatConfig (see comment above)
supportedChatVRange :: VersionRangeChat
@@ -142,6 +143,10 @@ groupJoinRejectVersion = VersionChat 14
groupKnockingVersion :: VersionChat
groupKnockingVersion = VersionChat 15
-- support short link data in invitation, contact and group links
shortLinkDataVersion :: VersionChat
shortLinkDataVersion = VersionChat 16
agentToChatVersion :: VersionSMPA -> VersionChat
agentToChatVersion v
| v < pqdrSMPAgentVersion = initialChatVersion
+2
View File
@@ -6,6 +6,7 @@ module Simplex.Chat.Store
ChatLockEntity (..),
UserMsgReceiptSettings (..),
UserContactLink (..),
GroupLink (..),
GroupLinkInfo (..),
AutoAccept (..),
createChatStore,
@@ -14,6 +15,7 @@ module Simplex.Chat.Store
)
where
import Simplex.Chat.Store.Groups (GroupLink (..))
import Simplex.Chat.Store.Profiles
import Simplex.Chat.Store.Shared
import Simplex.Messaging.Agent.Store.Common (DBStore (..), withTransaction)
-6
View File
@@ -60,7 +60,6 @@ module Simplex.Chat.Store.Direct
getAcceptedContactByXContactId,
getAcceptedBusinessChatByXContactId,
getUserContactLinkIdByCReq,
getContactRequest',
getContactRequest,
getContactRequestIdByName,
deleteContactRequest,
@@ -830,11 +829,6 @@ getUserContactLinkIdByCReq db contactRequestId =
ExceptT . firstRow fromOnly (SEContactRequestNotFound contactRequestId) $
DB.query db "SELECT user_contact_link_id FROM contact_requests WHERE contact_request_id = ?" (Only contactRequestId)
getContactRequest' :: DB.Connection -> Int64 -> ExceptT StoreError IO (User, UserContactRequest)
getContactRequest' db contactRequestId = do
user <- getUserByContactRequestId db contactRequestId
(user,) <$> getContactRequest db user contactRequestId
getContactRequest :: DB.Connection -> User -> Int64 -> ExceptT StoreError IO UserContactRequest
getContactRequest db User {userId} contactRequestId =
ExceptT . firstRow toContactRequest (SEContactRequestNotFound contactRequestId) $
+48 -10
View File
@@ -8,6 +8,7 @@
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-}
@@ -17,6 +18,7 @@ module Simplex.Chat.Store.Groups
GroupInfoRow,
GroupMemberRow,
MaybeGroupMemberRow,
GroupLink (..),
toGroupInfo,
toGroupMember,
toMaybeGroupMember,
@@ -28,6 +30,7 @@ module Simplex.Chat.Store.Groups
getGroupLink,
getGroupLinkId,
setGroupLinkMemberRole,
setGroupLinkShortLink,
getGroupAndMember,
createNewGroup,
createGroupInvitation,
@@ -151,6 +154,7 @@ import Control.Monad
import Control.Monad.Except
import Control.Monad.IO.Class
import Crypto.Random (ChaChaDRG)
import qualified Data.Aeson.TH as J
import Data.Bifunctor (second)
import Data.Bitraversable (bitraverse)
import Data.Either (rights)
@@ -175,6 +179,7 @@ import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..))
import qualified Simplex.Messaging.Agent.Store.DB as DB
import qualified Simplex.Messaging.Crypto as C
import Simplex.Messaging.Crypto.Ratchet (pattern PQEncOff, pattern PQSupportOff)
import Simplex.Messaging.Parsers (defaultJSON)
import Simplex.Messaging.Protocol (SubscriptionMode (..))
import Simplex.Messaging.Util (eitherToMaybe, firstRow', ($>>=), (<$$>))
import Simplex.Messaging.Version
@@ -194,16 +199,17 @@ toMaybeGroupMember userContactId ((Just groupMemberId, Just groupId, Just member
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) :. (supportChatTs, supportChatUnread, supportChatUnanswered, supportChatMentions, supportChatLastMsgFromMemberTs))
toMaybeGroupMember _ _ = Nothing
createGroupLink :: DB.Connection -> User -> GroupInfo -> ConnId -> CreatedLinkContact -> GroupLinkId -> GroupMemberRole -> SubscriptionMode -> ExceptT StoreError IO ()
createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName} agentConnId (CCLink cReq shortLink) groupLinkId memberRole subMode =
createGroupLink :: DB.Connection -> User -> GroupInfo -> ConnId -> CreatedLinkContact -> GroupLinkId -> GroupMemberRole -> Bool -> SubscriptionMode -> ExceptT StoreError IO GroupLink
createGroupLink db user@User {userId} groupInfo@GroupInfo {groupId, localDisplayName} agentConnId (CCLink cReq shortLink) groupLinkId memberRole shortLinkDataSet subMode = do
checkConstraint (SEDuplicateGroupLink groupInfo) . liftIO $ do
currentTs <- getCurrentTime
DB.execute
db
"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, shortLink, memberRole, BI True, currentTs, currentTs)
"INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, short_link_contact, short_link_data_set, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)"
((userId, groupId, groupLinkId, "group_link_" <> localDisplayName, cReq, shortLink, BI shortLinkDataSet) :. (memberRole, BI True, currentTs, currentTs))
userContactLinkId <- insertedRowId db
void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId ConnNew initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode PQSupportOff
getGroupLink db user groupInfo
getGroupLinkConnection :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> ExceptT StoreError IO Connection
getGroupLinkConnection db vr User {userId} groupInfo@GroupInfo {groupId} =
@@ -262,21 +268,51 @@ deleteGroupLink db User {userId} GroupInfo {groupId} = do
(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, CreatedLinkContact, GroupLinkId, GroupMemberRole)
data GroupLink = GroupLink
{ userContactLinkId :: Int64, -- db id
connLinkContact :: CreatedLinkContact,
shortLinkDataSet :: Bool,
groupLinkId :: GroupLinkId,
acceptMemberRole :: GroupMemberRole
}
deriving (Show)
getGroupLink :: DB.Connection -> User -> GroupInfo -> ExceptT StoreError IO GroupLink
getGroupLink db User {userId} gInfo@GroupInfo {groupId} =
ExceptT . firstRow groupLink (SEGroupLinkNotFound gInfo) $
DB.query db "SELECT user_contact_link_id, conn_req_contact, short_link_contact, group_link_id, group_link_member_role FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1" (userId, groupId)
ExceptT . firstRow toGroupLink (SEGroupLinkNotFound gInfo) $
DB.query db "SELECT user_contact_link_id, conn_req_contact, short_link_contact, short_link_data_set, group_link_id, group_link_member_role FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1" (userId, groupId)
where
groupLink (linkId, cReq, shortLink, gLinkId, mRole_) = (linkId, CCLink cReq shortLink, gLinkId, fromMaybe GRMember mRole_)
toGroupLink (userContactLinkId, cReq, shortLink, BI shortLinkDataSet, groupLinkId, mRole_) =
GroupLink {
userContactLinkId,
connLinkContact = CCLink cReq shortLink,
shortLinkDataSet,
groupLinkId,
acceptMemberRole = fromMaybe GRMember mRole_
}
getGroupLinkId :: DB.Connection -> User -> GroupInfo -> IO (Maybe GroupLinkId)
getGroupLinkId db User {userId} GroupInfo {groupId} =
fmap join . maybeFirstRow fromOnly $
DB.query db "SELECT group_link_id FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1" (userId, groupId)
setGroupLinkMemberRole :: DB.Connection -> User -> Int64 -> GroupMemberRole -> IO ()
setGroupLinkMemberRole db User {userId} userContactLinkId memberRole =
setGroupLinkMemberRole :: DB.Connection -> User -> GroupLink -> GroupMemberRole -> IO GroupLink
setGroupLinkMemberRole db User {userId} gLnk@GroupLink{userContactLinkId} memberRole = do
DB.execute db "UPDATE user_contact_links SET group_link_member_role = ? WHERE user_id = ? AND user_contact_link_id = ?" (memberRole, userId, userContactLinkId)
pure gLnk {acceptMemberRole = memberRole}
setGroupLinkShortLink :: DB.Connection -> GroupLink -> ShortLinkContact -> IO GroupLink
setGroupLinkShortLink db gLnk@GroupLink {userContactLinkId, connLinkContact = CCLink connFullLink _sLnk_} shortLink = do
DB.execute
db
[sql|
UPDATE user_contact_links
SET short_link_contact = ?,
short_link_data_set = ?
WHERE user_contact_link_id = ?
|]
(shortLink, BI True, userContactLinkId)
pure gLnk {connLinkContact = CCLink connFullLink (Just shortLink), shortLinkDataSet = True}
getGroupAndMember :: DB.Connection -> User -> Int64 -> VersionRangeChat -> ExceptT StoreError IO (GroupInfo, GroupMember)
getGroupAndMember db User {userId, userContactId} groupMemberId vr = do
@@ -2747,3 +2783,5 @@ updateGroupAlias db userId g@GroupInfo {groupId} localAlias = do
updatedAt <- getCurrentTime
DB.execute db "UPDATE groups SET local_alias = ?, updated_at = ? WHERE user_id = ? AND group_id = ?" (localAlias, updatedAt, userId, groupId)
pure (g :: GroupInfo) {localAlias = localAlias}
$(J.deriveJSON defaultJSON ''GroupLink)
+14 -11
View File
@@ -354,14 +354,14 @@ getUserContactProfiles db User {userId} =
toContactProfile :: (ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, Maybe Preferences) -> Profile
toContactProfile (displayName, fullName, image, contactLink, preferences) = Profile {displayName, fullName, image, contactLink, preferences}
createUserContactLink :: DB.Connection -> User -> ConnId -> CreatedLinkContact -> SubscriptionMode -> ExceptT StoreError IO ()
createUserContactLink db User {userId} agentConnId (CCLink cReq shortLink) subMode =
createUserContactLink :: DB.Connection -> User -> ConnId -> CreatedLinkContact -> Bool -> SubscriptionMode -> ExceptT StoreError IO ()
createUserContactLink db User {userId} agentConnId (CCLink cReq shortLink) shortLinkDataSet subMode =
checkConstraint SEDuplicateContactLink . liftIO $ do
currentTs <- getCurrentTime
DB.execute
db
"INSERT INTO user_contact_links (user_id, conn_req_contact, short_link_contact, created_at, updated_at) VALUES (?,?,?,?,?)"
(userId, cReq, shortLink, currentTs, currentTs)
"INSERT INTO user_contact_links (user_id, conn_req_contact, short_link_contact, short_link_data_set, created_at, updated_at) VALUES (?,?,?,?,?,?)"
(userId, cReq, shortLink, BI shortLinkDataSet, currentTs, currentTs)
userContactLinkId <- insertedRowId db
void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId ConnNew initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff
@@ -451,6 +451,7 @@ data UserMsgReceiptSettings = UserMsgReceiptSettings
data UserContactLink = UserContactLink
{ connLinkContact :: CreatedLinkContact,
shortLinkDataSet :: Bool,
autoAccept :: Maybe AutoAccept
}
deriving (Show)
@@ -472,9 +473,9 @@ $(J.deriveJSON defaultJSON ''AutoAccept)
$(J.deriveJSON defaultJSON ''UserContactLink)
toUserContactLink :: (ConnReqContact, Maybe ShortLinkContact, BoolInt, BoolInt, BoolInt, Maybe MsgContent) -> UserContactLink
toUserContactLink (connReq, shortLink, BI autoAccept, BI businessAddress, BI acceptIncognito, autoReply) =
UserContactLink (CCLink connReq shortLink) $
toUserContactLink :: (ConnReqContact, Maybe ShortLinkContact, BoolInt, BoolInt, BoolInt, BoolInt, Maybe MsgContent) -> UserContactLink
toUserContactLink (connReq, shortLink, BI shortLinkDataSet, BI autoAccept, BI businessAddress, BI acceptIncognito, autoReply) =
UserContactLink (CCLink connReq shortLink) shortLinkDataSet $
if autoAccept then Just AutoAccept {businessAddress, acceptIncognito, autoReply} else Nothing
getUserAddress :: DB.Connection -> User -> ExceptT StoreError IO UserContactLink
@@ -488,7 +489,7 @@ getUserContactLinkById db userId userContactLinkId =
DB.query
db
[sql|
SELECT conn_req_contact, short_link_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, short_link_data_set, 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 user_contact_link_id = ?
|]
@@ -524,7 +525,7 @@ getUserContactLinkViaShortLink db User {userId} shortLink =
userContactLinkQuery :: Query
userContactLinkQuery =
[sql|
SELECT conn_req_contact, short_link_contact, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content
SELECT conn_req_contact, short_link_contact, short_link_data_set, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content
FROM user_contact_links
|]
@@ -534,10 +535,12 @@ setUserContactLinkShortLink db userContactLinkId shortLink =
db
[sql|
UPDATE user_contact_links
SET short_link_contact = ?
SET short_link_contact = ?,
short_link_data_set = ?,
auto_accept_incognito = ?
WHERE user_contact_link_id = ?
|]
(shortLink, userContactLinkId)
(shortLink, BI True, BI False, userContactLinkId)
getContactWithoutConnViaAddress :: DB.Connection -> VersionRangeChat -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe Contact)
getContactWithoutConnViaAddress db vr user@User {userId} (cReqSchema1, cReqSchema2) = do
@@ -14,6 +14,8 @@ ALTER TABLE contacts ADD COLUMN conn_short_link_to_connect BLOB;
ALTER TABLE contacts ADD COLUMN contact_request_id INTEGER REFERENCES contact_requests ON DELETE SET NULL;
CREATE INDEX idx_contacts_contact_request_id ON contacts(contact_request_id);
ALTER TABLE user_contact_links ADD COLUMN short_link_data_set INTEGER NOT NULL DEFAULT 0;
ALTER TABLE groups ADD COLUMN conn_full_link_to_connect BLOB;
ALTER TABLE groups ADD COLUMN conn_short_link_to_connect BLOB;
ALTER TABLE groups ADD COLUMN conn_link_started_connection INTEGER NOT NULL DEFAULT 0;
@@ -28,6 +30,8 @@ ALTER TABLE contacts DROP COLUMN conn_short_link_to_connect;
DROP INDEX idx_contacts_contact_request_id;
ALTER TABLE contacts DROP COLUMN contact_request_id;
ALTER TABLE user_contact_links DROP COLUMN short_link_data_set;
ALTER TABLE groups DROP COLUMN conn_full_link_to_connect;
ALTER TABLE groups DROP COLUMN conn_short_link_to_connect;
ALTER TABLE groups DROP COLUMN conn_link_started_connection;
@@ -3062,7 +3062,7 @@ Plan:
SEARCH commands USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT conn_req_contact, short_link_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, short_link_data_set, 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 user_contact_link_id = ?
@@ -4854,21 +4854,21 @@ CORRELATED SCALAR SUBQUERY 1
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
SELECT conn_req_contact, short_link_contact, short_link_data_set, 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
SELECT conn_req_contact, short_link_contact, short_link_data_set, 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
SELECT conn_req_contact, short_link_contact, short_link_data_set, auto_accept, business_address, auto_accept_incognito, auto_reply_msg_content
FROM user_contact_links
WHERE user_id = ? AND short_link_contact = ?
Plan:
@@ -5022,19 +5022,6 @@ SEARCH u USING INTEGER PRIMARY KEY (rowid=?)
SEARCH uct USING INTEGER PRIMARY KEY (rowid=?)
SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.image, ucp.contact_link, ucp.preferences,
u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes
FROM users u
JOIN contacts uct ON uct.contact_id = u.contact_id
JOIN contact_profiles ucp ON ucp.contact_profile_id = uct.contact_profile_id
JOIN contact_requests cr ON cr.user_id = u.user_id WHERE cr.contact_request_id = ?
Plan:
SEARCH cr USING INTEGER PRIMARY KEY (rowid=?)
SEARCH u USING INTEGER PRIMARY KEY (rowid=?)
SEARCH uct USING INTEGER PRIMARY KEY (rowid=?)
SEARCH ucp USING INTEGER PRIMARY KEY (rowid=?)
Query:
SELECT u.user_id, u.agent_user_id, u.contact_id, ucp.contact_profile_id, u.active_user, u.active_order, u.local_display_name, ucp.full_name, ucp.image, ucp.contact_link, ucp.preferences,
u.show_ntfs, u.send_rcpts_contacts, u.send_rcpts_small_groups, u.view_pwd_hash, u.view_pwd_salt, u.user_member_profile_updated_at, u.ui_themes
@@ -5648,10 +5635,10 @@ SEARCH connections USING INTEGER PRIMARY KEY (rowid=?)
Query: INSERT INTO temp_conn_ids (conn_id) VALUES (?)
Plan:
Query: INSERT INTO user_contact_links (user_id, conn_req_contact, short_link_contact, created_at, updated_at) VALUES (?,?,?,?,?)
Query: INSERT INTO user_contact_links (user_id, conn_req_contact, short_link_contact, short_link_data_set, created_at, updated_at) VALUES (?,?,?,?,?,?)
Plan:
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 (?,?,?,?,?,?,?,?,?,?)
Query: INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, short_link_contact, short_link_data_set, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?)
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,?,?,?,?,?)
@@ -5880,7 +5867,7 @@ Query: SELECT user_contact_link_id FROM contact_requests WHERE contact_request_i
Plan:
SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?)
Query: SELECT user_contact_link_id, conn_req_contact, short_link_contact, group_link_id, 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, short_link_data_set, group_link_id, group_link_member_role FROM user_contact_links WHERE user_id = ? AND group_id = ? LIMIT 1
Plan:
SEARCH user_contact_links USING INDEX idx_user_contact_links_group_id (group_id=?)
@@ -332,6 +332,7 @@ CREATE TABLE user_contact_links(
group_link_member_role TEXT NULL,
business_address INTEGER DEFAULT 0,
short_link_contact BLOB,
short_link_data_set INTEGER NOT NULL DEFAULT 0,
UNIQUE(user_id, local_display_name)
);
CREATE TABLE contact_requests(
+6 -6
View File
@@ -50,7 +50,7 @@ import Simplex.Chat.Operators
import Simplex.Chat.Protocol
import Simplex.Chat.Remote.AppVersion (AppVersion (..), pattern AppVersionRange)
import Simplex.Chat.Remote.Types
import Simplex.Chat.Store (AutoAccept (..), StoreError (..), UserContactLink (..))
import Simplex.Chat.Store (AutoAccept (..), GroupLink (..), StoreError (..), UserContactLink (..))
import Simplex.Chat.Styled
import Simplex.Chat.Types
import Simplex.Chat.Types.Preferences
@@ -233,8 +233,8 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte
CRGroupUpdated u g g' m -> ttyUser u $ viewGroupUpdated g g' m
CRGroupProfile u g -> ttyUser u $ viewGroupProfile g
CRGroupDescription u g -> ttyUser u $ viewGroupDescription g
CRGroupLinkCreated u g ccLink mRole -> ttyUser u $ groupLink_ "Group link is created!" g ccLink mRole
CRGroupLink u g ccLink mRole -> ttyUser u $ groupLink_ "Group link:" g ccLink mRole
CRGroupLinkCreated u g gLink -> ttyUser u $ groupLink_ "Group link is created!" g gLink
CRGroupLink u g gLink -> ttyUser u $ groupLink_ "Group link:" g gLink
CRGroupLinkDeleted u g -> ttyUser u $ viewGroupLinkDeleted g
CRNewMemberContact u _ g m -> ttyUser u ["contact for member " <> ttyGroup' g <> " " <> ttyMember m <> " is created"]
CRNewMemberContactSentInv u _ct g m -> ttyUser u ["sent invitation to connect directly to member " <> ttyGroup' g <> " " <> ttyMember m]
@@ -1090,13 +1090,13 @@ autoAcceptStatus_ = \case
| otherwise = ""
_ -> ["auto_accept off"]
groupLink_ :: StyledString -> GroupInfo -> CreatedLinkContact -> GroupMemberRole -> [StyledString]
groupLink_ intro g (CCLink cReq shortLink) mRole =
groupLink_ :: StyledString -> GroupInfo -> GroupLink -> [StyledString]
groupLink_ intro g GroupLink {connLinkContact = CCLink cReq shortLink, acceptMemberRole} =
[ intro,
"",
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 acceptMemberRole <> " with: " <> highlight' "/c <group_link_above>",
"to show it again: " <> highlight ("/show link #" <> viewGroupName g),
"to delete it: " <> highlight ("/delete link #" <> viewGroupName g) <> " (joined members will remain connected to you)"
]
+2
View File
@@ -2811,6 +2811,8 @@ testShortLinkAddressPrepareContact =
]
alice <## "to accept: /ac bob"
alice <## "to reject: /rc bob (the sender will NOT be notified)"
alice ##> "/ac i bob"
alice <## "bad chat command: incognito not allowed for address with short link data"
alice ##> "/ac bob"
alice <## "bob (Bob): accepting contact request, you can send messages to contact"
concurrently_
+4 -4
View File
@@ -133,7 +133,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing)))
it "x.msg.new chat message with chat version range" $
"{\"v\":\"1-15\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
"{\"v\":\"1-16\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}"
##==## ChatMessage supportedChatVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing)))
it "x.msg.new quote" $
"{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}"
@@ -249,13 +249,13 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"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\"}}}}}}"
#==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} Nothing
it "x.grp.mem.new with member chat version range" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-15\",\"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.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-16\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}"
#==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} Nothing
it "x.grp.mem.intro" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"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} Nothing
it "x.grp.mem.intro with member chat version range" $
"{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-15\",\"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\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-16\",\"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 = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} Nothing
it "x.grp.mem.intro with member restrictions" $
"{\"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\"}}}}}}"
@@ -270,7 +270,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do
"{\"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}
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-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-15\",\"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-16\",\"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}
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\"}}}}}"