core, ios: create contact requests with attached contact (#5967)

This commit is contained in:
spaced4ndy
2025-06-09 16:18:01 +00:00
committed by GitHub
parent ed97606003
commit 2bc4836338
32 changed files with 663 additions and 305 deletions
+4 -4
View File
@@ -766,7 +766,7 @@ enum ChatResponse1: Decodable, ChatAPIResult {
case userContactLinkCreated(user: User, connLinkContact: CreatedConnLink)
case userContactLinkDeleted(user: User)
case acceptingContactRequest(user: UserRef, contact: Contact)
case contactRequestRejected(user: UserRef)
case contactRequestRejected(user: UserRef, contactRequest: UserContactRequest, contact_: Contact?)
case networkStatuses(user_: UserRef?, networkStatuses: [ConnNetworkStatus])
case newChatItems(user: UserRef, chatItems: [AChatItem])
case groupChatItemsDeleted(user: UserRef, groupInfo: GroupInfo, chatItemIDs: Set<Int64>, byUser: Bool, member_: GroupMember?)
@@ -842,7 +842,7 @@ enum ChatResponse1: Decodable, ChatAPIResult {
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))
case .contactRequestRejected: return noDetails
case let .contactRequestRejected(u, contactRequest, contact_): return withUser(u, "contactRequest: \(String(describing: contactRequest))\ncontact_: \(String(describing: contact_))")
case let .networkStatuses(u, statuses): return withUser(u, String(describing: statuses))
case let .newChatItems(u, chatItems):
let itemsString = chatItems.map { chatItem in String(describing: chatItem) }.joined(separator: "\n")
@@ -1028,7 +1028,7 @@ enum ChatEvent: Decodable, ChatAPIResult {
case contactConnected(user: UserRef, contact: Contact, userCustomProfile: Profile?)
case contactConnecting(user: UserRef, contact: Contact)
case contactSndReady(user: UserRef, contact: Contact)
case receivedContactRequest(user: UserRef, contactRequest: UserContactRequest)
case receivedContactRequest(user: UserRef, contactRequest: UserContactRequest, contact_: Contact?)
case contactUpdated(user: UserRef, toContact: Contact)
case groupMemberUpdated(user: UserRef, groupInfo: GroupInfo, fromMember: GroupMember, toMember: GroupMember)
case contactsMerged(user: UserRef, intoContact: Contact, mergedContact: Contact)
@@ -1179,7 +1179,7 @@ enum ChatEvent: Decodable, ChatAPIResult {
case let .contactConnected(u, contact, _): return withUser(u, String(describing: contact))
case let .contactConnecting(u, contact): return withUser(u, String(describing: contact))
case let .contactSndReady(u, contact): return withUser(u, String(describing: contact))
case let .receivedContactRequest(u, contactRequest): return withUser(u, String(describing: contactRequest))
case let .receivedContactRequest(u, contactRequest, contact_): return withUser(u, "contactRequest: \(String(describing: contactRequest))\ncontact_: \(String(describing: contact_))")
case let .contactUpdated(u, toContact): return withUser(u, String(describing: toContact))
case let .groupMemberUpdated(u, groupInfo, fromMember, toMember): return withUser(u, "groupInfo: \(groupInfo)\nfromMember: \(fromMember)\ntoMember: \(toMember)")
case let .contactsMerged(u, intoContact, mergedContact): return withUser(u, "intoContact: \(intoContact)\nmergedContact: \(mergedContact)")
+1
View File
@@ -1216,6 +1216,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
+4 -2
View File
@@ -63,7 +63,7 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
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, contactRequest: contactRequest) }
Task { await acceptContactRequest(incognito: incognito, contactRequestId: contactRequest.apiId) }
} else {
chatModel.ntfContactRequest = NTFContactRequest(incognito: incognito, chatId: chatId)
}
@@ -161,7 +161,9 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject {
identifier: ntfActionAcceptContact,
title: NSLocalizedString("Accept", comment: "accept contact request via notification"),
options: .foreground
), UNNotificationAction(
),
// 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
+46 -16
View File
@@ -1275,9 +1275,9 @@ func apiAcceptContactRequest(incognito: Bool, contactReqId: Int64) async -> Cont
return nil
}
func apiRejectContactRequest(contactReqId: Int64) async throws {
func apiRejectContactRequest(contactReqId: Int64) async throws -> Contact? {
let r: ChatResponse1 = try await chatSendCmd(.apiRejectContact(contactReqId: contactReqId))
if case .contactRequestRejected = r { return }
if case let .contactRequestRejected(_, _, contact_) = r { return contact_ }
throw r.unexpected
}
@@ -1511,11 +1511,15 @@ func networkErrorAlert<R>(_ res: APIResult<R>) -> Alert? {
}
}
func acceptContactRequest(incognito: Bool, contactRequest: UserContactRequest) async {
if let contact = await apiAcceptContactRequest(incognito: incognito, contactReqId: contactRequest.apiId) {
func acceptContactRequest(incognito: Bool, contactRequestId: Int64) async {
if let contact = await apiAcceptContactRequest(incognito: incognito, contactReqId: contactRequestId) {
let chat = Chat(chatInfo: ChatInfo.direct(contact: contact), chatItems: [])
await MainActor.run {
ChatModel.shared.replaceChat(contactRequest.id, chat)
if contact.contactRequestId != nil { // means contact request was initially created with contact, so we don't need to replace it
ChatModel.shared.updateContact(contact)
} else {
ChatModel.shared.replaceChat(contactRequestChatId(contactRequestId), chat)
}
NetworkModel.shared.setContactNetworkStatus(contact, .connected)
}
if contact.sndReady {
@@ -1528,12 +1532,27 @@ func acceptContactRequest(incognito: Bool, contactRequest: UserContactRequest) a
}
}
func rejectContactRequest(_ contactRequest: UserContactRequest) async {
func rejectContactRequest(_ contactRequestId: Int64, dismissToChatList: Bool = false) async {
do {
try await apiRejectContactRequest(contactReqId: contactRequest.apiId)
DispatchQueue.main.async { ChatModel.shared.removeChat(contactRequest.id) }
let contact_ = try await apiRejectContactRequest(contactReqId: contactRequestId)
await MainActor.run {
if let contact = contact_ { // means contact request was initially created with contact, so we need to remove contact chat
ChatModel.shared.removeChat(contact.id)
} else {
ChatModel.shared.removeChat(contactRequestChatId(contactRequestId))
}
if dismissToChatList {
ChatModel.shared.chatId = nil
}
}
} catch let error {
logger.error("rejectContactRequest: \(responseError(error))")
await MainActor.run {
showAlert(
NSLocalizedString("Error rejecting contact request", comment: "alert title"),
message: responseError(error)
)
}
}
}
@@ -2106,17 +2125,28 @@ func processReceivedMsg(_ res: ChatEvent) async {
await MainActor.run {
n.setContactNetworkStatus(contact, .connected)
}
case let .receivedContactRequest(user, contactRequest):
case let .receivedContactRequest(user, contactRequest, contact_):
if active(user) {
let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest)
await MainActor.run {
if m.hasChat(contactRequest.id) {
m.updateChatInfo(cInfo)
if let contact = contact_ { // means contact request was created with contact, so we need to add/update contact chat
if m.hasChat(contact.id) {
m.updateContact(contact)
} else {
m.addChat(Chat(
chatInfo: ChatInfo.direct(contact: contact),
chatItems: []
))
}
} else {
m.addChat(Chat(
chatInfo: cInfo,
chatItems: []
))
let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest)
if m.hasChat(contactRequest.id) {
m.updateChatInfo(cInfo)
} else {
m.addChat(Chat(
chatInfo: cInfo,
chatItems: []
))
}
}
}
}
+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, contactRequest: contactRequest) }
Task { await acceptContactRequest(incognito: ncr.incognito, contactRequestId: contactRequest.apiId) }
}
}
} catch let error {
+4 -2
View File
@@ -119,7 +119,8 @@ struct ChatView: View {
let reason = chat.chatInfo.userCantSendReason
let composeEnabled = (
chat.chatInfo.sendMsgEnabled ||
(chat.chatInfo.groupInfo?.nextConnectPrepared ?? false) // allow to join prepared group without message
(chat.chatInfo.groupInfo?.nextConnectPrepared ?? false) || // allow to join prepared group without message
(chat.chatInfo.contact?.nextAcceptContactRequest ?? false) // allow to accept or reject contact request
)
ComposeView(
chat: chat,
@@ -764,7 +765,8 @@ struct ChatView: View {
if case let .direct(contact) = chat.chatInfo,
!contact.sndReady,
contact.active,
!contact.sendMsgToConnect {
!contact.sendMsgToConnect,
!contact.nextAcceptContactRequest {
Text("connecting…")
.font(.caption)
.foregroundColor(theme.colors.secondary)
@@ -350,6 +350,12 @@ struct ComposeView: View {
var body: some View {
VStack(spacing: 0) {
Divider()
if let contact = chat.chatInfo.contact,
contact.nextAcceptContactRequest,
let contactRequestId = contact.contactRequestId {
ContextContactRequestActionsView(contactRequestId: contactRequestId)
}
if let groupInfo = chat.chatInfo.groupInfo,
case let .groupChatScopeContext(groupScopeInfo) = im.secondaryIMFilter,
case let .memberSupport(member) = groupScopeInfo,
@@ -0,0 +1,89 @@
//
// ContextContactRequestActionsView.swift
// SimpleX (iOS)
//
// Created by spaced4ndy on 02.05.2025.
// Copyright © 2025 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
struct ContextContactRequestActionsView: View {
@EnvironmentObject var theme: AppTheme
var contactRequestId: Int64
var body: some View {
HStack(spacing: 0) {
ZStack {
Text("Reject")
.foregroundColor(.red)
}
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
.onTapGesture {
showRejectRequestAlert(contactRequestId)
}
ZStack {
Text("Accept")
.foregroundColor(theme.colors.primary)
}
.frame(maxWidth: .infinity)
.contentShape(Rectangle())
.onTapGesture {
showAcceptRequestAlert(contactRequestId)
}
}
.frame(minHeight: 54)
.frame(maxWidth: .infinity)
.background(.thinMaterial)
}
}
func showRejectRequestAlert(_ contactRequestId: Int64) {
showAlert(
title: NSLocalizedString("Reject contact request", comment: "alert title"),
message: NSLocalizedString("The sender will NOT be notified", comment: "alert message"),
buttonTitle: "Reject",
buttonAction: {
Task {
await rejectContactRequest(contactRequestId, dismissToChatList: true)
}
},
cancelButton: true
)
}
func showAcceptRequestAlert(_ contactRequestId: Int64) {
showAlert(
NSLocalizedString("Accept contact request", comment: "alert title"),
actions: {[
UIAlertAction(
title: NSLocalizedString("Accept", 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
)
]}
)
}
#Preview {
ContextContactRequestActionsView(
contactRequestId: 1
)
}
@@ -122,29 +122,51 @@ struct ChatListNavLink: View {
label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) }
)
.frameCompat(height: dynamicRowHeight)
.swipeActions(edge: .leading, allowsFullSwipe: true) {
markReadButton()
toggleFavoriteButton()
toggleNtfsButton(chat: chat)
.if(!contact.nextAcceptContactRequest) { v in
v.swipeActions(edge: .leading, allowsFullSwipe: true) {
markReadButton()
toggleFavoriteButton()
toggleNtfsButton(chat: chat)
}
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
tagChatButton(chat)
if !chat.chatItems.isEmpty {
clearChatButton()
if contact.nextAcceptContactRequest,
let contactRequestId = contact.contactRequestId {
Button {
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)
}
.tint(.indigo)
Button {
AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequestId))
} label: {
SwipeLabel(NSLocalizedString("Reject", comment: "swipe action"), systemImage: "multiply.fill", inverted: oneHandUI)
}
.tint(.red)
} else {
tagChatButton(chat)
if !chat.chatItems.isEmpty {
clearChatButton()
}
Button {
deleteContactDialog(
chat,
contact,
dismissToChatList: false,
showAlert: { alert = $0 },
showActionSheet: { actionSheet = $0 },
showSheetContent: { sheet = $0 }
)
} label: {
deleteLabel
}
.tint(.red)
}
Button {
deleteContactDialog(
chat,
contact,
dismissToChatList: false,
showAlert: { alert = $0 },
showActionSheet: { actionSheet = $0 },
showSheetContent: { sheet = $0 }
)
} label: {
deleteLabel
}
.tint(.red)
}
}
}
@@ -436,17 +458,17 @@ struct ChatListNavLink: View {
.frameCompat(height: dynamicRowHeight)
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button {
Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) }
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, contactRequest: contactRequest) }
Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequest.apiId) }
} label: {
SwipeLabel(NSLocalizedString("Accept incognito", comment: "swipe action"), systemImage: "theatermasks.fill", inverted: oneHandUI)
}
.tint(.indigo)
Button {
AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequest))
AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequest.apiId))
} label: {
SwipeLabel(NSLocalizedString("Reject", comment: "swipe action"), systemImage: "multiply.fill", inverted: oneHandUI)
}
@@ -455,9 +477,9 @@ struct ChatListNavLink: View {
.contentShape(Rectangle())
.onTapGesture { showContactRequestDialog = true }
.confirmationDialog("Accept connection request?", isPresented: $showContactRequestDialog, titleVisibility: .visible) {
Button("Accept") { Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) } }
Button("Accept incognito") { Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) } }
Button("Reject (sender NOT notified)", role: .destructive) { Task { await rejectContactRequest(contactRequest) } }
Button("Accept") { Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) } }
Button("Accept incognito") { Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequest.apiId) } }
Button("Reject (sender NOT notified)", role: .destructive) { Task { await rejectContactRequest(contactRequest.apiId) } }
}
}
@@ -619,12 +641,12 @@ extension View {
}
}
func rejectContactRequestAlert(_ contactRequest: UserContactRequest) -> Alert {
func rejectContactRequestAlert(_ contactRequestId: Int64) -> Alert {
Alert(
title: Text("Reject contact request"),
message: Text("The sender will NOT be notified"),
primaryButton: .destructive(Text("Reject")) {
Task { await rejectContactRequest(contactRequest) }
Task { await rejectContactRequest(contactRequestId) }
},
secondaryButton: .cancel()
)
@@ -164,7 +164,16 @@ struct ChatPreviewView: View {
let t = Text(chat.chatInfo.chatViewName).font(.title3).fontWeight(.bold)
switch chat.chatInfo {
case let .direct(contact):
previewTitle(contact.verified == true ? verifiedIcon + t : t).foregroundColor(deleting ? Color.secondary : nil)
let color = (
deleting
? Color.secondary
: (
contact.nextAcceptContactRequest
? theme.colors.primary
: nil
)
)
previewTitle(contact.verified == true ? verifiedIcon + t : t).foregroundColor(color)
case let .group(groupInfo, _):
let v = previewTitle(t)
switch (groupInfo.membership.memberStatus) {
@@ -28,6 +28,8 @@ struct ContactListNavLink: View {
switch contactType {
case .recent:
recentContactNavLink(contact)
case .contactWithRequest:
contactWithRequestNavLink(contact)
case .chatDeleted:
deletedChatNavLink(contact)
case .card:
@@ -78,6 +80,36 @@ struct ContactListNavLink: View {
}
}
func contactWithRequestNavLink(_ contact: Contact) -> some View {
Button {
dismissAllSheets(animated: true) {
ItemsModel.shared.loadOpenChat(contact.id)
}
} label: {
contactRequestPreview()
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
if let contactRequestId = contact.contactRequestId {
Button {
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")
}
.tint(.indigo)
Button {
alert = SomeAlert(alert: rejectContactRequestAlert(contactRequestId), id: "rejectContactRequestAlert")
} label: {
Label("Reject", systemImage: "multiply")
}
.tint(.red)
}
}
}
func deletedChatNavLink(_ contact: Contact) -> some View {
Button {
Task {
@@ -219,36 +251,36 @@ struct ContactListNavLink: View {
Button {
showContactRequestDialog = true
} label: {
contactRequestPreview(contactRequest)
contactRequestPreview()
}
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
Button {
Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) }
Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) }
} label: { Label("Accept", systemImage: "checkmark") }
.tint(theme.colors.primary)
Button {
Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) }
Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequest.apiId) }
} label: {
Label("Accept incognito", systemImage: "theatermasks")
}
.tint(.indigo)
Button {
alert = SomeAlert(alert: rejectContactRequestAlert(contactRequest), id: "rejectContactRequestAlert")
alert = SomeAlert(alert: rejectContactRequestAlert(contactRequest.apiId), id: "rejectContactRequestAlert")
} label: {
Label("Reject", systemImage: "multiply")
}
.tint(.red)
}
.confirmationDialog("Accept connection request?", isPresented: $showContactRequestDialog, titleVisibility: .visible) {
Button("Accept") { Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) } }
Button("Accept incognito") { Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) } }
Button("Reject (sender NOT notified)", role: .destructive) { Task { await rejectContactRequest(contactRequest) } }
Button("Accept") { Task { await acceptContactRequest(incognito: false, contactRequestId: contactRequest.apiId) } }
Button("Accept incognito") { Task { await acceptContactRequest(incognito: true, contactRequestId: contactRequest.apiId) } }
Button("Reject (sender NOT notified)", role: .destructive) { Task { await rejectContactRequest(contactRequest.apiId) } }
}
}
func contactRequestPreview(_ contactRequest: UserContactRequest) -> some View {
func contactRequestPreview() -> some View {
HStack{
ProfileImage(imageStr: contactRequest.image, size: 30)
ProfileImage(imageStr: chat.chatInfo.image, size: 30)
Text(chat.chatInfo.chatViewName)
.foregroundColor(.accentColor)
@@ -10,7 +10,7 @@ import SwiftUI
import SimpleXChat
enum ContactType: Int {
case card, request, recent, chatDeleted, unlisted
case card, contactWithRequest, request, recent, chatDeleted, unlisted
}
struct NewChatMenuButton: View {
@@ -42,7 +42,7 @@ private var indent: CGFloat = 36
struct NewChatSheet: View {
@EnvironmentObject var theme: AppTheme
@State private var baseContactTypes: [ContactType] = [.card, .request, .recent]
@State private var baseContactTypes: [ContactType] = [.card, .contactWithRequest, .request, .recent]
@EnvironmentObject var chatModel: ChatModel
@State private var searchMode = false
@FocusState var searchFocussed: Bool
@@ -191,7 +191,9 @@ func chatContactType(_ chat: Chat) -> ContactType {
case .contactRequest:
return .request
case let .direct(contact):
if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active {
if contact.nextAcceptContactRequest {
return .contactWithRequest
} else if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active {
return .card
} else if contact.chatDeleted {
return .chatDeleted
@@ -184,6 +184,7 @@
64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2BF29F9688300B38D5F /* UserAddressView.swift */; };
64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; };
64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */; };
64E5E3632DF71A4E00A4D530 /* ContextContactRequestActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E5E3622DF71A4E00A4D530 /* ContextContactRequestActionsView.swift */; };
64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; };
64EEB0F72C353F1C00972D62 /* ServersSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */; };
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; };
@@ -548,6 +549,7 @@
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAddressLearnMore.swift; sourceTree = "<group>"; };
64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactLearnMore.swift; sourceTree = "<group>"; };
64DAE1502809D9F5000DA960 /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = "<group>"; };
64E5E3622DF71A4E00A4D530 /* ContextContactRequestActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextContactRequestActionsView.swift; sourceTree = "<group>"; };
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = "<group>"; };
64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersSummaryView.swift; sourceTree = "<group>"; };
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = "<group>"; };
@@ -1084,6 +1086,7 @@
644EFFDD292BCD9D00525D5B /* ComposeVoiceView.swift */,
D72A9087294BD7A70047C86D /* NativeTextEditor.swift */,
64A77A012DC4AD6100FDEF2F /* ContextPendingMemberActionsView.swift */,
64E5E3622DF71A4E00A4D530 /* ContextContactRequestActionsView.swift */,
);
path = ComposeMessage;
sourceTree = "<group>";
@@ -1576,6 +1579,7 @@
5C5E5D3B2824468B00B0488A /* ActiveCallView.swift in Sources */,
B70A39732D24090D00E80A5F /* TagListView.swift in Sources */,
5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */,
64E5E3632DF71A4E00A4D530 /* ContextContactRequestActionsView.swift in Sources */,
6440CA00288857A10062C672 /* CIEventView.swift in Sources */,
5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */,
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */,
+7 -1
View File
@@ -1719,6 +1719,7 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable {
var updatedAt: Date
var chatTs: Date?
public var connLinkToConnect: CreatedConnLink?
public var contactRequestId: Int64?
var contactGroupMemberId: Int64?
var contactGrpInvSent: Bool
public var chatTags: [Int64]
@@ -1733,6 +1734,7 @@ public struct Contact: Identifiable, Decodable, NamedChat, Hashable {
public var active: Bool { get { contactStatus == .active } }
public var nextSendGrpInv: Bool { get { contactGroupMemberId != nil && !contactGrpInvSent } }
public var nextConnectPrepared: Bool { get { connLinkToConnect != nil && activeConn == nil } }
public var nextAcceptContactRequest: Bool { get { contactRequestId != nil && activeConn == nil } }
public var sendMsgToConnect: Bool { get { nextSendGrpInv || nextConnectPrepared } }
public var displayName: String { localAlias == "" ? profile.displayName : localAlias }
public var fullName: String { get { profile.fullName } }
@@ -1908,7 +1910,7 @@ public struct UserContactRequest: Decodable, NamedChat, Hashable {
var createdAt: Date
public var updatedAt: Date
public var id: ChatId { get { "<@\(contactRequestId)" } }
public var id: ChatId { get { contactRequestChatId(contactRequestId) } }
public var apiId: Int64 { get { contactRequestId } }
var ready: Bool { get { true } }
public var displayName: String { get { profile.displayName } }
@@ -1927,6 +1929,10 @@ public struct UserContactRequest: Decodable, NamedChat, Hashable {
)
}
public func contactRequestChatId(_ contactRequestId: Int64) -> ChatId {
return "<@\(contactRequestId)"
}
public struct PendingContactConnection: Decodable, NamedChat, Hashable {
public var pccConnId: Int64
var pccAgentConnId: String
+2 -2
View File
@@ -668,7 +668,7 @@ data ChatResponse
| CRContactsList {user :: User, contacts :: [Contact]}
| CRUserContactLink {user :: User, contactLink :: UserContactLink}
| CRUserContactLinkUpdated {user :: User, contactLink :: UserContactLink}
| CRContactRequestRejected {user :: User, contactRequest :: UserContactRequest}
| CRContactRequestRejected {user :: User, contactRequest :: UserContactRequest, contact_ :: Maybe Contact}
| CRUserAcceptedGroupSent {user :: User, groupInfo :: GroupInfo, hostContact :: Maybe Contact}
| CRUserDeletedMembers {user :: User, groupInfo :: GroupInfo, members :: [GroupMember], withMessages :: Bool}
| CRGroupsList {user :: User, groups :: [(GroupInfo, GroupSummary)]}
@@ -781,7 +781,7 @@ data ChatEvent
| CEvtGroupMemberUpdated {user :: User, groupInfo :: GroupInfo, fromMember :: GroupMember, toMember :: GroupMember}
| CEvtContactsMerged {user :: User, intoContact :: Contact, mergedContact :: Contact, updatedContact :: Contact}
| CEvtContactDeletedByContact {user :: User, contact :: Contact}
| CEvtReceivedContactRequest {user :: User, contactRequest :: UserContactRequest}
| CEvtReceivedContactRequest {user :: User, contactRequest :: UserContactRequest, contact_ :: Maybe Contact}
| CEvtAcceptingContactRequest {user :: User, contact :: Contact} -- there is the same command response
| CEvtAcceptingBusinessRequest {user :: User, groupInfo :: GroupInfo}
| CEvtContactRequestAlreadyAccepted {user :: User, contact :: Contact}
+16 -5
View File
@@ -1144,6 +1144,12 @@ 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
@@ -1163,12 +1169,17 @@ processChatCommand' vr = \case
APIRejectContact connReqId -> withUser $ \user -> do
userContactLinkId <- withFastStore $ \db -> getUserContactLinkIdByCReq db connReqId
withUserContactLock "rejectContact" userContactLinkId $ do
cReq@UserContactRequest {agentContactConnId = AgentConnId connId, agentInvitationId = AgentInvId invId} <-
withFastStore $ \db ->
getContactRequest db user connReqId
`storeFinally` liftIO (deleteContactRequest db user connReqId)
(cReq@UserContactRequest {agentContactConnId = AgentConnId connId, agentInvitationId = AgentInvId invId}, ct_) <-
withFastStore $ \db -> do
cReq@UserContactRequest {contactId_} <- getContactRequest db user connReqId
ct_ <- forM contactId_ $ \contactId -> do
ct <- getContact db vr user contactId
deleteContact db user ct
pure ct
liftIO $ deleteContactRequest db user connReqId
pure (cReq, ct_)
withAgent $ \a -> rejectContact a connId invId
pure $ CRContactRequestRejected user cReq
pure $ CRContactRequestRejected user cReq ct_
APISendCallInvitation contactId callType -> withUser $ \user -> do
-- party initiating call
ct <- withFastStore $ \db -> getContact db vr user contactId
+55 -36
View File
@@ -878,12 +878,17 @@ acceptContactRequest user@User {userId} UserContactRequest {agentInvitationId =
Nothing -> do
incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing
connId <- withAgent $ \a -> prepareConnectionToAccept a True invId pqSup'
(ct, conn) <- withStore' $ \db -> createAcceptedContact db user connId chatV cReqChatVRange cName profileId cp userContactLinkId xContactId incognitoProfile subMode pqSup' False
(ct, conn) <- withStore' $ \db -> createContactFromRequest db user userContactLinkId connId chatV cReqChatVRange cName profileId cp xContactId incognitoProfile subMode pqSup' False
pure (ct, conn, incognitoProfile)
Just contactId -> do
ct <- withFastStore $ \db -> getContact db vr user contactId
case contactConn ct of
Nothing -> throwChatError $ CECommandError "contact has no connection"
Nothing -> do
incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing
connId <- withAgent $ \a -> prepareConnectionToAccept a True invId pqSup'
currentTs <- liftIO getCurrentTime
conn <- withStore' $ \db -> createAcceptedContactConn db user userContactLinkId contactId connId chatV cReqChatVRange pqSup' incognitoProfile subMode currentTs
pure (ct {activeConn = Just conn}, conn, incognitoProfile)
Just conn@Connection {customUserProfileId} -> do
incognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId
pure (ct, conn, ExistingIncognito <$> incognitoProfile)
@@ -891,32 +896,41 @@ acceptContactRequest user@User {userId} UserContactRequest {agentInvitationId =
dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend
(ct,conn,) <$> withAgent (\a -> acceptContact a (aConnId conn) True invId dm pqSup' subMode)
acceptContactRequestAsync :: User -> UserContactRequest -> Maybe IncognitoProfile -> PQSupport -> CM Contact
acceptContactRequestAsync user cReq@UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile pqSup = do
subMode <- chatReadVar subscriptionMode
let profileToSend = profileToSendOnAccept user incognitoProfile False
vr <- chatVersionRange
let chatV = vr `peerConnChatVersion` cReqChatVRange
(cmdId, acId) <- agentAcceptContactAsync user True invId (XInfo profileToSend) subMode pqSup chatV
withStore' $ \db -> do
(ct, Connection {connId}) <- createAcceptedContact db user acId chatV cReqChatVRange cName profileId p userContactLinkId xContactId incognitoProfile subMode pqSup True
deleteContactRequestRec db user cReq
setCommandConnId db user cmdId connId
pure ct
acceptContactRequestAsync :: User -> Int64 -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> PQSupport -> Maybe IncognitoProfile -> CM Contact
acceptContactRequestAsync
user
uclId
cReqInvId
cReqChatVRange
cReqProfile
cReqXContactId_
cReqPQSup
incognitoProfile = do
subMode <- chatReadVar subscriptionMode
let profileToSend = profileToSendOnAccept user incognitoProfile False
vr <- chatVersionRange
let chatV = vr `peerConnChatVersion` cReqChatVRange
(cmdId, acId) <- agentAcceptContactAsync user True cReqInvId (XInfo profileToSend) subMode cReqPQSup chatV
withStore $ \db -> do
(ct, Connection {connId}) <- createAcceptedContact db vr user uclId acId chatV cReqChatVRange cReqProfile cReqXContactId_ cReqPQSup incognitoProfile subMode
liftIO $ setCommandConnId db user cmdId connId
pure ct
acceptGroupJoinRequestAsync :: User -> GroupInfo -> UserContactRequest -> GroupAcceptance -> GroupMemberRole -> Maybe IncognitoProfile -> CM GroupMember
acceptGroupJoinRequestAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> GroupAcceptance -> GroupMemberRole -> Maybe IncognitoProfile -> CM GroupMember
acceptGroupJoinRequestAsync
user
uclId
gInfo@GroupInfo {groupProfile, membership, businessChat}
ucr@UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange}
cReqInvId
cReqChatVRange
cReqProfile
gAccepted
gLinkMemRole
incognitoProfile = do
gVar <- asks random
let initialStatus = acceptanceToStatus (memberAdmission groupProfile) gAccepted
(groupMemberId, memberId) <- withStore $ \db -> do
liftIO $ deleteContactRequestRec db user ucr
createJoiningMember db gVar user gInfo ucr gLinkMemRole initialStatus
(groupMemberId, memberId) <- withStore $ \db ->
createJoiningMember db gVar user gInfo cReqChatVRange cReqProfile gLinkMemRole initialStatus
currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo
let Profile {displayName} = profileToSendOnAccept user incognitoProfile True
GroupMember {memberRole = userRole, memberId = userMemberId} = membership
@@ -934,21 +948,23 @@ acceptGroupJoinRequestAsync
subMode <- chatReadVar subscriptionMode
vr <- chatVersionRange
let chatV = vr `peerConnChatVersion` cReqChatVRange
connIds <- agentAcceptContactAsync user True invId msg subMode PQSupportOff chatV
connIds <- agentAcceptContactAsync user True cReqInvId msg subMode PQSupportOff chatV
withStore $ \db -> do
liftIO $ createJoiningMemberConnection db user connIds chatV ucr groupMemberId subMode
liftIO $ createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode
getGroupMemberById db vr user groupMemberId
acceptGroupJoinSendRejectAsync :: User -> GroupInfo -> UserContactRequest -> GroupRejectionReason -> CM GroupMember
acceptGroupJoinSendRejectAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> GroupRejectionReason -> CM GroupMember
acceptGroupJoinSendRejectAsync
user
uclId
gInfo@GroupInfo {groupProfile, membership}
ucr@UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange}
cReqInvId
cReqChatVRange
cReqProfile
rejectionReason = do
gVar <- asks random
(groupMemberId, memberId) <- withStore $ \db -> do
liftIO $ deleteContactRequestRec db user ucr
createJoiningMember db gVar user gInfo ucr GRObserver GSMemRejected
(groupMemberId, memberId) <- withStore $ \db ->
createJoiningMember db gVar user gInfo cReqChatVRange cReqProfile GRObserver GSMemRejected
let GroupMember {memberRole = userRole, memberId = userMemberId} = membership
msg =
XGrpLinkReject $
@@ -961,22 +977,25 @@ acceptGroupJoinSendRejectAsync
subMode <- chatReadVar subscriptionMode
vr <- chatVersionRange
let chatV = vr `peerConnChatVersion` cReqChatVRange
connIds <- agentAcceptContactAsync user False invId msg subMode PQSupportOff chatV
connIds <- agentAcceptContactAsync user False cReqInvId msg subMode PQSupportOff chatV
withStore $ \db -> do
liftIO $ createJoiningMemberConnection db user connIds chatV ucr groupMemberId subMode
liftIO $ createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode
getGroupMemberById db vr user groupMemberId
acceptBusinessJoinRequestAsync :: User -> UserContactRequest -> CM GroupInfo
acceptBusinessJoinRequestAsync :: User -> Int64 -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> CM (GroupInfo, GroupMember)
acceptBusinessJoinRequestAsync
user
ucr@UserContactRequest {contactRequestId, agentInvitationId = AgentInvId invId, cReqChatVRange} = do
uclId
cReqInvId
cReqChatVRange
cReqProfile
cReqXContactId_ = do
vr <- chatVersionRange
gVar <- asks random
let userProfile@Profile {displayName, preferences} = profileToSendOnAccept user Nothing True
groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs preferences
(gInfo, clientMember) <- withStore $ \db -> do
liftIO $ deleteContactRequest db user contactRequestId
createBusinessRequestGroup db vr gVar user ucr groupPreferences
(gInfo, clientMember) <- withStore $ \db ->
createBusinessRequestGroup db vr gVar user cReqChatVRange cReqProfile cReqXContactId_ groupPreferences
let GroupInfo {membership} = gInfo
GroupMember {memberRole = userRole, memberId = userMemberId} = membership
GroupMember {groupMemberId, memberId} = clientMember
@@ -996,12 +1015,12 @@ acceptBusinessJoinRequestAsync
}
subMode <- chatReadVar subscriptionMode
let chatV = vr `peerConnChatVersion` cReqChatVRange
connIds <- agentAcceptContactAsync user True invId msg subMode PQSupportOff chatV
withStore' $ \db -> createJoiningMemberConnection db user connIds chatV ucr groupMemberId subMode
connIds <- agentAcceptContactAsync user True cReqInvId msg subMode PQSupportOff chatV
withStore' $ \db -> createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode
let cd = CDGroupSnd gInfo Nothing
createInternalChatItem user cd (CISndGroupE2EEInfo E2EInfo {pqEnabled = PQEncOff}) Nothing
createGroupFeatureItems user cd CISndGroupFeature gInfo
pure gInfo
pure (gInfo, clientMember)
where
businessGroupProfile :: Profile -> GroupPreferences -> GroupProfile
businessGroupProfile Profile {displayName, fullName, image} groupPreferences =
+58 -44
View File
@@ -668,7 +668,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
_ -> 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)
-- 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))
@@ -1208,7 +1208,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
RcvChunkError -> badRcvFileChunk ft $ "incorrect chunk number " <> show chunkNo
processUserContactRequest :: AEvent e -> ConnectionEntity -> Connection -> UserContact -> CM ()
processUserContactRequest agentMsg connEntity conn UserContact {userContactLinkId} = case agentMsg of
processUserContactRequest agentMsg connEntity conn UserContact {userContactLinkId = uclId} = case agentMsg of
REQ invId pqSupport _ connInfo -> do
ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo
case chatMsgEvent of
@@ -1227,54 +1227,68 @@ 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
-- TODO [short links] on contact request create contact with message
-- TODO - instead of creating a contact request, create a contact that can be accepted or rejected,
-- TODO and can be opened as a chat to view message
-- TODO - see schema comments on persistence
withStore (\db -> createOrUpdateContactRequest db vr user userContactLinkId invId chatVRange p xContactId_ reqPQSup) >>= \case
CORContact contact -> toView $ CEvtContactRequestAlreadyAccepted user contact
CORGroup gInfo -> toView $ CEvtBusinessRequestAlreadyAccepted user gInfo
CORRequest cReq -> do
ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId
let (UserContactLink {connLinkContact = CCLink connReq _, autoAccept}, gLinkInfo_) = ucl
isSimplexTeam = sameConnReqContact connReq adminContactReq
v = maxVersion chatVRange
case autoAccept of
Just AutoAccept {acceptIncognito, businessAddress}
| businessAddress ->
if isSimplexTeam && v < businessChatsVersion
then do
ct <- acceptContactRequestAsync user cReq Nothing reqPQSup
ucl <- withStore $ \db -> getUserContactLinkById db userId uclId
let (UserContactLink {connLinkContact = CCLink connReq _, autoAccept}, gLinkInfo_) = ucl
isSimplexTeam = sameConnReqContact connReq adminContactReq
v = maxVersion chatVRange
case autoAccept of
Nothing ->
withStore (\db -> createOrUpdateContactRequest db vr user uclId invId chatVRange p xContactId_ reqPQSup) >>= \case
CORContact ct -> toView $ CEvtContactRequestAlreadyAccepted user ct
CORRequest cReq ct_ -> do
forM_ ct_ $ \ct ->
forM_ mc_ $ \mc ->
createInternalChatItem user (CDDirectRcv ct) (CIRcvMsgContent mc) Nothing
toView $ CEvtReceivedContactRequest user cReq ct_
Just AutoAccept {acceptIncognito, businessAddress}
| businessAddress ->
if isSimplexTeam && v < businessChatsVersion
then
maybe (pure Nothing) (\xContactId -> withStore' (\db -> getAcceptedContactByXContactId db vr user xContactId)) xContactId_ >>= \case
Just ct -> toView $ CEvtContactRequestAlreadyAccepted user ct
Nothing -> do
ct <- acceptContactRequestAsync user uclId invId chatVRange p xContactId_ reqPQSup Nothing
forM_ mc_ $ \mc ->
createInternalChatItem user (CDDirectRcv ct) (CIRcvMsgContent mc) Nothing
toView $ CEvtAcceptingContactRequest user ct
else do
gInfo <- acceptBusinessJoinRequestAsync user cReq
else
maybe (pure Nothing) (\xContactId -> withStore' (\db -> getAcceptedBusinessChatByXContactId db vr user xContactId)) xContactId_ >>= \case
Just gInfo -> toView $ CEvtBusinessRequestAlreadyAccepted user gInfo
Nothing -> do
(gInfo, clientMember) <- acceptBusinessJoinRequestAsync user uclId invId chatVRange p xContactId_
forM_ mc_ $ \mc ->
createInternalChatItem user (CDGroupRcv gInfo Nothing clientMember) (CIRcvMsgContent mc) Nothing
toView $ CEvtAcceptingBusinessRequest user gInfo
| otherwise -> case gLinkInfo_ of
| otherwise -> case gLinkInfo_ of
Nothing ->
maybe (pure Nothing) (\xContactId -> withStore' (\db -> getAcceptedContactByXContactId db vr user xContactId)) xContactId_ >>= \case
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
ct <- acceptContactRequestAsync user cReq incognitoProfile reqPQSup
ct <- acceptContactRequestAsync user uclId invId chatVRange p xContactId_ reqPQSup incognitoProfile
forM_ mc_ $ \mc ->
createInternalChatItem user (CDDirectRcv ct) (CIRcvMsgContent mc) Nothing
toView $ CEvtAcceptingContactRequest user ct
Just gli@GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do
gInfo <- withStore $ \db -> getGroupInfo db vr user groupId
acceptMember_ <- asks $ acceptMember . chatHooks . config
maybe (pure $ Right (GAAccepted, gLinkMemRole)) (\am -> liftIO $ am gInfo gli p) acceptMember_ >>= \case
Right (acceptance, useRole)
| v < groupFastLinkJoinVersion ->
messageError "processUserContactRequest: chat version range incompatible for accepting group join request"
| otherwise -> do
let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo
mem <- acceptGroupJoinRequestAsync user gInfo cReq acceptance useRole profileMode
(gInfo', mem', scopeInfo) <- mkGroupChatScope gInfo mem
createInternalChatItem user (CDGroupRcv gInfo' scopeInfo mem') (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing
toView $ CEvtAcceptingGroupJoinRequestMember user gInfo' mem'
Left rjctReason
| v < groupJoinRejectVersion ->
messageWarning $ "processUserContactRequest (group " <> groupName' gInfo <> "): joining of " <> displayName <> " is blocked"
| otherwise -> do
mem <- acceptGroupJoinSendRejectAsync user gInfo cReq rjctReason
toViewTE $ TERejectingGroupJoinRequestMember user gInfo mem rjctReason
_ -> toView $ CEvtReceivedContactRequest user cReq
Just gli@GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do
gInfo <- withStore $ \db -> getGroupInfo db vr user groupId
acceptMember_ <- asks $ acceptMember . chatHooks . config
maybe (pure $ Right (GAAccepted, gLinkMemRole)) (\am -> liftIO $ am gInfo gli p) acceptMember_ >>= \case
Right (acceptance, useRole)
| v < groupFastLinkJoinVersion ->
messageError "processUserContactRequest: chat version range incompatible for accepting group join request"
| otherwise -> do
let profileMode = ExistingIncognito <$> incognitoMembershipProfile gInfo
mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p acceptance useRole profileMode
(gInfo', mem', scopeInfo) <- mkGroupChatScope gInfo mem
createInternalChatItem user (CDGroupRcv gInfo' scopeInfo mem') (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing
toView $ CEvtAcceptingGroupJoinRequestMember user gInfo' mem'
Left rjctReason
| v < groupJoinRejectVersion ->
messageWarning $ "processUserContactRequest (group " <> groupName' gInfo <> "): joining of " <> displayName <> " is blocked"
| otherwise -> do
mem <- acceptGroupJoinSendRejectAsync user uclId gInfo invId chatVRange p rjctReason
toViewTE $ TERejectingGroupJoinRequestMember user gInfo mem rjctReason
memberCanSend :: GroupMember -> Maybe MsgScope -> CM () -> CM ()
memberCanSend m@GroupMember {memberRole} msgScope a = case msgScope of
+3 -3
View File
@@ -112,7 +112,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
[sql|
SELECT
c.contact_profile_id, c.local_display_name, c.via_group, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite,
p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect,
p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.contact_request_id,
c.contact_group_member_id, c.contact_grp_inv_sent, c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl
FROM contacts c
JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id
@@ -120,13 +120,13 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do
|]
(userId, contactId)
toContact' :: Int64 -> Connection -> [ChatTagId] -> ContactRow' -> Contact
toContact' contactId conn chatTags ((profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. (connFullLink, connShortLink, contactGroupMemberId, BI contactGrpInvSent, uiThemes, BI chatDeleted, customData, chatItemTTL)) =
toContact' contactId conn chatTags ((profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. (connFullLink, connShortLink, contactRequestId, contactGroupMemberId, BI contactGrpInvSent, uiThemes, BI chatDeleted, customData, chatItemTTL)) =
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite}
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
activeConn = Just conn
connLinkToConnect = toACreatedConnLink_ connFullLink connShortLink
in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, connLinkToConnect, contactGroupMemberId, contactGrpInvSent, chatTags, chatItemTTL, uiThemes, chatDeleted, customData}
in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, connLinkToConnect, contactRequestId, contactGroupMemberId, contactGrpInvSent, chatTags, chatItemTTL, uiThemes, chatDeleted, customData}
getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember)
getGroupAndMember_ groupMemberId c = do
gm <-
+134 -60
View File
@@ -57,11 +57,15 @@ module Simplex.Chat.Store.Direct
setQuotaErrCounter,
getUserContacts,
createOrUpdateContactRequest,
getAcceptedContactByXContactId,
getAcceptedBusinessChatByXContactId,
getUserContactLinkIdByCReq,
getContactRequest',
getContactRequest,
getContactRequestIdByName,
deleteContactRequest,
createContactFromRequest,
createAcceptedContactConn,
createAcceptedContact,
deleteContactRequestRec,
updateContactAccepted,
@@ -107,7 +111,6 @@ import Simplex.Messaging.Agent.Store.DB (Binary (..), BoolInt (..))
import qualified Simplex.Messaging.Agent.Store.DB as DB
import Simplex.Messaging.Crypto.Ratchet (PQSupport)
import Simplex.Messaging.Protocol (SubscriptionMode (..))
import Simplex.Messaging.Util ((<$$>))
import Simplex.Messaging.Version
#if defined(dbPostgres)
import Database.PostgreSQL.Simple (Only (..), (:.) (..))
@@ -206,7 +209,7 @@ getContactByConnReqHash db vr user@User {userId} cReqHash = do
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.contact_request_id,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
@@ -269,6 +272,7 @@ createPreparedContact db user@User {userId} p@Profile {preferences} connLinkToCo
updatedAt = currentTs,
chatTs = Just currentTs,
connLinkToConnect = Just connLinkToConnect,
contactRequestId = Nothing,
contactGroupMemberId = Nothing,
contactGrpInvSent = False,
chatTags = [],
@@ -302,6 +306,7 @@ createDirectContact db user@User {userId} conn@Connection {connId, localAlias} p
updatedAt = currentTs,
chatTs = Just currentTs,
connLinkToConnect = Nothing,
contactRequestId = Nothing,
contactGroupMemberId = Nothing,
contactGrpInvSent = False,
chatTags = [],
@@ -648,24 +653,36 @@ getUserContacts db vr user@User {userId} = do
pure $ filter (\Contact {activeConn} -> isJust activeConn) contacts
createOrUpdateContactRequest :: DB.Connection -> VersionRangeChat -> User -> Int64 -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> PQSupport -> ExceptT StoreError IO ChatOrRequest
createOrUpdateContactRequest db vr user@User {userId, userContactId} userContactLinkId invId (VersionRange minV maxV) Profile {displayName, fullName, image, contactLink, preferences} xContactId_ pqSup =
liftIO (maybeM getContactOrGroup xContactId_) >>= \case
Just cr -> pure cr
Nothing -> CORRequest <$> createOrUpdate_
createOrUpdateContactRequest
db
vr
user@User {userId}
uclId
invId
(VersionRange minV maxV)
Profile {displayName, fullName, image, contactLink, preferences}
xContactId_
pqSup =
-- this doesn't return a newly created contact request with contact,
-- because we only set xContactId after contact is accepted
-- (this allows older clients to update contact request by reusing xContactId)
liftIO (maybeM (getAcceptedContactByXContactId db vr user) xContactId_) >>= \case
Just ct -> pure $ CORContact ct
Nothing -> do
(ucr, ct_) <- createOrUpdateRequest
pure $ CORRequest ucr ct_
where
maybeM = maybe (pure Nothing)
getContactOrGroup xContactId =
getContact' xContactId >>= \case
Just ct -> pure $ Just $ CORContact ct
Nothing -> CORGroup <$$> getGroupInfo' xContactId
createOrUpdate_ :: ExceptT StoreError IO UserContactRequest
createOrUpdate_ = do
createOrUpdateRequest :: ExceptT StoreError IO (UserContactRequest, Maybe Contact)
createOrUpdateRequest = do
cReqId <-
ExceptT $
maybeM getContactRequestByXContactId xContactId_ >>= \case
Nothing -> createContactRequest
Just cr@UserContactRequest {contactRequestId} -> updateContactRequest cr $> Right contactRequestId
getContactRequest db user cReqId
ucr@UserContactRequest {contactId_} <- getContactRequest db user cReqId
ct_ <- forM contactId_ $ \contactId -> getContact db vr user contactId
pure (ucr, ct_)
createContactRequest :: IO (Either StoreError Int64)
createContactRequest = do
currentTs <- getCurrentTime
@@ -685,44 +702,20 @@ createOrUpdateContactRequest db vr user@User {userId, userContactId} userContact
created_at, updated_at, xcontact_id, pq_support)
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|]
( (userContactLinkId, Binary invId, minV, maxV, profileId, ldn, userId)
( (uclId, Binary invId, minV, maxV, profileId, ldn, userId)
:. (currentTs, currentTs, xContactId_, pqSup)
)
insertedRowId db
getContact' :: XContactId -> IO (Maybe Contact)
getContact' xContactId = do
ct_ <-
maybeFirstRow (toContact vr user []) $
DB.query
contactRequestId <- insertedRowId db
DB.execute
db
[sql|
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
LEFT JOIN connections c ON c.contact_id = ct.contact_id
WHERE ct.user_id = ? AND ct.xcontact_id = ? AND ct.deleted = 0
ORDER BY c.created_at DESC
LIMIT 1
|]
(userId, xContactId)
mapM (addDirectChatTags db) ct_
getGroupInfo' :: XContactId -> IO (Maybe GroupInfo)
getGroupInfo' xContactId = do
g_ <-
maybeFirstRow (toGroupInfo vr userContactId []) $
DB.query
"INSERT INTO contacts (contact_profile_id, local_display_name, user_id, created_at, updated_at, chat_ts, contact_used, contact_request_id) VALUES (?,?,?,?,?,?,?,?)"
(profileId, ldn, userId, currentTs, currentTs, currentTs, BI True, contactRequestId)
contactId <- insertedRowId db
DB.execute
db
(groupInfoQuery <> " WHERE g.business_xcontact_id = ? AND g.user_id = ? AND mu.contact_id = ?")
(xContactId, userId, userContactId)
mapM (addGroupChatTags db) g_
"UPDATE contact_requests SET contact_id = ? WHERE contact_request_id = ?"
(contactId, contactRequestId)
pure contactRequestId
getContactRequestByXContactId :: XContactId -> IO (Maybe UserContactRequest)
getContactRequestByXContactId xContactId =
maybeFirstRow toContactRequest $
@@ -742,7 +735,7 @@ createOrUpdateContactRequest db vr user@User {userId, userContactId} userContact
|]
(userId, xContactId)
updateContactRequest :: UserContactRequest -> IO (Either StoreError ())
updateContactRequest UserContactRequest {contactRequestId = cReqId, localDisplayName = oldLdn, profile = Profile {displayName = oldDisplayName}} = do
updateContactRequest UserContactRequest {contactRequestId = cReqId, contactId_, localDisplayName = oldLdn, profile = Profile {displayName = oldDisplayName}} = do
currentTs <- liftIO getCurrentTime
updateProfile currentTs
if displayName == oldDisplayName
@@ -766,6 +759,15 @@ createOrUpdateContactRequest db vr user@User {userId, userContactId} userContact
WHERE user_id = ? AND contact_request_id = ?
|]
(Binary invId, pqSup, minV, maxV, ldn, currentTs, userId, cReqId)
forM_ contactId_ $ \contactId ->
DB.execute
db
[sql|
UPDATE contacts
SET local_display_name = ?, updated_at = ?
WHERE contact_id = ?
|]
(ldn, currentTs, contactId)
safeDeleteLDN db user oldLdn
where
updateProfile currentTs =
@@ -787,6 +789,42 @@ createOrUpdateContactRequest db vr user@User {userId, userContactId} userContact
|]
(displayName, fullName, image, contactLink, currentTs, userId, cReqId)
getAcceptedContactByXContactId :: DB.Connection -> VersionRangeChat -> User -> XContactId -> IO (Maybe Contact)
getAcceptedContactByXContactId db vr user@User {userId} xContactId = do
ct_ <-
maybeFirstRow (toContact vr user []) $
DB.query
db
[sql|
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.contact_request_id,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
LEFT JOIN connections c ON c.contact_id = ct.contact_id
WHERE ct.user_id = ? AND ct.xcontact_id = ? AND ct.deleted = 0
ORDER BY c.created_at DESC
LIMIT 1
|]
(userId, xContactId)
mapM (addDirectChatTags db) ct_
getAcceptedBusinessChatByXContactId :: DB.Connection -> VersionRangeChat -> User -> XContactId -> IO (Maybe GroupInfo)
getAcceptedBusinessChatByXContactId db vr User {userId, userContactId} xContactId = do
g_ <-
maybeFirstRow (toGroupInfo vr userContactId []) $
DB.query
db
(groupInfoQuery <> " WHERE g.business_xcontact_id = ? AND g.user_id = ? AND mu.contact_id = ?")
(xContactId, userId, userContactId)
mapM (addGroupChatTags db) g_
getUserContactLinkIdByCReq :: DB.Connection -> Int64 -> ExceptT StoreError IO Int64
getUserContactLinkIdByCReq db contactRequestId =
ExceptT . firstRow fromOnly (SEContactRequestNotFound contactRequestId) $
@@ -846,20 +884,17 @@ deleteContactRequest db User {userId} contactRequestId = do
(userId, userId, contactRequestId, userId)
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId)
createAcceptedContact :: DB.Connection -> User -> ConnId -> VersionChat -> VersionRangeChat -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> PQSupport -> Bool -> IO (Contact, Connection)
createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId connChatVersion cReqChatVRange localDisplayName profileId profile userContactLinkId xContactId incognitoProfile subMode pqSup contactUsed = do
createdAt <- getCurrentTime
customUserProfileId <- forM incognitoProfile $ \case
NewIncognito p -> createIncognitoProfile_ db userId createdAt p
ExistingIncognito LocalProfile {profileId = pId} -> pure pId
createContactFromRequest :: DB.Connection -> User -> Int64 -> ConnId -> VersionChat -> VersionRangeChat -> ContactName -> ProfileId -> Profile -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> PQSupport -> Bool -> IO (Contact, Connection)
createContactFromRequest db user@User {userId, profile = LocalProfile {preferences}} uclId agentConnId connChatVersion cReqChatVRange localDisplayName profileId profile xContactId incognitoProfile subMode pqSup contactUsed = do
currentTs <- getCurrentTime
let userPreferences = fromMaybe emptyChatPrefs $ incognitoProfile >> preferences
DB.execute
db
"INSERT INTO contacts (user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, created_at, updated_at, chat_ts, xcontact_id, contact_used) VALUES (?,?,?,?,?,?,?,?,?,?)"
(userId, localDisplayName, profileId, BI True, userPreferences, createdAt, createdAt, createdAt, xContactId, BI contactUsed)
(userId, localDisplayName, profileId, BI True, userPreferences, currentTs, currentTs, currentTs, xContactId, BI contactUsed)
contactId <- insertedRowId db
DB.execute db "UPDATE contact_requests SET contact_id = ? WHERE user_id = ? AND local_display_name = ?" (contactId, userId, localDisplayName)
conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId ConnNew connChatVersion cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode pqSup
conn <- createAcceptedContactConn db user uclId contactId agentConnId connChatVersion cReqChatVRange pqSup incognitoProfile subMode currentTs
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn
ct =
Contact
@@ -873,10 +908,11 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}
chatSettings = defaultChatSettings,
userPreferences,
mergedPreferences,
createdAt,
updatedAt = createdAt,
chatTs = Just createdAt,
createdAt = currentTs,
updatedAt = currentTs,
chatTs = Just currentTs,
connLinkToConnect = Nothing,
contactRequestId = Nothing,
contactGroupMemberId = Nothing,
contactGrpInvSent = False,
chatTags = [],
@@ -887,6 +923,44 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}
}
pure (ct, conn)
createAcceptedContactConn :: DB.Connection -> User -> Int64 -> ContactId -> ConnId -> VersionChat -> VersionRangeChat -> PQSupport -> Maybe IncognitoProfile -> SubscriptionMode -> UTCTime -> IO Connection
createAcceptedContactConn db User {userId} uclId contactId agentConnId connChatVersion cReqChatVRange pqSup incognitoProfile subMode currentTs = do
customUserProfileId <- forM incognitoProfile $ \case
NewIncognito p -> createIncognitoProfile_ db userId currentTs p
ExistingIncognito LocalProfile {profileId = pId} -> pure pId
createConnection_ db userId ConnContact (Just contactId) agentConnId ConnNew connChatVersion cReqChatVRange Nothing (Just uclId) customUserProfileId 0 currentTs subMode pqSup
createAcceptedContact :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ConnId -> VersionChat -> VersionRangeChat -> Profile -> Maybe XContactId -> PQSupport -> Maybe IncognitoProfile -> SubscriptionMode -> ExceptT StoreError IO (Contact, Connection)
createAcceptedContact
db
vr
user@User {userId}
uclId
agentConnId
connChatVersion
cReqChatVRange
Profile {displayName, fullName, image, contactLink, preferences}
xContactId
pqSup
incognitoProfile
subMode = do
currentTs <- liftIO getCurrentTime
let userPreferences = fromMaybe emptyChatPrefs $ incognitoProfile >> preferences
contactId <- ExceptT . withLocalDisplayName db userId displayName $ \ldn -> do
DB.execute
db
"INSERT INTO contact_profiles (display_name, full_name, image, contact_link, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
(displayName, fullName, image, contactLink, userId, preferences, currentTs, currentTs)
profileId <- insertedRowId db
DB.execute
db
"INSERT INTO contacts (user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, created_at, updated_at, chat_ts, xcontact_id, contact_used) VALUES (?,?,?,?,?,?,?,?,?,?)"
(userId, ldn, profileId, BI True, userPreferences, currentTs, currentTs, currentTs, xContactId, BI True)
Right <$> insertedRowId db
conn <- liftIO $ createAcceptedContactConn db user uclId contactId agentConnId connChatVersion cReqChatVRange pqSup incognitoProfile subMode currentTs
ct <- getContact db vr user contactId
pure (ct, conn)
deleteContactRequestRec :: DB.Connection -> User -> UserContactRequest -> IO ()
deleteContactRequestRec db User {userId} UserContactRequest {contactRequestId} =
DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId)
@@ -916,7 +990,7 @@ getContact_ db vr user@User {userId} contactId deleted = do
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.contact_request_id,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
+28 -17
View File
@@ -1183,23 +1183,31 @@ createNewContactMemberAsync db gVar user@User {userId, userContactId} GroupInfo
:. (minV, maxV)
)
createJoiningMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> UserContactRequest -> GroupMemberRole -> GroupMemberStatus -> ExceptT StoreError IO (GroupMemberId, MemberId)
createJoiningMember :: DB.Connection -> TVar ChaChaDRG -> User -> GroupInfo -> VersionRangeChat -> Profile -> GroupMemberRole -> GroupMemberStatus -> ExceptT StoreError IO (GroupMemberId, MemberId)
createJoiningMember
db
gVar
User {userId, userContactId}
GroupInfo {groupId, membership}
UserContactRequest {cReqChatVRange, localDisplayName, profileId}
cReqChatVRange
Profile {displayName, fullName, image, contactLink, preferences}
memberRole
memberStatus =
createWithRandomId gVar $ \memId -> do
createdAt <- liftIO getCurrentTime
insertMember_ (MemberId memId) createdAt
groupMemberId <- liftIO $ insertedRowId db
pure (groupMemberId, MemberId memId)
memberStatus = do
currentTs <- liftIO getCurrentTime
ExceptT . withLocalDisplayName db userId displayName $ \ldn -> runExceptT $ do
liftIO $
DB.execute
db
"INSERT INTO contact_profiles (display_name, full_name, image, contact_link, user_id, preferences, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)"
(displayName, fullName, image, contactLink, userId, preferences, currentTs, currentTs)
profileId <- liftIO $ insertedRowId db
createWithRandomId gVar $ \memId -> do
insertMember_ ldn profileId (MemberId memId) currentTs
groupMemberId <- liftIO $ insertedRowId db
pure (groupMemberId, MemberId memId)
where
VersionRange minV maxV = cReqChatVRange
insertMember_ memberId createdAt =
insertMember_ ldn profileId memberId currentTs =
DB.execute
db
[sql|
@@ -1210,30 +1218,33 @@ createJoiningMember
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|]
( (groupId, memberId, memberRole, GCInviteeMember, memberStatus, fromInvitedBy userContactId IBUser, groupMemberId' membership)
:. (userId, localDisplayName, Nothing :: (Maybe Int64), profileId, createdAt, createdAt)
:. (userId, ldn, Nothing :: (Maybe Int64), profileId, currentTs, currentTs)
:. (minV, maxV)
)
createJoiningMemberConnection :: DB.Connection -> User -> (CommandId, ConnId) -> VersionChat -> UserContactRequest -> GroupMemberId -> SubscriptionMode -> IO ()
createJoiningMemberConnection :: DB.Connection -> User -> Int64 -> (CommandId, ConnId) -> VersionChat -> VersionRangeChat -> GroupMemberId -> SubscriptionMode -> IO ()
createJoiningMemberConnection
db
user@User {userId}
uclId
(cmdId, agentConnId)
chatV
UserContactRequest {cReqChatVRange, userContactLinkId}
cReqChatVRange
groupMemberId
subMode = do
createdAt <- liftIO getCurrentTime
Connection {connId} <- createConnection_ db userId ConnMember (Just groupMemberId) agentConnId ConnNew chatV cReqChatVRange Nothing (Just userContactLinkId) Nothing 0 createdAt subMode PQSupportOff
Connection {connId} <- createConnection_ db userId ConnMember (Just groupMemberId) agentConnId ConnNew chatV cReqChatVRange Nothing (Just uclId) Nothing 0 createdAt subMode PQSupportOff
setCommandConnId db user cmdId connId
createBusinessRequestGroup :: DB.Connection -> VersionRangeChat -> TVar ChaChaDRG -> User -> UserContactRequest -> GroupPreferences -> ExceptT StoreError IO (GroupInfo, GroupMember)
createBusinessRequestGroup :: DB.Connection -> VersionRangeChat -> TVar ChaChaDRG -> User -> VersionRangeChat -> Profile -> Maybe XContactId -> GroupPreferences -> ExceptT StoreError IO (GroupInfo, GroupMember)
createBusinessRequestGroup
db
vr
gVar
user@User {userId, userContactId}
UserContactRequest {cReqChatVRange, xContactId, profile = Profile {displayName, fullName, image, contactLink, preferences}}
cReqChatVRange
Profile {displayName, fullName, image, contactLink, preferences}
xContactId
groupPreferences = do
currentTs <- liftIO getCurrentTime
(groupId, membership@GroupMember {memberId = userMemberId}) <- insertGroup_ currentTs
@@ -2435,7 +2446,7 @@ createMemberContact
quotaErrCounter = 0
}
mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, connLinkToConnect = Nothing, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing}
pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, connLinkToConnect = Nothing, contactRequestId = Nothing, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing}
getMemberContact :: DB.Connection -> VersionRangeChat -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation)
getMemberContact db vr user contactId = do
@@ -2472,7 +2483,7 @@ createMemberContactInvited
contactId <- createContactUpdateMember currentTs userPreferences
ctConn <- createMemberContactConn_ db user connIds gInfo mConn contactId subMode
let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn
mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, connLinkToConnect = Nothing, contactGroupMemberId = Nothing, contactGrpInvSent = False, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing}
mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, connLinkToConnect = Nothing, contactRequestId = Nothing, contactGroupMemberId = Nothing, contactGrpInvSent = False, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing}
m' = m {memberContactId = Just contactId}
pure (mCt', m')
where
+1
View File
@@ -1061,6 +1061,7 @@ getContactRequestChatPreviews_ db User {userId} pagination clq = case clq of
AND uc.user_id = ?
AND uc.local_display_name = ''
AND uc.group_id IS NULL
AND cr.contact_id IS NULL
AND (
LOWER(cr.local_display_name) LIKE '%' || LOWER(?) || '%'
OR LOWER(p.display_name) LIKE '%' || LOWER(?) || '%'
@@ -5,15 +5,15 @@ module Simplex.Chat.Store.SQLite.Migrations.M20250526_short_links where
import Database.SQLite.Simple (Query)
import Database.SQLite.Simple.QQ (sql)
-- TODO [short links] contacts with contact requests
-- TODO - contacts.is_contact_request flag?
-- TODO - link contact_requests and contacts?
m20250526_short_links :: Query
m20250526_short_links =
[sql|
ALTER TABLE contacts ADD COLUMN conn_full_link_to_connect BLOB;
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 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;
@@ -25,6 +25,9 @@ down_m20250526_short_links =
ALTER TABLE contacts DROP COLUMN conn_full_link_to_connect;
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 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;
@@ -1,3 +1,11 @@
Query:
UPDATE contacts
SET local_display_name = ?, updated_at = ?
WHERE contact_id = ?
Plan:
SEARCH contacts USING INTEGER PRIMARY KEY (rowid=?)
Query:
UPDATE groups
SET chat_ts = ?,
@@ -169,29 +177,6 @@ Query:
Plan:
Query:
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
LEFT JOIN connections c ON c.contact_id = ct.contact_id
WHERE ct.user_id = ? AND ct.xcontact_id = ? AND ct.deleted = 0
ORDER BY c.created_at DESC
LIMIT 1
Plan:
SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=?)
SEARCH cp USING INTEGER PRIMARY KEY (rowid=?)
SEARCH c USING INDEX idx_connections_contact_id (contact_id=?) LEFT-JOIN
USE TEMP B-TREE FOR ORDER BY
Query:
SELECT COUNT(1)
FROM chat_items i
@@ -342,7 +327,7 @@ Plan:
Query:
SELECT
c.contact_profile_id, c.local_display_name, c.via_group, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite,
p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect,
p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.conn_full_link_to_connect, c.conn_short_link_to_connect, c.contact_request_id,
c.contact_group_member_id, c.contact_grp_inv_sent, c.ui_themes, c.chat_deleted, c.custom_data, c.chat_item_ttl
FROM contacts c
JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id
@@ -859,7 +844,7 @@ Query:
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.contact_request_id,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
@@ -878,6 +863,29 @@ SEARCH ct USING INTEGER PRIMARY KEY (rowid=?)
SEARCH cp USING INTEGER PRIMARY KEY (rowid=?)
USE TEMP B-TREE FOR ORDER BY
Query:
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.contact_request_id,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter,
c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version
FROM contacts ct
JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id
LEFT JOIN connections c ON c.contact_id = ct.contact_id
WHERE ct.user_id = ? AND ct.xcontact_id = ? AND ct.deleted = 0
ORDER BY c.created_at DESC
LIMIT 1
Plan:
SEARCH ct USING INDEX idx_contacts_chat_ts (user_id=?)
SEARCH cp USING INTEGER PRIMARY KEY (rowid=?)
SEARCH c USING INDEX idx_connections_contact_id (contact_id=?) LEFT-JOIN
USE TEMP B-TREE FOR ORDER BY
Query:
SELECT
-- GroupInfo
@@ -1431,7 +1439,7 @@ Query:
SELECT
-- Contact
ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect,
cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.conn_full_link_to_connect, ct.conn_short_link_to_connect, ct.contact_request_id,
ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, ct.chat_item_ttl,
-- Connection
c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias,
@@ -1543,6 +1551,7 @@ Query:
AND uc.user_id = ?
AND uc.local_display_name = ''
AND uc.group_id IS NULL
AND cr.contact_id IS NULL
AND (
LOWER(cr.local_display_name) LIKE '%' || LOWER(?) || '%'
OR LOWER(p.display_name) LIKE '%' || LOWER(?) || '%'
@@ -1569,6 +1578,7 @@ Query:
AND uc.user_id = ?
AND uc.local_display_name = ''
AND uc.group_id IS NULL
AND cr.contact_id IS NULL
AND (
LOWER(cr.local_display_name) LIKE '%' || LOWER(?) || '%'
OR LOWER(p.display_name) LIKE '%' || LOWER(?) || '%'
@@ -1595,6 +1605,7 @@ Query:
AND uc.user_id = ?
AND uc.local_display_name = ''
AND uc.group_id IS NULL
AND cr.contact_id IS NULL
AND (
LOWER(cr.local_display_name) LIKE '%' || LOWER(?) || '%'
OR LOWER(p.display_name) LIKE '%' || LOWER(?) || '%'
@@ -5306,6 +5317,7 @@ SEARCH contacts USING COVERING INDEX idx_contacts_contact_profile_id (contact_pr
Query: DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?
Plan:
SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?)
SEARCH contacts USING COVERING INDEX idx_contacts_contact_request_id (contact_request_id=?)
Query: DELETE FROM contacts WHERE user_id = ? AND contact_id = ?
Plan:
@@ -5557,6 +5569,10 @@ Query: INSERT INTO contact_profiles (display_name, full_name, image, user_id, pr
Plan:
SEARCH contact_requests USING COVERING INDEX idx_contact_requests_contact_profile_id (contact_profile_id=?)
Query: INSERT INTO contacts (contact_profile_id, local_display_name, user_id, created_at, updated_at, chat_ts, contact_used, contact_request_id) VALUES (?,?,?,?,?,?,?,?)
Plan:
SEARCH users USING COVERING INDEX sqlite_autoindex_users_1 (contact_id=?)
Query: INSERT INTO contacts (contact_profile_id, local_display_name, user_id, is_user, created_at, updated_at, chat_ts) VALUES (?,?,?,?,?,?,?)
Plan:
SEARCH users USING COVERING INDEX sqlite_autoindex_users_1 (contact_id=?)
@@ -5940,9 +5956,9 @@ Query: UPDATE connections SET to_subscribe = 0 WHERE to_subscribe = 1
Plan:
SEARCH connections USING INDEX idx_connections_to_subscribe (to_subscribe=?)
Query: UPDATE contact_requests SET contact_id = ? WHERE user_id = ? AND local_display_name = ?
Query: UPDATE contact_requests SET contact_id = ? WHERE contact_request_id = ?
Plan:
SEARCH contact_requests USING INDEX sqlite_autoindex_contact_requests_1 (user_id=? AND local_display_name=?)
SEARCH contact_requests USING INTEGER PRIMARY KEY (rowid=?)
Query: UPDATE contacts SET chat_deleted = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?
Plan:
@@ -81,6 +81,7 @@ CREATE TABLE contacts(
chat_item_ttl INTEGER,
conn_full_link_to_connect BLOB,
conn_short_link_to_connect BLOB,
contact_request_id INTEGER REFERENCES contact_requests ON DELETE SET NULL,
FOREIGN KEY(user_id, local_display_name)
REFERENCES display_names(user_id, local_display_name)
ON DELETE CASCADE
@@ -1053,3 +1054,4 @@ CREATE INDEX idx_chat_items_group_scope_item_status ON chat_items(
item_status,
item_ts
);
CREATE INDEX idx_contacts_contact_request_id ON contacts(contact_request_id);
+3 -8
View File
@@ -46,7 +46,6 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport (..))
import qualified Simplex.Messaging.Crypto.Ratchet as CR
import Simplex.Messaging.Parsers (dropPrefix, sumTypeJSON)
import Simplex.Messaging.Protocol (SubscriptionMode (..))
import Simplex.Messaging.Util (allFinally)
import Simplex.Messaging.Version
import UnliftIO.STM
#if defined(dbPostgres)
@@ -178,10 +177,6 @@ handleSQLError err e
| constraintError e = err
| otherwise = SEInternalError $ show e
storeFinally :: ExceptT StoreError IO a -> ExceptT StoreError IO b -> ExceptT StoreError IO a
storeFinally = allFinally mkStoreError
{-# INLINE storeFinally #-}
mkStoreError :: E.SomeException -> StoreError
mkStoreError = SEInternalError . show
{-# INLINE mkStoreError #-}
@@ -426,19 +421,19 @@ deleteUnusedIncognitoProfileById_ db User {userId} profileId =
|]
(userId, profileId, userId, profileId, userId, profileId)
type ContactRow' = (ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. (Maybe AConnectionRequestUri, Maybe AConnShortLink, Maybe GroupMemberId, BoolInt, Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64)
type ContactRow' = (ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnLinkContact, LocalAlias, BoolInt, ContactStatus) :. (Maybe MsgFilter, Maybe BoolInt, BoolInt, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. (Maybe AConnectionRequestUri, Maybe AConnShortLink, Maybe Int64, Maybe GroupMemberId, BoolInt, Maybe UIThemeEntityOverrides, BoolInt, Maybe CustomData, Maybe Int64)
type ContactRow = Only ContactId :. ContactRow'
toContact :: VersionRangeChat -> User -> [ChatTagId] -> ContactRow :. MaybeConnectionRow -> Contact
toContact vr user chatTags ((Only contactId :. (profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. (connFullLink, connShortLink, contactGroupMemberId, BI contactGrpInvSent, uiThemes, BI chatDeleted, customData, chatItemTTL)) :. connRow) =
toContact vr user chatTags ((Only contactId :. (profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. (connFullLink, connShortLink, contactRequestId, contactGroupMemberId, BI contactGrpInvSent, uiThemes, BI chatDeleted, customData, chatItemTTL)) :. connRow) =
let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias}
activeConn = toMaybeConnection vr connRow
chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite}
incognito = maybe False connIncognito activeConn
mergedPreferences = contactUserPreferences user userPreferences preferences incognito
connLinkToConnect = toACreatedConnLink_ connFullLink connShortLink
in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, connLinkToConnect, contactGroupMemberId, contactGrpInvSent, chatTags, chatItemTTL, uiThemes, chatDeleted, customData}
in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, connLinkToConnect, contactRequestId, contactGroupMemberId, contactGrpInvSent, chatTags, chatItemTTL, uiThemes, chatDeleted, customData}
toACreatedConnLink_ :: Maybe AConnectionRequestUri -> Maybe AConnShortLink -> Maybe ACreatedConnLink
toACreatedConnLink_ connFullLink connShortLink = case (connFullLink, connShortLink) of
+1 -1
View File
@@ -211,7 +211,7 @@ chatEventNotification t@ChatTerminal {sendNotification} cc = \case
when (groupNtf u g False) $ sendNtf ("#" <> viewGroupName g, "member " <> viewMemberName m <> " is pending review")
CEvtConnectedToGroupMember u g m _ ->
when (groupNtf u g False) $ sendNtf ("#" <> viewGroupName g, "member " <> viewMemberName m <> " is connected")
CEvtReceivedContactRequest u UserContactRequest {localDisplayName = n} ->
CEvtReceivedContactRequest u UserContactRequest {localDisplayName = n} _ ->
when (userNtf u) $ sendNtf (viewName n <> ">", "wants to connect to you")
_ -> pure ()
where
+5 -1
View File
@@ -189,6 +189,7 @@ data Contact = Contact
updatedAt :: UTCTime,
chatTs :: Maybe UTCTime,
connLinkToConnect :: Maybe ACreatedConnLink,
contactRequestId :: Maybe Int64,
contactGroupMemberId :: Maybe GroupMemberId,
contactGrpInvSent :: Bool,
chatTags :: [ChatTagId],
@@ -366,7 +367,10 @@ instance ToJSON ConnReqUriHash where
toJSON = strToJSON
toEncoding = strToJEncoding
data ChatOrRequest = CORContact Contact | CORGroup GroupInfo | CORRequest UserContactRequest
data ChatOrRequest
= CORContact Contact
-- Contact is Maybe for backward compatibility with legacy requests, all new requests are created with contact
| CORRequest UserContactRequest (Maybe Contact)
type UserName = Text
+3 -3
View File
@@ -172,7 +172,7 @@ chatResponseToView hu cfg@ChatConfig {logLevel, showReactions, testView} liveIte
CRContactsList u cs -> ttyUser u $ viewContactsList cs
CRUserContactLink u UserContactLink {connLinkContact, autoAccept} -> ttyUser u $ connReqContact_ "Your chat address:" connLinkContact <> 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} _ct_ -> ttyUser u [ttyContact c <> ": contact request rejected"]
CRGroupCreated u g -> ttyUser u $ viewGroupCreated g testView
CRGroupMembers u g -> ttyUser u $ viewGroupMembers g
CRMemberSupportChats u g ms -> ttyUser u $ viewMemberSupportChats g ms
@@ -416,7 +416,7 @@ chatEventToView hu ChatConfig {logLevel, showReactions, showReceipts, testView}
CEvtContactUpdated {user = u, fromContact = c, toContact = c'} -> ttyUser u $ viewContactUpdated c c' <> viewContactPrefsUpdated u c c'
CEvtGroupMemberUpdated {} -> []
CEvtContactsMerged u intoCt mergedCt ct' -> ttyUser u $ viewContactsMerged intoCt mergedCt ct'
CEvtReceivedContactRequest u UserContactRequest {localDisplayName = c, profile} -> ttyUser u $ viewReceivedContactRequest c profile
CEvtReceivedContactRequest u UserContactRequest {localDisplayName = c, profile} _ct_ -> ttyUser u $ viewReceivedContactRequest c profile
CEvtRcvFileStart u ci -> ttyUser u $ receivingFile_' hu testView "started" ci
CEvtRcvFileComplete u ci -> ttyUser u $ receivingFile_' hu testView "completed" ci
CEvtRcvStandaloneFileComplete u _ ft -> ttyUser u $ receivingFileStandalone "completed" ft
@@ -1826,7 +1826,7 @@ viewConnectionUserChanged User {localDisplayName = n} PendingContactConnection {
cReqStr = strEncode $ simplexChatInvitation cReq
viewConnectionPlan :: ChatConfig -> ACreatedConnLink -> ConnectionPlan -> [StyledString]
viewConnectionPlan ChatConfig {logLevel, testView} connLink = \case
viewConnectionPlan ChatConfig {logLevel, testView} _connLink = \case
CPInvitationLink ilp -> case ilp of
ILPOk contactSLinkData -> [invLink "ok to connect"] <> [viewJSON contactSLinkData | testView]
ILPOwnLink -> [invLink "own link"]
+7 -9
View File
@@ -200,14 +200,14 @@ testPaginationAllChatTypes =
ts7 <- iso8601Show <$> getCurrentTime
getChats_ alice "count=10" [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on"), (":3", ""), ("<@cath", ""), ("@bob", "hey")]
getChats_ alice "count=10" [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on"), (":3", ""), ("@cath", ""), ("@bob", "hey")]
getChats_ alice "count=3" [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on")]
getChats_ alice ("after=" <> ts2 <> " count=2") [(":3", ""), ("<@cath", "")]
getChats_ alice ("after=" <> ts2 <> " count=2") [(":3", ""), ("@cath", "")]
getChats_ alice ("before=" <> ts5 <> " count=2") [("#team", "Recent history: on"), (":3", "")]
getChats_ alice ("after=" <> ts3 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on"), (":3", "")]
getChats_ alice ("before=" <> ts4 <> " count=10") [(":3", ""), ("<@cath", ""), ("@bob", "hey")]
getChats_ alice ("after=" <> ts1 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on"), (":3", ""), ("<@cath", ""), ("@bob", "hey")]
getChats_ alice ("before=" <> ts7 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on"), (":3", ""), ("<@cath", ""), ("@bob", "hey")]
getChats_ alice ("before=" <> ts4 <> " count=10") [(":3", ""), ("@cath", ""), ("@bob", "hey")]
getChats_ alice ("after=" <> ts1 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on"), (":3", ""), ("@cath", ""), ("@bob", "hey")]
getChats_ alice ("before=" <> ts7 <> " count=10") [("*", "psst"), ("@dan", "hey"), ("#team", "Recent history: on"), (":3", ""), ("@cath", ""), ("@bob", "hey")]
getChats_ alice ("after=" <> ts7 <> " count=10") []
getChats_ alice ("before=" <> ts1 <> " count=10") []
@@ -227,8 +227,6 @@ testPaginationAllChatTypes =
let queryUnread = "{\"type\": \"filters\", \"favorite\": false, \"unread\": true}"
getChats_ alice queryUnread [("<@cath", "")]
getChats_ alice queryUnread []
getChats_ alice ("before=" <> ts2 <> " count=10 " <> queryUnread) []
getChats_ alice ("before=" <> ts3 <> " count=10 " <> queryUnread) [("<@cath", "")]
getChats_ alice ("after=" <> ts2 <> " count=10 " <> queryUnread) [("<@cath", "")]
getChats_ alice ("after=" <> ts3 <> " count=10 " <> queryUnread) []
getChats_ alice ("after=" <> ts2 <> " count=10 " <> queryUnread) []
+2 -2
View File
@@ -1762,7 +1762,7 @@ testMultipleUserAddresses =
cLinkAlice <- getContactLink alice True
bob ##> ("/c " <> cLinkAlice)
alice <#? bob
alice @@@ [("<@bob", "")]
alice @@@ [("@bob", "")]
alice ##> "/ac bob"
alice <## "bob (Bob): accepting contact request, you can send messages to contact"
concurrently_
@@ -1780,7 +1780,7 @@ testMultipleUserAddresses =
cLinkAlisa <- getContactLink alice True
bob ##> ("/c " <> cLinkAlisa)
alice <#? bob
alice #$> ("/_get chats 2 pcc=on", chats, [("<@bob", ""), ("@SimpleX Chat team", ""), ("@SimpleX-Status", ""), ("*", "")])
alice #$> ("/_get chats 2 pcc=on", chats, [("@bob", ""), ("@SimpleX Chat team", ""), ("@SimpleX-Status", ""), ("*", "")])
alice ##> "/ac bob"
alice <## "bob (Bob): accepting contact request, you can send messages to contact"
concurrently_
+21 -16
View File
@@ -257,7 +257,7 @@ testUserContactLink =
cLink <- getContactLink alice True
bob ##> ("/c " <> cLink)
alice <#? bob
alice @@@ [("<@bob", "")]
alice @@@ [("@bob", "")]
alice ##> "/ac bob"
alice <## "bob (Bob): accepting contact request, you can send messages to contact"
concurrently_
@@ -269,7 +269,7 @@ testUserContactLink =
cath ##> ("/c " <> cLink)
alice <#? cath
alice @@@ [("<@cath", ""), ("@bob", "hey")]
alice @@@ [("@cath", ""), ("@bob", "hey")]
alice ##> "/ac cath"
alice <## "cath (Catherine): accepting contact request, you can send messages to contact"
concurrently_
@@ -432,7 +432,7 @@ testUserContactLinkAutoAccept =
bob ##> ("/c " <> cLink)
alice <#? bob
alice @@@ [("<@bob", "")]
alice @@@ [("@bob", "")]
alice ##> "/ac bob"
alice <## "bob (Bob): accepting contact request, you can send messages to contact"
concurrently_
@@ -461,7 +461,7 @@ testUserContactLinkAutoAccept =
dan ##> ("/c " <> cLink)
alice <#? dan
alice @@@ [("<@dan", ""), ("@cath", "hey"), ("@bob", "hey")]
alice @@@ [("@dan", ""), ("@cath", "hey"), ("@bob", "hey")]
alice ##> "/ac dan"
alice <## "dan (Daniel): accepting contact request, you can send messages to contact"
concurrently_
@@ -479,14 +479,14 @@ testDeduplicateContactRequests = testChat3 aliceProfile bobProfile cathProfile $
bob ##> ("/c " <> cLink)
alice <#? bob
alice @@@ [("<@bob", "")]
alice @@@ [("@bob", "")]
bob @@@! [(":1", "", Just ConnJoined)]
bob ##> ("/c " <> cLink)
alice <#? bob
bob ##> ("/c " <> cLink)
alice <#? bob
alice @@@ [("<@bob", "")]
alice @@@ [("@bob", "")]
bob @@@! [(":3", "", Just ConnJoined), (":2", "", Just ConnJoined), (":1", "", Just ConnJoined)]
alice ##> "/ac bob"
@@ -520,7 +520,7 @@ testDeduplicateContactRequests = testChat3 aliceProfile bobProfile cathProfile $
cath ##> ("/c " <> cLink)
alice <#? cath
alice @@@ [("<@cath", ""), ("@bob", "hey")]
alice @@@ [("@cath", ""), ("@bob", "hey")]
alice ##> "/ac cath"
alice <## "cath (Catherine): accepting contact request, you can send messages to contact"
concurrently_
@@ -538,7 +538,7 @@ testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile
bob ##> ("/c " <> cLink)
alice <#? bob
alice @@@ [("<@bob", "")]
alice @@@ [("@bob", "")]
bob ##> "/p bob"
bob <## "user full name removed (your 0 contacts are notified)"
@@ -547,19 +547,19 @@ testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile
alice <## "bob wants to connect to you!"
alice <## "to accept: /ac bob"
alice <## "to reject: /rc bob (the sender will NOT be notified)"
alice @@@ [("<@bob", "")]
alice @@@ [("@bob", "")]
bob ##> "/p bob Bob Ross"
bob <## "user full name changed to Bob Ross (your 0 contacts are notified)"
bob ##> ("/c " <> cLink)
alice <#? bob
alice @@@ [("<@bob", "")]
alice @@@ [("@bob", "")]
bob ##> "/p robert Robert"
bob <## "user profile is changed to robert (Robert) (your 0 contacts are notified)"
bob ##> ("/c " <> cLink)
alice <#? bob
alice @@@ [("<@robert", "")]
alice @@@ [("@robert", "")]
alice ##> "/ac bob"
alice <## "no contact request from bob"
@@ -597,7 +597,7 @@ testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile
cath ##> ("/c " <> cLink)
alice <#? cath
alice @@@ [("<@cath", ""), ("@robert", "hey")]
alice @@@ [("@cath", ""), ("@robert", "hey")]
alice ##> "/ac cath"
alice <## "cath (Catherine): accepting contact request, you can send messages to contact"
concurrently_
@@ -614,8 +614,11 @@ testRejectContactAndDeleteUserContact = testChat3 aliceProfile bobProfile cathPr
cLink <- getContactLink alice True
bob ##> ("/c " <> cLink)
alice <#? bob
alice @@@ [("@bob", "")]
alice ##> "/rc bob"
alice <## "bob: contact request rejected"
alice @@@ []
(bob </)
alice ##> "/_show_address 1"
@@ -898,7 +901,7 @@ testPlanAddressOkKnown =
bob ##> ("/c " <> cLink)
alice <#? bob
alice @@@ [("<@bob", "")]
alice @@@ [("@bob", "")]
alice ##> "/ac bob"
alice <## "bob (Bob): accepting contact request, you can send messages to contact"
concurrently_
@@ -937,7 +940,7 @@ testPlanAddressOwn ps =
alice <## "alice_1 (Alice) wants to connect to you!"
alice <## "to accept: /ac alice_1"
alice <## "to reject: /rc alice_1 (the sender will NOT be notified)"
alice @@@ [("<@alice_1", ""), (":2", "")]
alice @@@ [("@alice_1", ""), (":2", "")]
alice ##> "/ac alice_1"
alice <## "alice_1 (Alice): accepting contact request, you can send messages to contact"
alice
@@ -2802,8 +2805,10 @@ testShortLinkAddressPrepareContact =
<### [ "alice: connection started",
WithTime "@alice hello"
]
-- TODO [short links] for alice create message from contact request
alice <## "bob (Bob) wants to connect to you!"
alice
<### [ "bob (Bob) wants to connect to you!",
WithTime "bob> hello"
]
alice <## "to accept: /ac bob"
alice <## "to reject: /rc bob (the sender will NOT be notified)"
alice ##> "/ac bob"