diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index c17d8e23a8..29c87ff7e0 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -577,7 +577,7 @@ struct ChatInfoView: View { private func clearChatAlert() -> Alert { Alert( title: Text("Clear conversation?"), - message: Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."), + message: Text(chat.chatInfo.displayName + "\n\n") + Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."), primaryButton: .destructive(Text("Clear")) { Task { await clearChat(chat) @@ -1185,6 +1185,7 @@ private func deleteContactOrConversationDialog( showActionSheet(SomeActionSheet( actionSheet: ActionSheet( title: Text("Delete contact?"), + message: Text(contact.displayName), buttons: [ .destructive(Text("Only delete conversation")) { deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: .messages, dismissToChatList, showAlert) @@ -1331,6 +1332,7 @@ private func deleteContactWithoutConversation( showActionSheet(SomeActionSheet( actionSheet: ActionSheet( title: Text("Confirm contact deletion?"), + message: Text(contact.displayName), buttons: [ .destructive(Text("Delete and notify contact")) { deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: .full(notify: true), dismissToChatList, showAlert) @@ -1355,6 +1357,7 @@ private func deleteNotReadyContact( showActionSheet(SomeActionSheet( actionSheet: ActionSheet( title: Text("Confirm contact deletion?"), + message: Text(contact.displayName), buttons: [ .destructive(Text("Confirm")) { deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: .full(notify: false), dismissToChatList, showAlert) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 0a448a2772..1353f590fa 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -845,7 +845,7 @@ struct GroupChatInfoView: View { let label: LocalizedStringKey = groupInfo.useRelays ? "Delete channel?" : groupInfo.businessChat == nil ? "Delete group?" : "Delete chat?" return Alert( title: Text(label), - message: deleteGroupAlertMessage(groupInfo), + message: Text(chat.chatInfo.displayName + "\n\n") + deleteGroupAlertMessage(groupInfo), primaryButton: .destructive(Text("Delete")) { Task { do { @@ -867,7 +867,7 @@ struct GroupChatInfoView: View { private func clearChatAlert() -> Alert { Alert( title: Text("Clear conversation?"), - message: Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."), + message: Text(chat.chatInfo.displayName + "\n\n") + Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."), primaryButton: .destructive(Text("Clear")) { Task { await clearChat(chat) @@ -889,7 +889,7 @@ struct GroupChatInfoView: View { ) return Alert( title: Text(titleLabel), - message: Text(messageLabel), + message: Text(chat.chatInfo.displayName + "\n\n") + Text(messageLabel), primaryButton: .destructive(Text("Leave")) { Task { await leaveGroup(chat.chatInfo.apiId) diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index b4590fc124..76734dcb42 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -568,7 +568,7 @@ struct ChatListNavLink: View { let label: LocalizedStringKey = groupInfo.useRelays ? "Delete channel?" : groupInfo.businessChat == nil ? "Delete group?" : "Delete chat?" return Alert( title: Text(label), - message: deleteGroupAlertMessage(groupInfo), + message: Text(chat.chatInfo.displayName + "\n\n") + deleteGroupAlertMessage(groupInfo), primaryButton: .destructive(Text("Delete")) { Task { await deleteChat(chat) } }, @@ -600,7 +600,7 @@ struct ChatListNavLink: View { private func clearChatAlert() -> Alert { Alert( title: Text("Clear conversation?"), - message: Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."), + message: Text(chat.chatInfo.displayName + "\n\n") + Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."), primaryButton: .destructive(Text("Clear")) { Task { await clearChat(chat) } }, @@ -630,7 +630,7 @@ struct ChatListNavLink: View { ) return Alert( title: Text(titleLabel), - message: Text(messageLabel), + message: Text(chat.chatInfo.displayName + "\n\n") + Text(messageLabel), primaryButton: .destructive(Text("Leave")) { Task { await leaveGroup(groupInfo.groupId) } }, @@ -701,10 +701,10 @@ func rejectContactRequestAlert(_ contactRequestId: Int64) -> Alert { func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, showError: @escaping (ErrorAlert) -> Void, success: @escaping () -> Void = {}) -> Alert { Alert( title: Text("Delete pending connection?"), - message: - contactConnection.initiated - ? Text("The contact you shared this link with will NOT be able to connect!") - : Text("The connection you accepted will be cancelled!"), + message: Text(contactConnection.displayName + "\n\n") + + (contactConnection.initiated + ? Text("The contact you shared this link with will NOT be able to connect!") + : Text("The connection you accepted will be cancelled!")), primaryButton: .destructive(Text("Delete")) { Task { do { diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index d5d70abaea..278893a669 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -110,33 +110,88 @@ struct DatabaseView: View { } Section { - settingsRow( - stopped ? "exclamationmark.octagon.fill" : "play.fill", - color: stopped ? .red : .green - ) { - Toggle( - stopped ? "Chat is stopped" : "Chat is running", - isOn: $runChat - ) - .onChange(of: runChat) { _ in - if runChat { - DatabaseView.startChat($runChat, $progressIndicator) - } else if !stoppingChat { - stoppingChat = false - alert = .stopChat - } - } - } - } header: { - Text("Run chat") - .foregroundColor(theme.colors.secondary) - } footer: { - if case .documents = dbContainer { - Text("Database will be migrated when the app restarts") - .foregroundColor(theme.colors.secondary) - } + NavigationLink("Database passphrase & export", destination: databaseManagementView) } + Section { + Button(m.users.count > 1 ? "Delete files for all chat profiles" : "Delete all files", role: .destructive) { + alert = .deleteFilesAndMedia + } + .disabled(progressIndicator || appFilesCountAndSize?.0 == 0) + } header: { + Text("Files & media") + .foregroundColor(theme.colors.secondary) + } footer: { + if let (fileCount, size) = appFilesCountAndSize { + if fileCount == 0 { + Text("No received or sent files") + .foregroundColor(theme.colors.secondary) + } else { + Text("\(fileCount) file(s) with total size of \(ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: .binary))") + .foregroundColor(theme.colors.secondary) + } + } + } + } + .onAppear { + runChat = m.chatRunning ?? true + appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory()) + currentChatItemTTL = chatItemTTL + } + .onChange(of: chatItemTTL) { ttl in + if ttl < currentChatItemTTL { + alert = .setChatItemTTL(ttl: ttl) + } else if ttl != currentChatItemTTL { + setCiTTL(ttl) + } + } + .alert(item: $alert) { item in databaseAlert(item) } + .fileImporter( + isPresented: $showFileImporter, + allowedContentTypes: [.zip], + allowsMultipleSelection: false + ) { result in + if case let .success(files) = result, let fileURL = files.first { + importedArchivePath = fileURL + alert = .importArchive + } + } + } + + private func runChatToggleView() -> some View { + Section { + let stopped = m.chatRunning == false + settingsRow( + stopped ? "exclamationmark.octagon.fill" : "play.fill", + color: stopped ? .red : .green + ) { + Toggle( + stopped ? "Chat is stopped" : "Chat is running", + isOn: $runChat + ) + .onChange(of: runChat) { _ in + if runChat { + DatabaseView.startChat($runChat, $progressIndicator) + } else if !stoppingChat { + stoppingChat = false + alert = .stopChat + } + } + } + } header: { + Text("Run chat") + .foregroundColor(theme.colors.secondary) + } footer: { + if case .documents = dbContainer { + Text("Database will be migrated when the app restarts") + .foregroundColor(theme.colors.secondary) + } + } + } + + private func databaseManagementView() -> some View { + List { + let stopped = m.chatRunning == false Section { let unencrypted = m.chatDbEncrypted == false let color: Color = unencrypted ? .orange : theme.colors.secondary @@ -194,49 +249,9 @@ struct DatabaseView: View { } } - Section { - Button(m.users.count > 1 ? "Delete files for all chat profiles" : "Delete all files", role: .destructive) { - alert = .deleteFilesAndMedia - } - .disabled(progressIndicator || appFilesCountAndSize?.0 == 0) - } header: { - Text("Files & media") - .foregroundColor(theme.colors.secondary) - } footer: { - if let (fileCount, size) = appFilesCountAndSize { - if fileCount == 0 { - Text("No received or sent files") - .foregroundColor(theme.colors.secondary) - } else { - Text("\(fileCount) file(s) with total size of \(ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: .binary))") - .foregroundColor(theme.colors.secondary) - } - } - } - } - .onAppear { - runChat = m.chatRunning ?? true - appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory()) - currentChatItemTTL = chatItemTTL - } - .onChange(of: chatItemTTL) { ttl in - if ttl < currentChatItemTTL { - alert = .setChatItemTTL(ttl: ttl) - } else if ttl != currentChatItemTTL { - setCiTTL(ttl) - } - } - .alert(item: $alert) { item in databaseAlert(item) } - .fileImporter( - isPresented: $showFileImporter, - allowedContentTypes: [.zip], - allowsMultipleSelection: false - ) { result in - if case let .success(files) = result, let fileURL = files.first { - importedArchivePath = fileURL - alert = .importArchive - } + runChatToggleView() } + .modifier(ThemedBackground(grouped: true)) } private func databaseAlert(_ alertItem: DatabaseAlert) -> Alert { diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift index f10b945dc0..24cf088918 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers/NetworkAndServers.swift @@ -121,16 +121,6 @@ struct NetworkAndServers: View { } } - Section(header: Text("Calls").foregroundColor(theme.colors.secondary)) { - NavigationLink { - RTCServers() - .navigationTitle("Your ICE servers") - .modifier(ThemedBackground(grouped: true)) - } label: { - Text("WebRTC ICE servers") - } - } - Section(header: Text("Network connection").foregroundColor(theme.colors.secondary)) { HStack { Text(m.networkInfo.networkType.text) diff --git a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift index c4d0588987..131eeecef7 100644 --- a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift +++ b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift @@ -63,36 +63,6 @@ struct NotificationsView: View { } } - NavigationLink { - List { - Section { - SelectionListView(list: NotificationPreviewMode.values, selection: $m.notificationPreview) { previewMode in - ntfPreviewModeGroupDefault.set(previewMode) - m.notificationPreview = previewMode - } - } footer: { - VStack(alignment: .leading, spacing: 1) { - Text("You can set lock screen notification preview via settings.") - .foregroundColor(theme.colors.secondary) - Button("Open Settings") { - DispatchQueue.main.async { - UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) - } - } - } - } - } - .navigationTitle("Show preview") - .modifier(ThemedBackground(grouped: true)) - .navigationBarTitleDisplayMode(.inline) - } label: { - HStack { - Text("Show preview") - Spacer() - Text(m.notificationPreview.label) - } - } - if let server = m.notificationServer { smpServers("Push server", [server], theme.colors.secondary) testTokenButton(server) diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index 3ae9f0eacd..ad6b2d4454 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -81,30 +81,12 @@ struct PrivacySettings: View { settingsRow("link", color: theme.colors.secondary) { Toggle("Remove link tracking", isOn: $privacySanitizeLinks) } - settingsRow("message", color: theme.colors.secondary) { - Toggle("Show last messages", isOn: $showChatPreviews) - } - settingsRow("rectangle.and.pencil.and.ellipsis", color: theme.colors.secondary) { - Toggle("Message draft", isOn: $saveLastDraft) - } - .onChange(of: saveLastDraft) { saveDraft in - if !saveDraft { - m.draft = nil - m.draftChatId = nil - } - } } header: { Text("Chats") .foregroundColor(theme.colors.secondary) } Section { - settingsRow("lock.doc", color: theme.colors.secondary) { - Toggle("Encrypt local files", isOn: $encryptLocalFiles) - .onChange(of: encryptLocalFiles) { - setEncryptLocalFiles($0) - } - } settingsRow("photo", color: theme.colors.secondary) { Toggle("Auto-accept images", isOn: $autoAcceptImages) .onChange(of: autoAcceptImages) { @@ -126,20 +108,9 @@ struct PrivacySettings: View { } } } - settingsRow("network.badge.shield.half.filled", color: theme.colors.secondary) { - Toggle("Protect IP address", isOn: $askToApproveRelays) - } } header: { Text("Files") .foregroundColor(theme.colors.secondary) - } footer: { - if askToApproveRelays { - Text("The app will ask to confirm downloads from unknown file servers (except .onion).") - .foregroundColor(theme.colors.secondary) - } else { - Text("Without Tor or VPN, your IP address will be visible to file servers.") - .foregroundColor(theme.colors.secondary) - } } Section { @@ -155,45 +126,8 @@ struct PrivacySettings: View { } Section { - settingsRow("person", color: theme.colors.secondary) { - Toggle("Contacts", isOn: $contactReceipts) - } - settingsRow("person.2", color: theme.colors.secondary) { - Toggle("Small groups (max 20)", isOn: $groupReceipts) - } - } header: { - Text("Send delivery receipts to") - .foregroundColor(theme.colors.secondary) - } footer: { - VStack(alignment: .leading) { - Text("These settings are for your current profile **\(m.currentUser?.displayName ?? "")**.") - Text("They can be overridden in contact and group settings.") - } - .foregroundColor(theme.colors.secondary) - .frame(maxWidth: .infinity, alignment: .leading) - } - .confirmationDialog(contactReceiptsDialogTitle, isPresented: $contactReceiptsDialogue, titleVisibility: .visible) { - Button(contactReceipts ? "Enable (keep overrides)" : "Disable (keep overrides)") { - setSendReceiptsContacts(contactReceipts, clearOverrides: false) - } - Button(contactReceipts ? "Enable for all" : "Disable for all", role: .destructive) { - setSendReceiptsContacts(contactReceipts, clearOverrides: true) - } - Button("Cancel", role: .cancel) { - contactReceiptsReset = true - contactReceipts.toggle() - } - } - .confirmationDialog(groupReceiptsDialogTitle, isPresented: $groupReceiptsDialogue, titleVisibility: .visible) { - Button(groupReceipts ? "Enable (keep overrides)" : "Disable (keep overrides)") { - setSendReceiptsGroups(groupReceipts, clearOverrides: false) - } - Button(groupReceipts ? "Enable for all" : "Disable for all", role: .destructive) { - setSendReceiptsGroups(groupReceipts, clearOverrides: true) - } - Button("Cancel", role: .cancel) { - groupReceiptsReset = true - groupReceipts.toggle() + NavigationLink(destination: morePrivacyView) { + settingsRow("ellipsis", color: theme.colors.secondary) { Text("More privacy") } } } } @@ -243,6 +177,132 @@ struct PrivacySettings: View { } } + @ViewBuilder + private func morePrivacyView() -> some View { + List { + Section { + settingsRow("message", color: theme.colors.secondary) { + Toggle("Show last messages", isOn: $showChatPreviews) + } + settingsRow("rectangle.and.pencil.and.ellipsis", color: theme.colors.secondary) { + Toggle("Message draft", isOn: $saveLastDraft) + } + .onChange(of: saveLastDraft) { saveDraft in + if !saveDraft { + m.draft = nil + m.draftChatId = nil + } + } + } header: { + Text("Chats") + .foregroundColor(theme.colors.secondary) + } + + Section { + settingsRow("lock.doc", color: theme.colors.secondary) { + Toggle("Encrypt local files", isOn: $encryptLocalFiles) + .onChange(of: encryptLocalFiles) { + setEncryptLocalFiles($0) + } + } + settingsRow("network.badge.shield.half.filled", color: theme.colors.secondary) { + Toggle("Protect IP address", isOn: $askToApproveRelays) + } + } header: { + Text("Files") + .foregroundColor(theme.colors.secondary) + } footer: { + if askToApproveRelays { + Text("The app will ask to confirm downloads from unknown file servers (except .onion).") + .foregroundColor(theme.colors.secondary) + } else { + Text("Without Tor or VPN, your IP address will be visible to file servers.") + .foregroundColor(theme.colors.secondary) + } + } + + Section { + NavigationLink { + List { + Section { + SelectionListView(list: NotificationPreviewMode.values, selection: $m.notificationPreview) { previewMode in + ntfPreviewModeGroupDefault.set(previewMode) + m.notificationPreview = previewMode + } + } footer: { + VStack(alignment: .leading, spacing: 1) { + Text("You can set lock screen notification preview via settings.") + .foregroundColor(theme.colors.secondary) + Button("Open Settings") { + DispatchQueue.main.async { + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) + } + } + } + } + } + .navigationTitle("Show preview") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.inline) + } label: { + HStack { + Text("Show preview") + Spacer() + Text(m.notificationPreview.label) + } + } + } header: { + Text("Notifications") + .foregroundColor(theme.colors.secondary) + } + + Section { + settingsRow("person", color: theme.colors.secondary) { + Toggle("Contacts", isOn: $contactReceipts) + } + settingsRow("person.2", color: theme.colors.secondary) { + Toggle("Small groups (max 20)", isOn: $groupReceipts) + } + } header: { + Text("Send delivery receipts to") + .foregroundColor(theme.colors.secondary) + } footer: { + VStack(alignment: .leading) { + Text("These settings are for your current profile **\(m.currentUser?.displayName ?? "")**.") + Text("They can be overridden in contact and group settings.") + } + .foregroundColor(theme.colors.secondary) + .frame(maxWidth: .infinity, alignment: .leading) + } + .confirmationDialog(contactReceiptsDialogTitle, isPresented: $contactReceiptsDialogue, titleVisibility: .visible) { + Button(contactReceipts ? "Enable (keep overrides)" : "Disable (keep overrides)") { + setSendReceiptsContacts(contactReceipts, clearOverrides: false) + } + Button(contactReceipts ? "Enable for all" : "Disable for all", role: .destructive) { + setSendReceiptsContacts(contactReceipts, clearOverrides: true) + } + Button("Cancel", role: .cancel) { + contactReceiptsReset = true + contactReceipts.toggle() + } + } + .confirmationDialog(groupReceiptsDialogTitle, isPresented: $groupReceiptsDialogue, titleVisibility: .visible) { + Button(groupReceipts ? "Enable (keep overrides)" : "Disable (keep overrides)") { + setSendReceiptsGroups(groupReceipts, clearOverrides: false) + } + Button(groupReceipts ? "Enable for all" : "Disable for all", role: .destructive) { + setSendReceiptsGroups(groupReceipts, clearOverrides: true) + } + Button("Cancel", role: .cancel) { + groupReceiptsReset = true + groupReceipts.toggle() + } + } + } + .navigationTitle("More privacy") + .modifier(ThemedBackground(grouped: true)) + } + private func setEncryptLocalFiles(_ enable: Bool) { do { try apiSetEncryptLocalFiles(enable) diff --git a/apps/ios/Shared/Views/UserSettings/SetDeliveryReceiptsView.swift b/apps/ios/Shared/Views/UserSettings/SetDeliveryReceiptsView.swift index e03dace43d..e46edbc5af 100644 --- a/apps/ios/Shared/Views/UserSettings/SetDeliveryReceiptsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SetDeliveryReceiptsView.swift @@ -69,7 +69,7 @@ struct SetDeliveryReceiptsView: View { Button { AlertManager.shared.showAlert(Alert( title: Text("Delivery receipts are disabled!"), - message: Text("You can enable them later via app Privacy & Security settings."), + message: Text("You can enable them later via app Your privacy settings."), primaryButton: .default(Text("Don't show again")) { m.setDeliveryReceipts = false privacyDeliveryReceiptsSet.set(true) diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index a903329454..93f32a53a6 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -290,47 +290,7 @@ struct SettingsView: View { func settingsView() -> some View { List { - let user = chatModel.currentUser - Section(header: Text("Settings").foregroundColor(theme.colors.secondary)) { - NavigationLink { - NotificationsView() - .navigationTitle("Notifications") - .modifier(ThemedBackground(grouped: true)) - } label: { - HStack { - notificationsIcon() - Text("Notifications") - } - } - .disabled(chatModel.chatRunning != true) - - NavigationLink { - NetworkAndServers() - .navigationTitle("Network & servers") - .modifier(ThemedBackground(grouped: true)) - } label: { - settingsRow("externaldrive.connected.to.line.below", color: theme.colors.secondary) { Text("Network & servers") } - } - .disabled(chatModel.chatRunning != true) - - NavigationLink { - CallSettings() - .navigationTitle("Your calls") - .modifier(ThemedBackground(grouped: true)) - } label: { - settingsRow("video", color: theme.colors.secondary) { Text("Audio & video calls") } - } - .disabled(chatModel.chatRunning != true) - - NavigationLink { - PrivacySettings() - .navigationTitle("Your privacy") - .modifier(ThemedBackground(grouped: true)) - } label: { - settingsRow("lock", color: theme.colors.secondary) { Text("Privacy & security") } - } - .disabled(chatModel.chatRunning != true) - + Section(header: Text(verbatim: "").foregroundColor(theme.colors.secondary)) { if UIApplication.shared.supportsAlternateIcons { NavigationLink { AppearanceSettings() @@ -341,10 +301,24 @@ struct SettingsView: View { } .disabled(chatModel.chatRunning != true) } - } - Section(header: Text("Chat database").foregroundColor(theme.colors.secondary)) { + NavigationLink { + PrivacySettings() + .navigationTitle("Your privacy") + .modifier(ThemedBackground(grouped: true)) + } label: { + settingsRow("lock", color: theme.colors.secondary) { Text("Your privacy") } + } + .disabled(chatModel.chatRunning != true) + + NavigationLink { + helpAndSupportView + } label: { + settingsRow("questionmark", color: theme.colors.secondary) { Text("Help & support") } + } + chatDatabaseRow() + NavigationLink { MigrateFromDevice(showProgressOnSettings: $showProgress) .toolbar { @@ -360,6 +334,58 @@ struct SettingsView: View { } } + Section(header: Text("Advanced settings").foregroundColor(theme.colors.secondary)) { + NavigationLink { + NetworkAndServers() + .navigationTitle("Network & servers") + .modifier(ThemedBackground(grouped: true)) + } label: { + settingsRow("externaldrive.connected.to.line.below", color: theme.colors.secondary) { Text("Network & servers") } + } + .disabled(chatModel.chatRunning != true) + + NavigationLink { + NotificationsView() + .navigationTitle("Notifications") + .modifier(ThemedBackground(grouped: true)) + } label: { + HStack { + notificationsIcon() + Text("Notifications") + } + } + .disabled(chatModel.chatRunning != true) + + NavigationLink { + CallSettings() + .navigationTitle("Your calls") + .modifier(ThemedBackground(grouped: true)) + } label: { + settingsRow("video", color: theme.colors.secondary) { Text("Audio & video calls") } + } + .disabled(chatModel.chatRunning != true) + + NavigationLink { + VersionView() + .navigationBarTitle("App version") + .modifier(ThemedBackground()) + } label: { + Text(verbatim: "v\(appVersion ?? "?")") + } + } + } + .navigationTitle("Your settings") + .modifier(ThemedBackground(grouped: true)) + .onDisappear { + chatModel.showingTerminal = false + chatModel.terminalItems = [] + } + } + + @ViewBuilder + private var helpAndSupportView: some View { + List { + let user = chatModel.currentUser Section(header: Text("Help").foregroundColor(theme.colors.secondary)) { if let user = user { NavigationLink { @@ -378,6 +404,7 @@ struct SettingsView: View { } label: { settingsRow("plus", color: theme.colors.secondary) { Text("What's new") } } + NavigationLink { SimpleXInfo(onboarding: false) .navigationBarTitle("", displayMode: .inline) @@ -386,6 +413,9 @@ struct SettingsView: View { } label: { settingsRow("info", color: theme.colors.secondary) { Text("About SimpleX Chat") } } + } + + Section(header: Text("Contact").foregroundColor(theme.colors.secondary)) { settingsRow("number", color: theme.colors.secondary) { Button("Send questions and ideas") { dismiss() @@ -398,7 +428,7 @@ struct SettingsView: View { settingsRow("envelope", color: theme.colors.secondary) { Text("[Send us email](mailto:chat@simplex.chat)") } } - Section(header: Text("Support SimpleX Chat").foregroundColor(theme.colors.secondary)) { + Section(header: Text("Support the project").foregroundColor(theme.colors.secondary)) { settingsRow("keyboard", color: theme.colors.secondary) { ExternalLink("Contribute", destination: URL(string: "https://github.com/simplex-chat/simplex-chat#contribute")!) } @@ -421,42 +451,21 @@ struct SettingsView: View { } } } - - Section(header: Text("Develop").foregroundColor(theme.colors.secondary)) { - NavigationLink { - DeveloperView() - .navigationTitle("Developer tools") - .modifier(ThemedBackground(grouped: true)) - } label: { - settingsRow("chevron.left.forwardslash.chevron.right", color: theme.colors.secondary) { Text("Developer tools") } - } - NavigationLink { - VersionView() - .navigationBarTitle("App version") - .modifier(ThemedBackground()) - } label: { - Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))") - } - } } - .navigationTitle("Your settings") + .navigationTitle("Help & support") .modifier(ThemedBackground(grouped: true)) - .onDisappear { - chatModel.showingTerminal = false - chatModel.terminalItems = [] - } } - + private func chatDatabaseRow() -> some View { NavigationLink { DatabaseView(dismissSettingsSheet: dismiss, chatItemTTL: chatModel.chatItemTTL) - .navigationTitle("Your chat database") + .navigationTitle("Chat data") .modifier(ThemedBackground(grouped: true)) } label: { let color: Color = chatModel.chatDbEncrypted == false ? .orange : theme.colors.secondary settingsRow("internaldrive", color: color) { HStack { - Text("Database passphrase & export") + Text("Chat data") Spacer() if chatModel.chatRunning == false { Image(systemName: "exclamationmark.octagon.fill").foregroundColor(.red) diff --git a/apps/ios/Shared/Views/UserSettings/VersionView.swift b/apps/ios/Shared/Views/UserSettings/VersionView.swift index 0fc2b4cb3e..e30c11699e 100644 --- a/apps/ios/Shared/Views/UserSettings/VersionView.swift +++ b/apps/ios/Shared/Views/UserSettings/VersionView.swift @@ -10,21 +10,33 @@ import SwiftUI import SimpleXChat struct VersionView: View { + @EnvironmentObject var theme: AppTheme @State var versionInfo: CoreVersionInfo? var body: some View { - VStack(alignment: .leading) { - Text("App version: v\(appVersion ?? "?")") - Text("App build: \(appBuild ?? "?")") - if let info = versionInfo { - Text("Core version: v\(info.version)") - if let v = try? AttributedString(markdown: "simplexmq: v\(info.simplexmqVersion) ([\(info.simplexmqCommit.prefix(7))](https://github.com/simplex-chat/simplexmq/commit/\(info.simplexmqCommit)))") { - Text(v) + List { + Section { + Text("App version: v\(appVersion ?? "?")") + Text("App build: \(appBuild ?? "?")") + if let info = versionInfo { + Text("Core version: v\(info.version)") + if let v = try? AttributedString(markdown: "simplexmq: v\(info.simplexmqVersion) ([\(info.simplexmqCommit.prefix(7))](https://github.com/simplex-chat/simplexmq/commit/\(info.simplexmqCommit)))") { + Text(v) + } + } + } + + Section { + NavigationLink { + DeveloperView() + .navigationTitle("Developer") + .modifier(ThemedBackground(grouped: true)) + } label: { + Text("Developer") } } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) - .padding() .onAppear { do { versionInfo = try apiGetVersion() diff --git a/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff b/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff index 427430b833..bd8d6a17ef 100644 --- a/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff +++ b/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff @@ -1157,8 +1157,8 @@ يطور No comment provided by engineer. - - Developer tools + + Developer أدوات المطور No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index 364cee97e5..89d5014c95 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -3014,8 +3014,8 @@ alert button Developer options No comment provided by engineer. - - Developer tools + + Developer Инструменти за разработчици No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff b/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff index fbda1abd29..bdb0c0ce24 100644 --- a/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff +++ b/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff @@ -1223,8 +1223,8 @@ Develop No comment provided by engineer. - - Developer tools + + Developer No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index 5ba29ec846..f7beecf4ba 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -2904,8 +2904,8 @@ alert button Developer options No comment provided by engineer. - - Developer tools + + Developer Nástroje pro vývojáře No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 797a489c92..822c13649e 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -3130,8 +3130,8 @@ alert button Optionen für Entwickler No comment provided by engineer. - - Developer tools + + Developer Entwicklertools No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff index 7a560bb41b..181f51eb99 100644 --- a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff +++ b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff @@ -1100,8 +1100,8 @@ Available in v5.1 Develop No comment provided by engineer. - - Developer tools + + Developer No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index c108dcc904..bd498d53e3 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -3140,9 +3140,9 @@ alert button Developer options No comment provided by engineer. - - Developer tools - Developer tools + + Developer + Developer No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index d93e692a63..60de758c9c 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -3130,8 +3130,8 @@ alert button Opciones desarrollador No comment provided by engineer. - - Developer tools + + Developer Herramientas desarrollo No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index 5656516b7d..c818fdc472 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -2791,8 +2791,8 @@ alert button Developer options No comment provided by engineer. - - Developer tools + + Developer Kehittäjätyökalut No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index 3ea0859d76..4d1616df20 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -3039,8 +3039,8 @@ alert button Options pour les développeurs No comment provided by engineer. - - Developer tools + + Developer Outils du développeur No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff b/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff index f94d6cefd8..a22d30dd73 100644 --- a/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff +++ b/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff @@ -1356,8 +1356,8 @@ Available in v5.1 לְפַתֵחַ No comment provided by engineer. - - Developer tools + + Developer כלי מפתחים No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff b/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff index 2aa945f603..33fe9a0e9c 100644 --- a/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff +++ b/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff @@ -1012,8 +1012,8 @@ Develop No comment provided by engineer. - - Developer tools + + Developer No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index 129436ecb0..11b447dc2b 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -3130,8 +3130,8 @@ alert button Fejlesztői beállítások No comment provided by engineer. - - Developer tools + + Developer Fejlesztői eszközök No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index 469da88ce2..876cdb10bb 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -3130,8 +3130,8 @@ alert button Opzioni sviluppatore No comment provided by engineer. - - Developer tools + + Developer Strumenti di sviluppo No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 13396b13a4..ff0d085c08 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -2891,8 +2891,8 @@ alert button 開発者向けの設定 No comment provided by engineer. - - Developer tools + + Developer 開発ツール No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff b/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff index ca51a875c7..01b5e2b9a2 100644 --- a/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff +++ b/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff @@ -1141,8 +1141,8 @@ Develop No comment provided by engineer. - - Developer tools + + Developer No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff b/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff index 4b51d66a34..5aa2a98854 100644 --- a/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff +++ b/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff @@ -1005,8 +1005,8 @@ Develop No comment provided by engineer. - - Developer tools + + Developer No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index 9f1818fba9..a7670a4475 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -3040,8 +3040,8 @@ alert button Ontwikkelaars opties No comment provided by engineer. - - Developer tools + + Developer Ontwikkel gereedschap No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index 2644708927..a60b5891d4 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -3063,8 +3063,8 @@ alert button Opcje deweloperskie No comment provided by engineer. - - Developer tools + + Developer Narzędzia deweloperskie No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff b/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff index d9af0624bf..032a33ff62 100644 --- a/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff +++ b/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff @@ -1179,8 +1179,8 @@ Desenvolver No comment provided by engineer. - - Developer tools + + Developer Ferramentas de desenvolvimento No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff index e4fac55bcb..3905130e84 100644 --- a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff +++ b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff @@ -1203,8 +1203,8 @@ Available in v5.1 Develop No comment provided by engineer. - - Developer tools + + Developer No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index a3971c0325..22df6680f6 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -3130,8 +3130,8 @@ alert button Опции разработчика No comment provided by engineer. - - Developer tools + + Developer Инструменты разработчика No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index cd2e30977d..11d6b28091 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -2779,8 +2779,8 @@ alert button Developer options No comment provided by engineer. - - Developer tools + + Developer เครื่องมือสำหรับนักพัฒนา No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index 1189b53e3c..b673b609b9 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -3066,8 +3066,8 @@ alert button Geliştirici seçenekleri No comment provided by engineer. - - Developer tools + + Developer Geliştirici araçları No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index 49f9e21eda..3c0c6ef8de 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -3050,8 +3050,8 @@ alert button Можливості для розробників No comment provided by engineer. - - Developer tools + + Developer Інструменти для розробників No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index 8823f17204..9001375615 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -3060,8 +3060,8 @@ alert button 开发者选项 No comment provided by engineer. - - Developer tools + + Developer 开发者工具 No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff b/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff index 0e4e383b52..ebd6f4da71 100644 --- a/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff @@ -1148,8 +1148,8 @@ 開發 No comment provided by engineer. - - Developer tools + + Developer 開發者工具 No comment provided by engineer. diff --git a/apps/ios/SimpleX SE/ShareModel.swift b/apps/ios/SimpleX SE/ShareModel.swift index 18f3e2c344..9790e5944f 100644 --- a/apps/ios/SimpleX SE/ShareModel.swift +++ b/apps/ios/SimpleX SE/ShareModel.swift @@ -75,7 +75,7 @@ class ShareModel: ObservableObject { func setup(context: NSExtensionContext) { if appLocalAuthEnabledGroupDefault.get() && !allowShareExtensionGroupDefault.get() { - errorAlert = ErrorAlert(title: "App is locked!", message: "You can allow sharing in Privacy & Security / SimpleX Lock settings.") + errorAlert = ErrorAlert(title: "App is locked!", message: "You can allow sharing in Your privacy / SimpleX Lock settings.") return } if let item = context.inputItems.first as? NSExtensionItem, diff --git a/apps/ios/SimpleX SE/de.lproj/Localizable.strings b/apps/ios/SimpleX SE/de.lproj/Localizable.strings index df368686e8..1d2e5a4b01 100644 --- a/apps/ios/SimpleX SE/de.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/de.lproj/Localizable.strings @@ -107,5 +107,5 @@ "Wrong database passphrase" = "Falsches Datenbank-Passwort"; /* No comment provided by engineer. */ -"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Sie können das Teilen in den Einstellungen zu Datenschutz & Sicherheit / SimpleX-Sperre erlauben."; +"You can allow sharing in Your privacy / SimpleX Lock settings." = "Sie können das Teilen in den Einstellungen zu Datenschutz & Sicherheit / SimpleX-Sperre erlauben."; diff --git a/apps/ios/SimpleX SE/es.lproj/Localizable.strings b/apps/ios/SimpleX SE/es.lproj/Localizable.strings index 4cc5029537..5bb42b4f0a 100644 --- a/apps/ios/SimpleX SE/es.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/es.lproj/Localizable.strings @@ -107,5 +107,5 @@ "Wrong database passphrase" = "Contraseña incorrecta de la base de datos"; /* No comment provided by engineer. */ -"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Puedes dar permiso para compartir en Privacidad y Seguridad / Bloque SimpleX."; +"You can allow sharing in Your privacy / SimpleX Lock settings." = "Puedes dar permiso para compartir en Privacidad y Seguridad / Bloque SimpleX."; diff --git a/apps/ios/SimpleX SE/fr.lproj/Localizable.strings b/apps/ios/SimpleX SE/fr.lproj/Localizable.strings index 46a458b471..6b492ee882 100644 --- a/apps/ios/SimpleX SE/fr.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/fr.lproj/Localizable.strings @@ -107,5 +107,5 @@ "Wrong database passphrase" = "Mauvaise phrase secrète pour la base de données"; /* No comment provided by engineer. */ -"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Vous pouvez autoriser le partage dans les paramètres Confidentialité et sécurité / SimpleX Lock."; +"You can allow sharing in Your privacy / SimpleX Lock settings." = "Vous pouvez autoriser le partage dans les paramètres Confidentialité et sécurité / SimpleX Lock."; diff --git a/apps/ios/SimpleX SE/hu.lproj/Localizable.strings b/apps/ios/SimpleX SE/hu.lproj/Localizable.strings index 3aad39c5d1..dae06b1d73 100644 --- a/apps/ios/SimpleX SE/hu.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/hu.lproj/Localizable.strings @@ -107,5 +107,5 @@ "Wrong database passphrase" = "Érvénytelen adatbázis-jelmondat"; /* No comment provided by engineer. */ -"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "A megosztást az Adatvédelem és biztonság / SimpleX-zár menüben engedélyezheti."; +"You can allow sharing in Your privacy / SimpleX Lock settings." = "A megosztást az Adatvédelem és biztonság / SimpleX-zár menüben engedélyezheti."; diff --git a/apps/ios/SimpleX SE/it.lproj/Localizable.strings b/apps/ios/SimpleX SE/it.lproj/Localizable.strings index e3d34650a3..3b27092f24 100644 --- a/apps/ios/SimpleX SE/it.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/it.lproj/Localizable.strings @@ -107,5 +107,5 @@ "Wrong database passphrase" = "Password del database sbagliata"; /* No comment provided by engineer. */ -"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Puoi consentire la condivisione in Privacy e sicurezza / impostazioni di SimpleX Lock."; +"You can allow sharing in Your privacy / SimpleX Lock settings." = "Puoi consentire la condivisione in Privacy e sicurezza / impostazioni di SimpleX Lock."; diff --git a/apps/ios/SimpleX SE/nl.lproj/Localizable.strings b/apps/ios/SimpleX SE/nl.lproj/Localizable.strings index e5d2487b54..050cb1a735 100644 --- a/apps/ios/SimpleX SE/nl.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/nl.lproj/Localizable.strings @@ -107,5 +107,5 @@ "Wrong database passphrase" = "Verkeerde database wachtwoord"; /* No comment provided by engineer. */ -"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "U kunt delen toestaan in de instellingen voor Privacy en beveiliging / SimpleX Lock."; +"You can allow sharing in Your privacy / SimpleX Lock settings." = "U kunt delen toestaan in de instellingen voor Privacy en beveiliging / SimpleX Lock."; diff --git a/apps/ios/SimpleX SE/pl.lproj/Localizable.strings b/apps/ios/SimpleX SE/pl.lproj/Localizable.strings index c563431c28..0755e1e374 100644 --- a/apps/ios/SimpleX SE/pl.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/pl.lproj/Localizable.strings @@ -107,5 +107,5 @@ "Wrong database passphrase" = "Nieprawidłowe hasło bazy danych"; /* No comment provided by engineer. */ -"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Możesz zezwolić na udostępnianie w ustawieniach Prywatność i bezpieczeństwo / Blokada SimpleX."; +"You can allow sharing in Your privacy / SimpleX Lock settings." = "Możesz zezwolić na udostępnianie w ustawieniach Prywatność i bezpieczeństwo / Blokada SimpleX."; diff --git a/apps/ios/SimpleX SE/ru.lproj/Localizable.strings b/apps/ios/SimpleX SE/ru.lproj/Localizable.strings index e4c8c000d4..90a5841738 100644 --- a/apps/ios/SimpleX SE/ru.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/ru.lproj/Localizable.strings @@ -107,5 +107,5 @@ "Wrong database passphrase" = "Неправильный пароль базы данных"; /* No comment provided by engineer. */ -"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Вы можете разрешить функцию Поделиться в настройках Конфиденциальности / Блокировка SimpleX."; +"You can allow sharing in Your privacy / SimpleX Lock settings." = "Вы можете разрешить функцию Поделиться в настройках Конфиденциальности / Блокировка SimpleX."; diff --git a/apps/ios/SimpleX SE/tr.lproj/Localizable.strings b/apps/ios/SimpleX SE/tr.lproj/Localizable.strings index baef71c127..69b122832d 100644 --- a/apps/ios/SimpleX SE/tr.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/tr.lproj/Localizable.strings @@ -107,5 +107,5 @@ "Wrong database passphrase" = "Yanlış veritabanı parolası"; /* No comment provided by engineer. */ -"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Gizlilik ve Güvenlik / SimpleX Lock ayarlarından paylaşıma izin verebilirsiniz."; +"You can allow sharing in Your privacy / SimpleX Lock settings." = "Gizlilik ve Güvenlik / SimpleX Lock ayarlarından paylaşıma izin verebilirsiniz."; diff --git a/apps/ios/SimpleX SE/uk.lproj/Localizable.strings b/apps/ios/SimpleX SE/uk.lproj/Localizable.strings index a6da81185e..7574470d01 100644 --- a/apps/ios/SimpleX SE/uk.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/uk.lproj/Localizable.strings @@ -107,5 +107,5 @@ "Wrong database passphrase" = "Неправильна ключова фраза до бази даних"; /* No comment provided by engineer. */ -"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Ви можете дозволити спільний доступ у налаштуваннях Конфіденційність і безпека / SimpleX Lock."; +"You can allow sharing in Your privacy / SimpleX Lock settings." = "Ви можете дозволити спільний доступ у налаштуваннях Конфіденційність і безпека / SimpleX Lock."; diff --git a/apps/ios/SimpleX SE/zh-Hans.lproj/Localizable.strings b/apps/ios/SimpleX SE/zh-Hans.lproj/Localizable.strings index 362e2edb74..fc046ab087 100644 --- a/apps/ios/SimpleX SE/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/SimpleX SE/zh-Hans.lproj/Localizable.strings @@ -107,5 +107,5 @@ "Wrong database passphrase" = "数据库密码错误"; /* No comment provided by engineer. */ -"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "您可以在 \"隐私与安全\"/\"SimpleX Lock \"设置中允许共享。"; +"You can allow sharing in Your privacy / SimpleX Lock settings." = "您可以在 \"隐私与安全\"/\"SimpleX Lock \"设置中允许共享。"; diff --git a/apps/ios/SimpleXChat/ImageUtils.swift b/apps/ios/SimpleXChat/ImageUtils.swift index f93b090517..2d36a7a53a 100644 --- a/apps/ios/SimpleXChat/ImageUtils.swift +++ b/apps/ios/SimpleXChat/ImageUtils.swift @@ -297,7 +297,7 @@ private func uniqueCombine(_ fileName: String, fullPath: Bool = false) -> String let name = ns.deletingPathExtension let ext = ns.pathExtension let suffix = (n == 0) ? "" : "_\(n)" - let f = "\(name)\(suffix).\(ext)" + let f = ext.isEmpty ? "\(name)\(suffix)" : "\(name)\(suffix).\(ext)" return (FileManager.default.fileExists(atPath: fullPath ? f : getAppFilePath(f).path)) ? tryCombine(fileName, n + 1) : f } return tryCombine(fileName, 0) diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index ec869e05b4..68f2cfe502 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -1659,7 +1659,7 @@ alert button */ "Develop" = "Разработване"; /* No comment provided by engineer. */ -"Developer tools" = "Инструменти за разработчици"; +"Developer" = "Инструменти за разработчици"; /* No comment provided by engineer. */ "Device" = "Устройство"; @@ -3217,7 +3217,7 @@ alert button */ "Preview" = "Визуализация"; /* No comment provided by engineer. */ -"Privacy & security" = "Поверителност и сигурност"; +"Your privacy" = "Поверителност и сигурност"; /* No comment provided by engineer. */ "Private filenames" = "Поверителни имена на файлове"; @@ -4452,7 +4452,7 @@ server test failure */ "You can enable later via Settings" = "Можете да активирате по-късно през Настройки"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Можете да ги активирате по-късно през настройките за \"Поверителност и сигурност\" на приложението."; +"You can enable them later via app Your privacy settings." = "Можете да ги активирате по-късно през настройките за \"Поверителност и сигурност\" на приложението."; /* No comment provided by engineer. */ "You can give another try." = "Можете да опитате още веднъж."; diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index fc4b3f0fc6..0b74ef9504 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -1306,7 +1306,7 @@ alert button */ "Develop" = "Vyvinout"; /* No comment provided by engineer. */ -"Developer tools" = "Nástroje pro vývojáře"; +"Developer" = "Nástroje pro vývojáře"; /* No comment provided by engineer. */ "Device" = "Zařízení"; @@ -2578,7 +2578,7 @@ alert button */ "Preview" = "Náhled"; /* No comment provided by engineer. */ -"Privacy & security" = "Ochrana osobních údajů a zabezpečení"; +"Your privacy" = "Ochrana osobních údajů a zabezpečení"; /* No comment provided by engineer. */ "Private filenames" = "Soukromé názvy souborů"; @@ -3543,7 +3543,7 @@ server test failure */ "You can enable later via Settings" = "Můžete povolit později v Nastavení"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Můžete je povolit později v nastavení Soukromí & Bezpečnosti aplikace"; +"You can enable them later via app Your privacy settings." = "Můžete je povolit později v nastavení Soukromí & Bezpečnosti aplikace"; /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "Profil uživatele můžete skrýt nebo ztlumit - přejeďte prstem doprava."; diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index 2c4e37791b..80246b727b 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -2064,7 +2064,7 @@ alert button */ "Developer options" = "Optionen für Entwickler"; /* No comment provided by engineer. */ -"Developer tools" = "Entwicklertools"; +"Developer" = "Entwicklertools"; /* No comment provided by engineer. */ "Device" = "Gerät"; @@ -4533,7 +4533,7 @@ alert button */ "Previously connected servers" = "Bisher verbundene Server"; /* No comment provided by engineer. */ -"Privacy & security" = "Datenschutz & Sicherheit"; +"Your privacy" = "Datenschutz & Sicherheit"; /* No comment provided by engineer. */ "Privacy for your customers." = "Schutz der Privatsphäre Ihrer Kunden."; @@ -6759,7 +6759,7 @@ server test failure */ "You can enable later via Settings" = "Sie können diese später in den Einstellungen aktivieren"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Sie können diese später in den Datenschutz & Sicherheits-Einstellungen der App aktivieren."; +"You can enable them later via app Your privacy settings." = "Sie können diese später in den Datenschutz & Sicherheits-Einstellungen der App aktivieren."; /* No comment provided by engineer. */ "You can give another try." = "Sie können es nochmal probieren."; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index cf03ae6dbf..e0611c0afc 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -2064,7 +2064,7 @@ alert button */ "Developer options" = "Opciones desarrollador"; /* No comment provided by engineer. */ -"Developer tools" = "Herramientas desarrollo"; +"Developer" = "Herramientas desarrollo"; /* No comment provided by engineer. */ "Device" = "Dispositivo"; @@ -4533,7 +4533,7 @@ alert button */ "Previously connected servers" = "Servidores conectados previamente"; /* No comment provided by engineer. */ -"Privacy & security" = "Seguridad y Privacidad"; +"Your privacy" = "Seguridad y Privacidad"; /* No comment provided by engineer. */ "Privacy for your customers." = "Privacidad para tus clientes."; @@ -6759,7 +6759,7 @@ server test failure */ "You can enable later via Settings" = "Puedes activar más tarde en Configuración"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Puedes activarlos más tarde en la configuración de Privacidad y Seguridad."; +"You can enable them later via app Your privacy settings." = "Puedes activarlos más tarde en la configuración de Privacidad y Seguridad."; /* No comment provided by engineer. */ "You can give another try." = "Puedes intentarlo de nuevo."; diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index 3b1bd6523c..36b516374e 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -982,7 +982,7 @@ alert button */ "Develop" = "Kehitä"; /* No comment provided by engineer. */ -"Developer tools" = "Kehittäjätyökalut"; +"Developer" = "Kehittäjätyökalut"; /* No comment provided by engineer. */ "Device" = "Laite"; @@ -2232,7 +2232,7 @@ new chat action */ "Preview" = "Esikatselu"; /* No comment provided by engineer. */ -"Privacy & security" = "Yksityisyys ja turvallisuus"; +"Your privacy" = "Yksityisyys ja turvallisuus"; /* No comment provided by engineer. */ "Private filenames" = "Yksityiset tiedostonimet"; @@ -3179,7 +3179,7 @@ server test failure */ "You can enable later via Settings" = "Voit ottaa käyttöön myöhemmin asetusten kautta"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Voit ottaa ne käyttöön myöhemmin sovelluksen Yksityisyys & Turvallisuus -asetuksista."; +"You can enable them later via app Your privacy settings." = "Voit ottaa ne käyttöön myöhemmin sovelluksen Yksityisyys & Turvallisuus -asetuksista."; /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "Voit piilottaa tai mykistää käyttäjäprofiilin pyyhkäisemällä sitä oikealle."; diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 91cd6f3078..98cdb4aec8 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -1745,7 +1745,7 @@ alert button */ "Developer options" = "Options pour les développeurs"; /* No comment provided by engineer. */ -"Developer tools" = "Outils du développeur"; +"Developer" = "Outils du développeur"; /* No comment provided by engineer. */ "Device" = "Appareil"; @@ -3742,7 +3742,7 @@ alert button */ "Previously connected servers" = "Serveurs précédemment connectés"; /* No comment provided by engineer. */ -"Privacy & security" = "Vie privée et sécurité"; +"Your privacy" = "Vie privée et sécurité"; /* No comment provided by engineer. */ "Privacy for your customers." = "Respect de la vie privée de vos clients."; @@ -5448,7 +5448,7 @@ server test failure */ "You can enable later via Settings" = "Vous pouvez l'activer ultérieurement via Paramètres"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Vous pouvez les activer ultérieurement via les paramètres de Confidentialité et Sécurité de l'application."; +"You can enable them later via app Your privacy settings." = "Vous pouvez les activer ultérieurement via les paramètres de Confidentialité et Sécurité de l'application."; /* No comment provided by engineer. */ "You can give another try." = "Vous pouvez faire un nouvel essai."; diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index 029fb9edd5..362d9157fd 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -2064,7 +2064,7 @@ alert button */ "Developer options" = "Fejlesztői beállítások"; /* No comment provided by engineer. */ -"Developer tools" = "Fejlesztői eszközök"; +"Developer" = "Fejlesztői eszközök"; /* No comment provided by engineer. */ "Device" = "Eszköz"; @@ -4533,7 +4533,7 @@ alert button */ "Previously connected servers" = "Korábban kapcsolódott kiszolgálók"; /* No comment provided by engineer. */ -"Privacy & security" = "Adatvédelem és biztonság"; +"Your privacy" = "Adatvédelem és biztonság"; /* No comment provided by engineer. */ "Privacy for your customers." = "Saját ügyfeleinek adatvédelme."; @@ -6759,7 +6759,7 @@ server test failure */ "You can enable later via Settings" = "Később engedélyezheti a beállításokban"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Később engedélyezheti őket az „Adatvédelem és biztonság” menüben."; +"You can enable them later via app Your privacy settings." = "Később engedélyezheti őket az „Adatvédelem és biztonság” menüben."; /* No comment provided by engineer. */ "You can give another try." = "Megpróbálhatja még egyszer."; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index c882eb662c..8d4fa3532b 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -2064,7 +2064,7 @@ alert button */ "Developer options" = "Opzioni sviluppatore"; /* No comment provided by engineer. */ -"Developer tools" = "Strumenti di sviluppo"; +"Developer" = "Strumenti di sviluppo"; /* No comment provided by engineer. */ "Device" = "Dispositivo"; @@ -4533,7 +4533,7 @@ alert button */ "Previously connected servers" = "Server precedentemente connessi"; /* No comment provided by engineer. */ -"Privacy & security" = "Privacy e sicurezza"; +"Your privacy" = "Privacy e sicurezza"; /* No comment provided by engineer. */ "Privacy for your customers." = "Privacy per i tuoi clienti."; @@ -6759,7 +6759,7 @@ server test failure */ "You can enable later via Settings" = "Puoi attivarle più tardi nelle impostazioni"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Puoi attivarle più tardi nelle impostazioni di privacy e sicurezza dell'app."; +"You can enable them later via app Your privacy settings." = "Puoi attivarle più tardi nelle impostazioni di privacy e sicurezza dell'app."; /* No comment provided by engineer. */ "You can give another try." = "Puoi fare un altro tentativo."; diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index 35d8732e3f..b88eb10414 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -1270,7 +1270,7 @@ alert button */ "Developer options" = "開発者向けの設定"; /* No comment provided by engineer. */ -"Developer tools" = "開発ツール"; +"Developer" = "開発ツール"; /* No comment provided by engineer. */ "Device" = "端末"; @@ -2533,7 +2533,7 @@ alert button */ "Preview" = "プレビュー"; /* No comment provided by engineer. */ -"Privacy & security" = "プライバシーとセキュリティ"; +"Your privacy" = "プライバシーとセキュリティ"; /* No comment provided by engineer. */ "Private filenames" = "プライベートなファイル名"; @@ -3459,7 +3459,7 @@ server test failure */ "You can enable later via Settings" = "あとで設定から有効にできます"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "あとでアプリのプライバシーとセキュリティの設定から有効にすることができます。"; +"You can enable them later via app Your privacy settings." = "あとでアプリのプライバシーとセキュリティの設定から有効にすることができます。"; /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "ユーザープロファイルを右にスワイプすると、非表示またはミュートにすることができます。"; diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index 407665bbec..1c776a7bbe 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -1773,7 +1773,7 @@ alert button */ "Developer options" = "Ontwikkelaars opties"; /* No comment provided by engineer. */ -"Developer tools" = "Ontwikkel gereedschap"; +"Developer" = "Ontwikkel gereedschap"; /* No comment provided by engineer. */ "Device" = "Apparaat"; @@ -3929,7 +3929,7 @@ alert button */ "Previously connected servers" = "Eerder verbonden servers"; /* No comment provided by engineer. */ -"Privacy & security" = "Privacy en beveiliging"; +"Your privacy" = "Privacy en beveiliging"; /* No comment provided by engineer. */ "Privacy for your customers." = "Privacy voor uw klanten."; @@ -5777,7 +5777,7 @@ server test failure */ "You can enable later via Settings" = "U kunt later inschakelen via Instellingen"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "U kunt ze later inschakelen via de privacy- en beveiligingsinstellingen van de app."; +"You can enable them later via app Your privacy settings." = "U kunt ze later inschakelen via de privacy- en beveiligingsinstellingen van de app."; /* No comment provided by engineer. */ "You can give another try." = "Je kunt het nog een keer proberen."; diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index 8905300160..9cc6eca252 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -1845,7 +1845,7 @@ alert button */ "Developer options" = "Opcje deweloperskie"; /* No comment provided by engineer. */ -"Developer tools" = "Narzędzia deweloperskie"; +"Developer" = "Narzędzia deweloperskie"; /* No comment provided by engineer. */ "Device" = "Urządzenie"; @@ -4131,7 +4131,7 @@ alert button */ "Previously connected servers" = "Wcześniej połączone serwery"; /* No comment provided by engineer. */ -"Privacy & security" = "Prywatność i bezpieczeństwo"; +"Your privacy" = "Prywatność i bezpieczeństwo"; /* No comment provided by engineer. */ "Privacy for your customers." = "Prywatność dla Twoich klientów."; @@ -6135,7 +6135,7 @@ server test failure */ "You can enable later via Settings" = "Możesz włączyć później w Ustawieniach"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Możesz je włączyć później w ustawieniach Prywatności i Bezpieczeństwa aplikacji."; +"You can enable them later via app Your privacy settings." = "Możesz je włączyć później w ustawieniach Prywatności i Bezpieczeństwa aplikacji."; /* No comment provided by engineer. */ "You can give another try." = "Możesz spróbować ponownie."; diff --git a/apps/ios/product/README.md b/apps/ios/product/README.md index 107c0e6569..fd25b09d01 100644 --- a/apps/ios/product/README.md +++ b/apps/ios/product/README.md @@ -101,7 +101,7 @@ End-to-end encrypted audio and video communication. | Call history | Call events displayed as chat items | `Shared/Views/Chat/ChatItem/CICallItemView.swift` | | Incoming call view | Dedicated UI for incoming call notifications | `Shared/Views/Call/IncomingCallView.swift` | -### 5. Privacy & Security +### 5. Your privacy Encryption, authentication, and privacy controls. diff --git a/apps/ios/product/views/settings.md b/apps/ios/product/views/settings.md index 3cc4da5d2b..7e7f653910 100644 --- a/apps/ios/product/views/settings.md +++ b/apps/ios/product/views/settings.md @@ -4,7 +4,7 @@ ## Purpose -Configure all aspects of app behavior including notifications, network/servers, privacy, appearance, database management, call settings, and developer tools. Accessed from the UserPicker sheet on the chat list. +Configure all aspects of app behavior including notifications, network/servers, privacy, appearance, database management, call settings, and Developer. Accessed from the UserPicker sheet on the chat list. ## Route / Navigation @@ -22,7 +22,7 @@ Configure all aspects of app behavior including notifications, network/servers, | Notifications | `bolt` (color varies by token status) | `NotificationsView` | Push notification mode and preview settings | | Network & servers | `externaldrive.connected.to.line.below` | `NetworkAndServers` | SMP/XFTP servers, proxy, .onion hosts, advanced network | | Audio & video calls | `video` | `CallSettings` | WebRTC relay policy, ICE servers, CallKit options | -| Privacy & security | `lock` | `PrivacySettings` | SimpleX Lock, screen protection, delivery receipts, auto-accept | +| Your privacy | `lock` | `PrivacySettings` | SimpleX Lock, screen protection, delivery receipts, auto-accept | | Appearance | `sun.max` | `AppearanceSettings` | Theme, language, wallpapers, chat bubbles, toolbar opacity | All rows disabled when `chatModel.chatRunning != true`. Appearance row only shown when `UIApplication.shared.supportsAlternateIcons`. @@ -77,7 +77,7 @@ Adding a relay: `NewChatRelayView` form with name, address, test, and enable tog Server validation (`validateServers_`) now returns both errors and warnings. -#### Privacy & Security (`PrivacySettings`) +#### Your privacy (`PrivacySettings`) | Setting | Description | |---|---| @@ -152,7 +152,7 @@ Database row shows exclamation octagon icon in red when `chatRunning == false`. | Row | Icon | Destination | Description | |---|---|---|---| -| Developer tools | `chevron.left.forwardslash.chevron.right` | `DeveloperView` | Chat console/terminal, log level, confirm DB upgrades | +| Developer | `chevron.left.forwardslash.chevron.right` | `DeveloperView` | Chat console/terminal, log level, confirm DB upgrades | | App version | (none) | `VersionView` | Shows "v{version} ({build})" | ## Loading / Error States diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 9f79b5dea0..042359dd7e 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -2064,7 +2064,7 @@ alert button */ "Developer options" = "Опции разработчика"; /* No comment provided by engineer. */ -"Developer tools" = "Инструменты разработчика"; +"Developer" = "Разработчик"; /* No comment provided by engineer. */ "Device" = "Устройство"; @@ -4533,7 +4533,7 @@ alert button */ "Previously connected servers" = "Ранее подключенные серверы"; /* No comment provided by engineer. */ -"Privacy & security" = "Конфиденциальность"; +"Your privacy" = "Конфиденциальность"; /* No comment provided by engineer. */ "Privacy for your customers." = "Конфиденциальность для ваших покупателей."; @@ -6759,7 +6759,7 @@ server test failure */ "You can enable later via Settings" = "Вы можете включить их позже в Настройках"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Вы можете включить их позже в настройках Конфиденциальности."; +"You can enable them later via app Your privacy settings." = "Вы можете включить их позже в настройках Конфиденциальности."; /* No comment provided by engineer. */ "You can give another try." = "Вы можете попробовать ещё раз."; diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index cc3abea189..62bf4d3d59 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -946,7 +946,7 @@ alert button */ "Develop" = "พัฒนา"; /* No comment provided by engineer. */ -"Developer tools" = "เครื่องมือสำหรับนักพัฒนา"; +"Developer" = "เครื่องมือสำหรับนักพัฒนา"; /* No comment provided by engineer. */ "Device" = "อุปกรณ์"; @@ -2172,7 +2172,7 @@ new chat action */ "Preview" = "ดูตัวอย่าง"; /* No comment provided by engineer. */ -"Privacy & security" = "ความเป็นส่วนตัวและความปลอดภัย"; +"Your privacy" = "ความเป็นส่วนตัวและความปลอดภัย"; /* No comment provided by engineer. */ "Private filenames" = "ชื่อไฟล์ส่วนตัว"; @@ -3089,7 +3089,7 @@ server test failure */ "You can enable later via Settings" = "คุณสามารถเปิดใช้งานในภายหลังผ่านการตั้งค่า"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "คุณสามารถเปิดใช้งานได้ในภายหลังผ่านการตั้งค่าความเป็นส่วนตัวและความปลอดภัยของแอป"; +"You can enable them later via app Your privacy settings." = "คุณสามารถเปิดใช้งานได้ในภายหลังผ่านการตั้งค่าความเป็นส่วนตัวและความปลอดภัยของแอป"; /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "คุณสามารถซ่อนหรือปิดเสียงโปรไฟล์ผู้ใช้ - ปัดไปทางขวา"; diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index e06989afee..8dfc23078c 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -1859,7 +1859,7 @@ alert button */ "Developer options" = "Geliştirici seçenekleri"; /* No comment provided by engineer. */ -"Developer tools" = "Geliştirici araçları"; +"Developer" = "Geliştirici araçları"; /* No comment provided by engineer. */ "Device" = "Cihaz"; @@ -4099,7 +4099,7 @@ alert button */ "Previously connected servers" = "Önceden bağlanılmış sunucular"; /* No comment provided by engineer. */ -"Privacy & security" = "Gizlilik & güvenlik"; +"Your privacy" = "Gizlilik & güvenlik"; /* No comment provided by engineer. */ "Privacy for your customers." = "Müşterileriniz için gizlilik."; @@ -6064,7 +6064,7 @@ server test failure */ "You can enable later via Settings" = "Daha sonra Ayarlardan etkinleştirebilirsin"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Daha sonra uygulamanın Gizlilik ve Güvenlik ayarlarından etkinleştirebilirsiniz."; +"You can enable them later via app Your privacy settings." = "Daha sonra uygulamanın Gizlilik ve Güvenlik ayarlarından etkinleştirebilirsiniz."; /* No comment provided by engineer. */ "You can give another try." = "Bir kez daha deneyebilirsiniz."; diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index 4a21eb4ae8..ff2f64e2ed 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -1806,7 +1806,7 @@ alert button */ "Developer options" = "Можливості для розробників"; /* No comment provided by engineer. */ -"Developer tools" = "Інструменти для розробників"; +"Developer" = "Інструменти для розробників"; /* No comment provided by engineer. */ "Device" = "Пристрій"; @@ -4019,7 +4019,7 @@ alert button */ "Previously connected servers" = "Раніше підключені сервери"; /* No comment provided by engineer. */ -"Privacy & security" = "Конфіденційність і безпека"; +"Your privacy" = "Конфіденційність і безпека"; /* No comment provided by engineer. */ "Privacy for your customers." = "Конфіденційність для ваших клієнтів."; @@ -5966,7 +5966,7 @@ server test failure */ "You can enable later via Settings" = "Ви можете увімкнути пізніше в Налаштуваннях"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Ви можете увімкнути їх пізніше в налаштуваннях конфіденційності та безпеки програми."; +"You can enable them later via app Your privacy settings." = "Ви можете увімкнути їх пізніше в налаштуваннях конфіденційності та безпеки програми."; /* No comment provided by engineer. */ "You can give another try." = "Ви можете спробувати ще раз."; diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index 13be5125ea..f3138230f9 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -1836,7 +1836,7 @@ alert button */ "Developer options" = "开发者选项"; /* No comment provided by engineer. */ -"Developer tools" = "开发者工具"; +"Developer" = "开发者工具"; /* No comment provided by engineer. */ "Device" = "设备"; @@ -4107,7 +4107,7 @@ alert button */ "Previously connected servers" = "以前连接的服务器"; /* No comment provided by engineer. */ -"Privacy & security" = "隐私和安全"; +"Your privacy" = "隐私和安全"; /* No comment provided by engineer. */ "Privacy for your customers." = "客户隐私。"; @@ -6093,7 +6093,7 @@ server test failure */ "You can enable later via Settings" = "您可以稍后在设置中启用它"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "您可以稍后通过应用程序的 \"隐私与安全 \"设置启用它们。"; +"You can enable them later via app Your privacy settings." = "您可以稍后通过应用程序的 \"隐私与安全 \"设置启用它们。"; /* No comment provided by engineer. */ "You can give another try." = "你可以再试一次。"; diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt index 04b59732dd..5e9706d713 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt @@ -1,7 +1,15 @@ package chat.simplex.common.views.usersettings +import SectionItemView import SectionView +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.* import chat.simplex.common.views.helpers.* @@ -11,19 +19,19 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @Composable -actual fun SettingsSectionApp( +actual fun AdvancedSettingsAppSection( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - showVersion: () -> Unit, - withAuth: (title: String, desc: String, block: () -> Unit) -> Unit + withAuth: (title: String, desc: String, block: () -> Unit) -> Unit, ) { - SectionView(stringResource(MR.strings.settings_section_title_app)) { - SettingsActionItem(painterResource(MR.images.ic_restart_alt), stringResource(MR.strings.settings_restart_app), ::restartApp) - SettingsActionItem(painterResource(MR.images.ic_power_settings_new), stringResource(MR.strings.settings_shutdown), { shutdownAppAlert(::shutdownApp) }) + SectionView { SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(withAuth) }) - AppVersionItem(showVersion) } } +@Composable +actual fun AppShutdownItem() { + SettingsActionItem(painterResource(MR.images.ic_power_settings_new), stringResource(MR.strings.settings_shutdown), ::shutdownAppAlert) +} fun restartApp() { ProcessPhoenix.triggerRebirth(androidAppContext) @@ -36,11 +44,28 @@ private fun shutdownApp() { Runtime.getRuntime().exit(0) } -private fun shutdownAppAlert(onConfirm: () -> Unit) { - AlertManager.shared.showAlertDialog( +private fun shutdownAppAlert() { + AlertManager.shared.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.shutdown_alert_question), text = generalGetString(MR.strings.shutdown_alert_desc), - destructive = true, - onConfirm = onConfirm + buttons = { + Column { + SectionItemView({ AlertManager.shared.hideAlert() }) { + Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center) + } + SectionItemView({ + AlertManager.shared.hideAlert() + restartApp() + }) { + Text(stringResource(MR.strings.settings_restart_app), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + SectionItemView({ + AlertManager.shared.hideAlert() + shutdownApp() + }) { + Text(stringResource(MR.strings.settings_shutdown), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + } + } ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index dce1b6ea33..13be8b1d71 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -248,6 +248,8 @@ fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = private fun deleteContactOrConversationDialog(chat: Chat, contact: Contact, chatModel: ChatModel, close: (() -> Unit)?) { AlertManager.shared.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.delete_contact_question), + text = contact.displayName, + parseHtml = false, buttons = { Column { // Only delete conversation @@ -306,7 +308,8 @@ private fun deleteActiveContactDialog(chat: Chat, contact: Contact, chatModel: C AlertManager.shared.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.delete_contact_question), - text = generalGetString(MR.strings.delete_contact_cannot_undo_warning), + text = "${contact.displayName}\n\n${generalGetString(MR.strings.delete_contact_cannot_undo_warning)}", + parseHtml = false, buttons = { Column { // Keep conversation toggle @@ -361,7 +364,8 @@ private fun deleteActiveContactDialog(chat: Chat, contact: Contact, chatModel: C private fun deleteContactWithoutConversation(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?) { AlertManager.shared.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.confirm_delete_contact_question), - text = generalGetString(MR.strings.delete_contact_cannot_undo_warning), + text = "${chat.chatInfo.displayName}\n\n${generalGetString(MR.strings.delete_contact_cannot_undo_warning)}", + parseHtml = false, buttons = { Column { // Delete and notify contact @@ -417,7 +421,8 @@ private fun deleteContactWithoutConversation(chat: Chat, chatModel: ChatModel, c private fun deleteNotReadyContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?) { AlertManager.shared.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.confirm_delete_contact_question), - text = generalGetString(MR.strings.delete_contact_cannot_undo_warning), + text = "${chat.chatInfo.displayName}\n\n${generalGetString(MR.strings.delete_contact_cannot_undo_warning)}", + parseHtml = false, buttons = { // Confirm SectionItemView({ @@ -492,7 +497,8 @@ fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, chatDe fun clearChatDialog(chat: Chat, close: (() -> Unit)? = null) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.clear_chat_question), - text = generalGetString(MR.strings.clear_chat_warning), + text = "${chat.chatInfo.displayName}\n\n${generalGetString(MR.strings.clear_chat_warning)}", + parseHtml = false, confirmText = generalGetString(MR.strings.clear_verb), onConfirm = { controller.clearChat(chat, close) }, destructive = true, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index d874079238..26824cdd49 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -288,16 +288,22 @@ expect fun AttachmentSelection( ) fun MutableState.onFilesAttached(uris: List) { - val groups = uris.groupBy { isImage(it) } - val images = groups[true] ?: emptyList() + val groups = uris.groupBy { isImage(it) || isVideoUri(it) } + val media = groups[true] ?: emptyList() val files = groups[false] ?: emptyList() - if (images.isNotEmpty()) { - CoroutineScope(Dispatchers.IO).launch { processPickedMedia(images, null) } + if (media.isNotEmpty()) { + CoroutineScope(Dispatchers.IO).launch { processPickedMedia(media, null) } } else if (files.isNotEmpty()) { processPickedFile(uris.first(), null) } } +private fun isVideoUri(uri: URI): Boolean { + val name = getFileName(uri)?.lowercase() ?: return false + return name.endsWith(".mov") || name.endsWith(".avi") || name.endsWith(".mp4") || + name.endsWith(".mpg") || name.endsWith(".mpeg") || name.endsWith(".mkv") +} + fun MutableState.processPickedFile(uri: URI?, text: String?) { if (uri != null) { val fileSize = getFileSize(uri) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index 3e9b5f6f48..c88e3b2bff 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -199,7 +199,8 @@ fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, cl } AlertManager.shared.showAlertDialog( title = generalGetString(titleId), - text = generalGetString(messageId), + text = "${groupInfo.displayName}\n\n${generalGetString(messageId)}", + parseHtml = false, confirmText = generalGetString(MR.strings.delete_verb), onConfirm = { withBGApi { @@ -233,7 +234,8 @@ fun leaveGroupDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl MR.strings.you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved AlertManager.shared.showAlertDialog( title = generalGetString(titleId), - text = generalGetString(messageId), + text = "${groupInfo.displayName}\n\n${generalGetString(messageId)}", + parseHtml = false, confirmText = generalGetString(MR.strings.leave_group_button), onConfirm = { withLongRunningApi(60_000) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 64288d9055..b4abbe70a2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -12,8 +12,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.geometry.* import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.clipPath import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.* @@ -1224,12 +1226,25 @@ fun Modifier.clipChatItem(chatItem: ChatItem? = null, tailVisible: Boolean = fal val style = shapeStyle(chatItem, chatItemTail.value, tailVisible, revealed) val cornerRoundness = chatItemRoundness.value.coerceIn(0f, 1f) - val shape = when (style) { - is ShapeStyle.Bubble -> chatItemShape(cornerRoundness, LocalDensity.current, style.tailVisible, chatItem?.chatDir?.sent == true) - is ShapeStyle.RoundRect -> RoundedCornerShape(style.radius * cornerRoundness) + return when (style) { + is ShapeStyle.Bubble -> { + // Modifier.clip of the bubble GenericShape mis-hit-tests its path on very tall + // items, dropping long-press on the lower part of the bubble (issue #6991). Clip + // in the draw pass instead — drawing is clipped identically (the press ripple + // included), with no effect on hit-test. + val shape = chatItemShape(cornerRoundness, LocalDensity.current, style.tailVisible, chatItem?.chatDir?.sent == true) + this.drawWithCache { + val path = Path().apply { + addOutline(shape.createOutline(size, layoutDirection, this@drawWithCache)) + } + onDrawWithContent { + clipPath(path) { this@onDrawWithContent.drawContent() } + } + } + } + // RoundRect hit-tests correctly — no bug here, keep the antialiased Modifier.clip. + is ShapeStyle.RoundRect -> this.clip(RoundedCornerShape(style.radius * cornerRoundness)) } - - return this.clip(shape) } private fun chatItemShape(roundness: Float, density: Density, tailVisible: Boolean, sent: Boolean = false): GenericShape = GenericShape { size, _ -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index d3533bbd02..7dfb52063e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -772,10 +772,11 @@ fun rejectContactRequest(rhId: Long?, contactRequestId: Long, chatModel: ChatMod fun deleteContactConnectionAlert(rhId: Long?, connection: PendingContactConnection, chatModel: ChatModel, onSuccess: () -> Unit) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.delete_pending_connection__question), - text = generalGetString( + text = "${connection.displayName}\n\n" + generalGetString( if (connection.initiated) MR.strings.contact_you_shared_link_with_wont_be_able_to_connect else MR.strings.connection_you_accepted_will_be_cancelled ), + parseHtml = false, confirmText = generalGetString(MR.strings.delete_verb), onConfirm = { withBGApi { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index 648a0eb8e7..80f97d1caf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -44,29 +44,8 @@ fun DatabaseView() { val prefs = m.controller.appPrefs val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) } val chatLastStart = remember { mutableStateOf(prefs.chatLastStart.get()) } - val chatArchiveFile = remember { mutableStateOf(null) } val stopped = remember { m.chatRunning }.value == false - val saveArchiveLauncher = rememberFileChooserLauncher(false) { to: URI? -> - val archive = chatArchiveFile.value - if (archive != null && to != null) { - copyFileToFile(File(archive), to) {} - } - // delete no matter the database was exported or canceled the export process - if (archive != null) { - File(archive).delete() - chatArchiveFile.value = null - } - } val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(appFilesDir.absolutePath)) } - val importArchiveLauncher = rememberFileChooserLauncher(true) { to: URI? -> - if (to != null) { - importArchiveAlert { - stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) { - importArchive(to, appFilesCountAndSize, progressIndicator, false) - } - } - } - } val chatItemTTL = remember { mutableStateOf(m.chatItemTTL.value) } Box( Modifier.fillMaxSize(), @@ -79,27 +58,10 @@ fun DatabaseView() { useKeychain.value, m.chatDbEncrypted.value, m.controller.appPrefs.storeDBPassphrase.state.value, - m.controller.appPrefs.initialRandomDBPassphrase, - importArchiveLauncher, appFilesCountAndSize, chatItemTTL, user, m.users, - startChat = { startChat(m, chatLastStart, m.chatDbChanged, progressIndicator) }, - stopChatAlert = { stopChatAlert(m, progressIndicator) }, - exportArchive = { - stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) { - exportArchive(m, progressIndicator, chatArchiveFile, saveArchiveLauncher) - } - }, - deleteChatAlert = { - deleteChatAlert { - stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) { - deleteChat(m, progressIndicator) - true - } - } - }, deleteAppFilesAndMedia = { deleteFilesAndMediaAlert { stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) { @@ -120,12 +82,9 @@ fun DatabaseView() { setCiTTL(m, rhId, chatItemTTL, progressIndicator, appFilesCountAndSize) } }, - disconnectAllHosts = { - val connected = chatModel.remoteHosts.filter { it.sessionState is RemoteHostSessionState.Connected } - connected.forEachIndexed { index, h -> - controller.stopRemoteHostAndReloadHosts(h, index == connected.lastIndex && chatModel.connectedToRemote()) - } - } + showDatabaseManagement = { + ModalManager.start.showModal(cardScreen = true) { DatabaseManagementView() } + }, ) if (progressIndicator.value) { Box( @@ -151,24 +110,18 @@ fun DatabaseLayout( useKeyChain: Boolean, chatDbEncrypted: Boolean?, passphraseSaved: Boolean, - initialRandomDBPassphrase: SharedPreference, - importArchiveLauncher: FileChooserLauncher, appFilesCountAndSize: MutableState>, chatItemTTL: MutableState, currentUser: User?, users: List, - startChat: () -> Unit, - stopChatAlert: () -> Unit, - exportArchive: () -> Unit, - deleteChatAlert: () -> Unit, deleteAppFilesAndMedia: () -> Unit, onChatItemTTLSelected: (ChatItemTTL?) -> Unit, - disconnectAllHosts: () -> Unit, + showDatabaseManagement: () -> Unit, ) { val operationsDisabled = progressIndicator && !chatModel.desktopNoUserNoRemote ColumnWithScrollBar { - AppBarTitle(stringResource(MR.strings.your_chat_database)) + AppBarTitle(stringResource(MR.strings.chat_data)) if (!chatModel.desktopNoUserNoRemote) { SectionView(stringResource(MR.strings.messages_section_title)) { @@ -187,79 +140,17 @@ fun DatabaseLayout( ) SectionDividerSpaced() } - val toggleEnabled = remember { chatModel.remoteHosts }.none { it.sessionState is RemoteHostSessionState.Connected } - if (chatModel.localUserCreated.value == true) { - // still show the toggle in case database was stopped when the user opened this screen because it can be in the following situations: - // - database was stopped after migration and the app relaunched - // - something wrong happened with database operations and the database couldn't be launched when it should - SectionView(stringResource(MR.strings.run_chat_section)) { - if (!toggleEnabled) { - SectionItemView(disconnectAllHosts) { - Text(generalGetString(MR.strings.disconnect_remote_hosts), Modifier.fillMaxWidth(), color = WarningOrange) - } - } - RunChatSetting(stopped, toggleEnabled && !progressIndicator, startChat, stopChatAlert) - } - if (stopped) SectionTextFooter(stringResource(MR.strings.you_must_use_the_most_recent_version_of_database)) - SectionDividerSpaced() - } - SectionView(stringResource(MR.strings.chat_database_section)) { - if (chatModel.localUserCreated.value != true && !toggleEnabled) { - SectionItemView(disconnectAllHosts) { - Text(generalGetString(MR.strings.disconnect_remote_hosts), Modifier.fillMaxWidth(), color = WarningOrange) - } - } + SectionView { val unencrypted = chatDbEncrypted == false SettingsActionItem( if (unencrypted) painterResource(MR.images.ic_lock_open_right) else if (useKeyChain) painterResource(MR.images.ic_vpn_key_filled) else painterResource(MR.images.ic_lock), - stringResource(MR.strings.database_passphrase), - click = { ModalManager.start.showModal(cardScreen = true) { DatabaseEncryptionView(chatModel, false) } }, + stringResource(MR.strings.database_passphrase_and_export), + click = showDatabaseManagement, iconColor = if (unencrypted || (appPlatform.isDesktop && passphraseSaved)) WarningOrange else MaterialTheme.colors.secondary, disabled = operationsDisabled ) - if (appPlatform.isDesktop) { - SettingsActionItem( - painterResource(MR.images.ic_folder_open), - stringResource(MR.strings.open_database_folder), - ::desktopOpenDatabaseDir, - disabled = operationsDisabled - ) - } - SettingsActionItem( - painterResource(MR.images.ic_ios_share), - stringResource(MR.strings.export_database), - click = { - if (initialRandomDBPassphrase.get()) { - exportProhibitedAlert() - ModalManager.start.showModal { - DatabaseEncryptionView(chatModel, false) - } - } else { - exportArchive() - } - }, - textColor = MaterialTheme.colors.primary, - iconColor = MaterialTheme.colors.primary, - disabled = operationsDisabled - ) - SettingsActionItem( - painterResource(MR.images.ic_download), - stringResource(MR.strings.import_database), - { withLongRunningApi { importArchiveLauncher.launch("application/zip") } }, - textColor = Color.Red, - iconColor = Color.Red, - disabled = operationsDisabled - ) - SettingsActionItem( - painterResource(MR.images.ic_delete_forever), - stringResource(MR.strings.delete_database), - deleteChatAlert, - textColor = Color.Red, - iconColor = Color.Red, - disabled = operationsDisabled - ) } SectionDividerSpaced() @@ -287,6 +178,155 @@ fun DatabaseLayout( } } +@Composable +fun DatabaseManagementView() { + val m = chatModel + val progressIndicator = remember { mutableStateOf(false) } + val prefs = m.controller.appPrefs + val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) } + val chatLastStart = remember { mutableStateOf(prefs.chatLastStart.get()) } + val chatArchiveFile = remember { mutableStateOf(null) } + val stopped = remember { m.chatRunning }.value == false + val saveArchiveLauncher = rememberFileChooserLauncher(false) { to: URI? -> + val archive = chatArchiveFile.value + if (archive != null && to != null) { + copyFileToFile(File(archive), to) {} + } + // delete no matter the database was exported or canceled the export process + if (archive != null) { + File(archive).delete() + chatArchiveFile.value = null + } + } + val appFilesCountAndSize = remember { mutableStateOf(directoryFileCountAndSize(appFilesDir.absolutePath)) } + val importArchiveLauncher = rememberFileChooserLauncher(true) { to: URI? -> + if (to != null) { + importArchiveAlert { + stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) { + importArchive(to, appFilesCountAndSize, progressIndicator, false) + } + } + } + } + val operationsDisabled = progressIndicator.value && !m.desktopNoUserNoRemote + + Box(Modifier.fillMaxSize()) { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.database_passphrase_and_export)) + + val toggleEnabled = remember { chatModel.remoteHosts }.none { it.sessionState is RemoteHostSessionState.Connected } + val disconnectAllHosts = { + val connected = chatModel.remoteHosts.filter { it.sessionState is RemoteHostSessionState.Connected } + connected.forEachIndexed { index, h -> + controller.stopRemoteHostAndReloadHosts(h, index == connected.lastIndex && chatModel.connectedToRemote()) + } + } + SectionView(stringResource(MR.strings.chat_database_section)) { + if (chatModel.localUserCreated.value != true && !toggleEnabled) { + SectionItemView(disconnectAllHosts) { + Text(generalGetString(MR.strings.disconnect_remote_hosts), Modifier.fillMaxWidth(), color = WarningOrange) + } + } + val unencrypted = m.chatDbEncrypted.value == false + SettingsActionItem( + if (unencrypted) painterResource(MR.images.ic_lock_open_right) else if (useKeychain.value) painterResource(MR.images.ic_vpn_key_filled) + else painterResource(MR.images.ic_lock), + stringResource(MR.strings.database_passphrase), + click = { ModalManager.start.showModal(cardScreen = true) { DatabaseEncryptionView(chatModel, false) } }, + iconColor = if (unencrypted || (appPlatform.isDesktop && prefs.storeDBPassphrase.state.value)) WarningOrange else MaterialTheme.colors.secondary, + disabled = operationsDisabled + ) + if (appPlatform.isDesktop) { + SettingsActionItem( + painterResource(MR.images.ic_folder_open), + stringResource(MR.strings.open_database_folder), + ::desktopOpenDatabaseDir, + disabled = operationsDisabled + ) + } + SettingsActionItem( + painterResource(MR.images.ic_ios_share), + stringResource(MR.strings.export_database), + click = { + if (prefs.initialRandomDBPassphrase.get()) { + exportProhibitedAlert() + ModalManager.start.showModal { + DatabaseEncryptionView(chatModel, false) + } + } else { + stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) { + exportArchive(m, progressIndicator, chatArchiveFile, saveArchiveLauncher) + } + } + }, + textColor = MaterialTheme.colors.primary, + iconColor = MaterialTheme.colors.primary, + disabled = operationsDisabled + ) + SettingsActionItem( + painterResource(MR.images.ic_download), + stringResource(MR.strings.import_database), + { withLongRunningApi { importArchiveLauncher.launch("application/zip") } }, + textColor = Color.Red, + iconColor = Color.Red, + disabled = operationsDisabled + ) + SettingsActionItem( + painterResource(MR.images.ic_delete_forever), + stringResource(MR.strings.delete_database), + { + deleteChatAlert { + stopChatRunBlockStartChat(stopped, chatLastStart, progressIndicator) { + deleteChat(m, progressIndicator) + true + } + } + }, + textColor = Color.Red, + iconColor = Color.Red, + disabled = operationsDisabled + ) + } + + if (chatModel.localUserCreated.value == true) { + SectionDividerSpaced() + // still show the toggle in case database was stopped when the user opened this screen because it can be in the following situations: + // - database was stopped after migration and the app relaunched + // - something wrong happened with database operations and the database couldn't be launched when it should + SectionView(stringResource(MR.strings.run_chat_section)) { + if (!toggleEnabled) { + SectionItemView(disconnectAllHosts) { + Text(generalGetString(MR.strings.disconnect_remote_hosts), Modifier.fillMaxWidth(), color = WarningOrange) + } + } + RunChatSetting( + stopped, + toggleEnabled && !progressIndicator.value, + startChat = { startChat(m, chatLastStart, m.chatDbChanged, progressIndicator) }, + stopChatAlert = { stopChatAlert(m, progressIndicator) } + ) + } + if (stopped) SectionTextFooter(stringResource(MR.strings.you_must_use_the_most_recent_version_of_database)) + } + SectionBottomSpacer() + } + if (progressIndicator.value) { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + Modifier + .padding(horizontal = 2.dp) + .size(30.dp), + color = MaterialTheme.colors.secondary, + strokeWidth = 2.5.dp + ) + } + } + } +} + private fun setChatItemTTLAlert( m: ChatModel, rhId: Long?, selectedChatItemTTL: MutableState, progressIndicator: MutableState, @@ -832,19 +872,13 @@ fun PreviewDatabaseLayout() { useKeyChain = false, chatDbEncrypted = false, passphraseSaved = false, - initialRandomDBPassphrase = SharedPreference({ true }, {}), - importArchiveLauncher = rememberFileChooserLauncher(true) {}, appFilesCountAndSize = remember { mutableStateOf(0 to 0L) }, chatItemTTL = remember { mutableStateOf(ChatItemTTL.None) }, currentUser = User.sampleData, users = listOf(UserInfo.sampleData), - startChat = {}, - stopChatAlert = {}, - exportArchive = {}, - deleteChatAlert = {}, deleteAppFilesAndMedia = {}, onChatItemTTLSelected = {}, - disconnectAllHosts = {}, + showDatabaseManagement = {}, ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt index 3d670d1c43..1774cf4394 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt @@ -75,6 +75,8 @@ class AlertManager { onDismissRequest: (() -> Unit)? = null, hostDevice: Pair? = null, belowTextContent: @Composable (() -> Unit) = {}, + // When false, [text] is rendered as literal text — use for user-controlled content. + parseHtml: Boolean = true, buttons: @Composable () -> Unit, ) { showAlert { @@ -82,8 +84,14 @@ class AlertManager { onDismissRequest = { onDismissRequest?.invoke(); if (dismissible) hideAlert() }, title = alertTitle(title), buttons = { - AlertContent(text, hostDevice, extraPadding = true, textAlign = textAlign, belowTextContent = belowTextContent) { - buttons() + if (parseHtml) { + AlertContent(text, hostDevice, extraPadding = true, textAlign = textAlign, belowTextContent = belowTextContent) { + buttons() + } + } else { + AlertContent(text?.let { AnnotatedString(it) }, hostDevice, extraPadding = true) { + buttons() + } } }, shape = RoundedCornerShape(corner = CornerSize(25.dp)) @@ -122,13 +130,15 @@ class AlertManager { onDismissRequest: (() -> Unit)? = null, destructive: Boolean = false, hostDevice: Pair? = null, + // When false, [text] is rendered as literal text — use for user-controlled content. + parseHtml: Boolean = true, ) { showAlert { AlertDialog( onDismissRequest = { onDismissRequest?.invoke(); hideAlert() }, title = alertTitle(title), buttons = { - AlertContent(text, hostDevice, true) { + val buttonRow: @Composable () -> Unit = { Row( Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), horizontalArrangement = Arrangement.SpaceBetween @@ -149,6 +159,11 @@ class AlertManager { }, Modifier.focusRequester(focusRequester)) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) } } } + if (parseHtml) { + AlertContent(text, hostDevice, true, content = buttonRow) + } else { + AlertContent(text?.let { AnnotatedString(it) }, hostDevice, true, content = buttonRow) + } }, shape = RoundedCornerShape(corner = CornerSize(25.dp)) ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index 424d500085..23c622bc34 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -383,7 +383,7 @@ fun uniqueCombine(fileName: String, dir: File): String { val ext = orig.extension fun tryCombine(n: Int): String { val suffix = if (n == 0) "" else "_$n" - val f = "$name$suffix.$ext" + val f = if (ext.isEmpty()) "$name$suffix" else "$name$suffix.$ext" return if (File(dir, f).exists()) tryCombine(n + 1) else f } return tryCombine(0) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt index 150b2a38e0..91324bb39a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NotificationsSettingsView.kt @@ -24,43 +24,28 @@ import kotlin.collections.ArrayList fun NotificationsSettingsView( chatModel: ChatModel, ) { - val onNotificationPreviewModeSelected = { mode: NotificationPreviewMode -> - chatModel.controller.appPrefs.notificationPreviewMode.set(mode.name) - chatModel.notificationPreviewMode.value = mode - } - NotificationsSettingsLayout( notificationsMode = remember { chatModel.controller.appPrefs.notificationsMode.state }, - notificationPreviewMode = chatModel.notificationPreviewMode, - showPage = { page -> + showNotificationsMode = { ModalManager.start.showModalCloseable(true) { - when (page) { - CurrentPage.NOTIFICATIONS_MODE -> NotificationsModeView(chatModel.controller.appPrefs.notificationsMode.state) { changeNotificationsMode(it, chatModel) } - CurrentPage.NOTIFICATION_PREVIEW_MODE -> NotificationPreviewView(chatModel.notificationPreviewMode, onNotificationPreviewModeSelected) - } + NotificationsModeView(chatModel.controller.appPrefs.notificationsMode.state) { changeNotificationsMode(it, chatModel) } } }, ) } -enum class CurrentPage { - NOTIFICATIONS_MODE, NOTIFICATION_PREVIEW_MODE -} - @Composable fun NotificationsSettingsLayout( notificationsMode: State, - notificationPreviewMode: State, - showPage: (CurrentPage) -> Unit, + showNotificationsMode: () -> Unit, ) { val modes = remember { notificationModes() } - val previewModes = remember { notificationPreviewModes() } ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.notifications)) SectionView(null) { if (appPlatform == AppPlatform.ANDROID) { - SettingsActionItemWithContent(null, stringResource(MR.strings.settings_notifications_mode_title), { showPage(CurrentPage.NOTIFICATIONS_MODE) }) { + SettingsActionItemWithContent(null, stringResource(MR.strings.settings_notifications_mode_title), showNotificationsMode) { Text( modes.firstOrNull { it.value == notificationsMode.value }?.title ?: "", maxLines = 1, @@ -69,14 +54,6 @@ fun NotificationsSettingsLayout( ) } } - SettingsActionItemWithContent(null, stringResource(MR.strings.settings_notification_preview_mode_title), { showPage(CurrentPage.NOTIFICATION_PREVIEW_MODE) }) { - Text( - previewModes.firstOrNull { it.value == notificationPreviewMode.value }?.title ?: "", - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colors.secondary - ) - } } if (platform.androidIsXiaomiDevice() && (notificationsMode.value == NotificationsMode.PERIODIC || notificationsMode.value == NotificationsMode.SERVICE)) { SectionTextFooter(annotatedStringResource(MR.strings.xiaomi_ignore_battery_optimization)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index 7316c9bd82..cf34fd5a44 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp @@ -73,6 +74,48 @@ fun PrivacySettingsView( stringResource(MR.strings.sanitize_links_toggle), chatModel.controller.appPrefs.privacySanitizeLinks ) + } + SectionDividerSpaced() + + SectionView(stringResource(MR.strings.settings_section_title_files)) { + SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages) + BlurRadiusOptions(remember { appPrefs.privacyMediaBlurRadius.state }) { + appPrefs.privacyMediaBlurRadius.set(it) + } + } + + val currentUser = chatModel.currentUser.value + if (currentUser != null && !chatModel.desktopNoUserNoRemote) { + SectionDividerSpaced() + ContacRequestsFromGroupsSection( + currentUser = currentUser, + setAutoAcceptGrpDirectInvs = { enable -> + withApi { + chatModel.controller.apiSetUserAutoAcceptMemberContacts(currentUser, enable) + chatModel.currentUser.value = currentUser.copy(autoAcceptMemberContacts = enable) + } + } + ) + } + + SectionDividerSpaced() + SectionView { + SettingsActionItem( + painterResource(MR.images.ic_more_horiz), + stringResource(MR.strings.more_privacy), + showSettingsModal { MorePrivacyView(it) } + ) + } + SectionBottomSpacer() + } +} + +@Composable +fun MorePrivacyView(chatModel: ChatModel) { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.more_privacy)) + + SectionView(stringResource(MR.strings.settings_section_title_chats)) { SettingsPreferenceItem( painterResource(MR.images.ic_chat_bubble), stringResource(MR.strings.privacy_show_last_messages), @@ -98,10 +141,6 @@ fun PrivacySettingsView( SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles, onChange = { enable -> withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) } }) - SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages) - BlurRadiusOptions(remember { appPrefs.privacyMediaBlurRadius.state }) { - appPrefs.privacyMediaBlurRadius.set(it) - } SettingsPreferenceItem(painterResource(MR.images.ic_security), stringResource(MR.strings.protect_ip_address), chatModel.controller.appPrefs.privacyAskToApproveRelays) } SectionTextFooter( @@ -111,9 +150,34 @@ fun PrivacySettingsView( stringResource(MR.strings.without_tor_or_vpn_ip_address_will_be_visible_to_file_servers) } ) + SectionDividerSpaced() + + SectionView(stringResource(MR.strings.notifications)) { + val previewModes = remember { notificationPreviewModes() } + val notificationPreviewMode = remember { chatModel.notificationPreviewMode } + SettingsActionItemWithContent( + painterResource(MR.images.ic_visibility_off), + stringResource(MR.strings.settings_notification_preview_mode_title), + click = { + ModalManager.start.showModalCloseable(true) { + NotificationPreviewView(notificationPreviewMode) { mode -> + chatModel.controller.appPrefs.notificationPreviewMode.set(mode.name) + chatModel.notificationPreviewMode.value = mode + } + } + } + ) { + Text( + previewModes.firstOrNull { it.value == notificationPreviewMode.value }?.title ?: "", + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = MaterialTheme.colors.secondary + ) + } + } val currentUser = chatModel.currentUser.value - if (currentUser != null) { + if (currentUser != null && !chatModel.desktopNoUserNoRemote) { fun setSendReceiptsContacts(enable: Boolean, clearOverrides: Boolean) { withLongRunningApi(slow = 60_000) { val mrs = UserMsgReceiptSettings(enable, clearOverrides) @@ -164,57 +228,40 @@ fun PrivacySettingsView( } } - fun setAutoAcceptGrpDirectInvs(enable: Boolean) { - withApi { - chatModel.controller.apiSetUserAutoAcceptMemberContacts(currentUser, enable) - chatModel.currentUser.value = currentUser.copy(autoAcceptMemberContacts = enable) + SectionDividerSpaced() + DeliveryReceiptsSection( + currentUser = currentUser, + setOrAskSendReceiptsContacts = { enable -> + val contactReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat -> + if (chat.chatInfo is ChatInfo.Direct) { + val sendRcpts = chat.chatInfo.contact.chatSettings.sendRcpts + count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) + } else { + count + } + } + if (contactReceiptsOverrides == 0) { + setSendReceiptsContacts(enable, clearOverrides = false) + } else { + showUserContactsReceiptsAlert(enable, contactReceiptsOverrides, ::setSendReceiptsContacts) + } + }, + setOrAskSendReceiptsGroups = { enable -> + val groupReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat -> + if (chat.chatInfo is ChatInfo.Group) { + val sendRcpts = chat.chatInfo.groupInfo.chatSettings.sendRcpts + count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) + } else { + count + } + } + if (groupReceiptsOverrides == 0) { + setSendReceiptsGroups(enable, clearOverrides = false) + } else { + showUserGroupsReceiptsAlert(enable, groupReceiptsOverrides, ::setSendReceiptsGroups) + } } - } - - if (!chatModel.desktopNoUserNoRemote) { - SectionDividerSpaced() - ContacRequestsFromGroupsSection( - currentUser = currentUser, - setAutoAcceptGrpDirectInvs = { enable -> - setAutoAcceptGrpDirectInvs(enable) - } - ) - - SectionDividerSpaced() - DeliveryReceiptsSection( - currentUser = currentUser, - setOrAskSendReceiptsContacts = { enable -> - val contactReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat -> - if (chat.chatInfo is ChatInfo.Direct) { - val sendRcpts = chat.chatInfo.contact.chatSettings.sendRcpts - count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) - } else { - count - } - } - if (contactReceiptsOverrides == 0) { - setSendReceiptsContacts(enable, clearOverrides = false) - } else { - showUserContactsReceiptsAlert(enable, contactReceiptsOverrides, ::setSendReceiptsContacts) - } - }, - setOrAskSendReceiptsGroups = { enable -> - val groupReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat -> - if (chat.chatInfo is ChatInfo.Group) { - val sendRcpts = chat.chatInfo.groupInfo.chatSettings.sendRcpts - count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) - } else { - count - } - } - if (groupReceiptsOverrides == 0) { - setSendReceiptsGroups(enable, clearOverrides = false) - } else { - showUserGroupsReceiptsAlert(enable, groupReceiptsOverrides, ::setSendReceiptsGroups) - } - } - ) - } + ) } SectionBottomSpacer() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt index f17d3a6e4b..c0b822dbb4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt @@ -37,17 +37,15 @@ import chat.simplex.res.MR @Composable fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: () -> Unit) { - val user = chatModel.currentUser.value val stopped = chatModel.chatRunning.value == false + val showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit) = { modalView -> { ModalManager.start.showModal(settings = true, cardScreen = true) { modalView(chatModel) } } } SettingsLayout( stopped, chatModel.chatDbEncrypted.value == true, remember { chatModel.controller.appPrefs.storeDBPassphrase.state }.value, - remember { chatModel.controller.appPrefs.notificationsMode.state }, - user?.displayName, setPerformLA = setPerformLA, showModal = { modalView -> { ModalManager.start.showModal { modalView(chatModel) } } }, - showSettingsModal = { modalView -> { ModalManager.start.showModal(settings = true, cardScreen = true) { modalView(chatModel) } } }, + showSettingsModal = showSettingsModal, showSettingsModalWithSearch = { modalView -> ModalManager.start.showCustomModal { close -> val search = rememberSaveable { mutableStateOf("") } @@ -62,12 +60,7 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, close: ( }, showCustomModal = { modalView -> { ModalManager.start.showCustomModal { close -> modalView(chatModel, close) } } }, showVersion = { - withBGApi { - val info = chatModel.controller.apiGetVersion() - if (info != null) { - ModalManager.start.showModal { VersionInfoView(info) } - } - } + ModalManager.start.showModal(cardScreen = true) { VersionInfoView(showSettingsModal, ::doWithAuth) } }, withAuth = ::doWithAuth, ) @@ -84,8 +77,6 @@ fun SettingsLayout( stopped: Boolean, encrypted: Boolean, passphraseSaved: Boolean, - notificationsMode: State, - userDisplayName: String?, setPerformLA: (Boolean) -> Unit, showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), @@ -98,30 +89,52 @@ fun SettingsLayout( LaunchedEffect(Unit) { hideKeyboard(view) } - val uriHandler = LocalUriHandler.current + val notificationsMode = remember { chatModel.controller.appPrefs.notificationsMode.state } ColumnWithScrollBar { AppBarTitle(stringResource(MR.strings.your_settings)) - SectionView(stringResource(MR.strings.settings_section_title_settings)) { - SettingsActionItem(painterResource(if (notificationsMode.value == NotificationsMode.OFF) MR.images.ic_bolt_off else MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped) - SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showCustomModal { _, close -> NetworkAndServersView(close) }, disabled = stopped) - SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped) - SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped) + SectionView { SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it) }) - } - SectionDividerSpaced() - - SectionView(stringResource(MR.strings.settings_section_title_chat_database)) { + SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.your_privacy), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped) + SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.help_and_support), showSettingsModal { HelpAndSupportView(it, showModal, showCustomModal) }) DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView() }, stopped) SettingsActionItem(painterResource(MR.images.ic_ios_share), stringResource(MR.strings.migrate_from_device_to_another_device), { withAuth(generalGetString(MR.strings.auth_open_migration_to_another_device), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.fullscreen.showCustomModal { close -> MigrateFromDeviceView(close) } } }, disabled = stopped) } - SectionDividerSpaced() + SectionView(stringResource(MR.strings.advanced_settings)) { + SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showCustomModal { _, close -> NetworkAndServersView(close) }, disabled = stopped) + if (appPlatform == AppPlatform.ANDROID) { + SettingsActionItem(painterResource(if (notificationsMode.value == NotificationsMode.OFF) MR.images.ic_bolt_off else MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped) + } + SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped) + AppShutdownItem() + AppVersionItem(showVersion) + } + SectionBottomSpacer() + } +} + +@Composable +fun HelpAndSupportView( + chatModel: ChatModel, + showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + showCustomModal: (@Composable ModalData.(ChatModel, () -> Unit) -> Unit) -> (() -> Unit), +) { + val uriHandler = LocalUriHandler.current + val stopped = chatModel.chatRunning.value == false + val userDisplayName = chatModel.currentUser.value?.displayName ?: "" + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.help_and_support)) + SectionView(stringResource(MR.strings.settings_section_title_help)) { - SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName ?: "") }, disabled = stopped) + SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName) }, disabled = stopped) SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close = close) }, disabled = stopped) SettingsActionItem(painterResource(MR.images.ic_info), stringResource(MR.strings.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) }) + } + SectionDividerSpaced() + + SectionView(stringResource(MR.strings.settings_section_title_contact)) { if (!chatModel.desktopNoUserNoRemote) { SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openVerifiedSimplexUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped) } @@ -129,27 +142,29 @@ fun SettingsLayout( } SectionDividerSpaced() - SectionView(stringResource(MR.strings.settings_section_title_support)) { + SectionView(stringResource(MR.strings.settings_section_title_support_project)) { if (!BuildConfigCommon.ANDROID_BUNDLE) { ContributeItem(uriHandler) } - RateAppItem(uriHandler) + if (appPlatform.isAndroid) { + RateAppItem(uriHandler) + } StarOnGithubItem(uriHandler) } - SectionDividerSpaced() - - SettingsSectionApp(showSettingsModal, showVersion, withAuth) SectionBottomSpacer() } } @Composable -expect fun SettingsSectionApp( +expect fun AdvancedSettingsAppSection( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - showVersion: () -> Unit, - withAuth: (title: String, desc: String, block: () -> Unit) -> Unit + withAuth: (title: String, desc: String, block: () -> Unit) -> Unit, ) +// Shutdown is only available on Android; on desktop the app is closed via the window. +@Composable +expect fun AppShutdownItem() + @Composable private fun DatabaseItem(encrypted: Boolean, saved: Boolean, openDatabaseView: () -> Unit, stopped: Boolean) { SectionItemView(openDatabaseView) { Row( @@ -160,11 +175,11 @@ expect fun SettingsSectionApp( Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) { Icon( painterResource(MR.images.ic_database), - contentDescription = stringResource(MR.strings.database_passphrase_and_export), + contentDescription = stringResource(MR.strings.chat_data), tint = if (encrypted && (appPlatform.isAndroid || !saved)) MaterialTheme.colors.secondary else WarningOrange, ) TextIconSpaced(false) - Text(stringResource(MR.strings.database_passphrase_and_export)) + Text(stringResource(MR.strings.chat_data)) } if (stopped) { Icon( @@ -208,7 +223,7 @@ fun ChatLockItem( } } -@Composable private fun ContributeItem(uriHandler: UriHandler) { +@Composable fun ContributeItem(uriHandler: UriHandler) { SectionItemView({ uriHandler.openExternalLink("https://github.com/simplex-chat/simplex-chat#contribute") }) { Icon( painterResource(MR.images.ic_keyboard), @@ -220,7 +235,7 @@ fun ChatLockItem( } } -@Composable private fun RateAppItem(uriHandler: UriHandler) { +@Composable fun RateAppItem(uriHandler: UriHandler) { SectionItemView({ runCatching { uriHandler.openUriCatching("market://details?id=chat.simplex.app") } .onFailure { uriHandler.openUriCatching("https://play.google.com/store/apps/details?id=chat.simplex.app") } @@ -236,7 +251,7 @@ fun ChatLockItem( } } -@Composable private fun StarOnGithubItem(uriHandler: UriHandler) { +@Composable fun StarOnGithubItem(uriHandler: UriHandler) { SectionItemView({ uriHandler.openExternalLink("https://github.com/simplex-chat/simplex-chat") }) { Icon( painter = painterResource(MR.images.ic_github), @@ -486,8 +501,6 @@ fun PreviewSettingsLayout() { stopped = false, encrypted = false, passphraseSaved = false, - notificationsMode = remember { mutableStateOf(NotificationsMode.OFF) }, - userDisplayName = "Alice", setPerformLA = { _ -> }, showModal = { {} }, showSettingsModal = { {} }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt index 52addd146b..5070c3c0aa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/VersionInfoView.kt @@ -1,33 +1,55 @@ package chat.simplex.common.views.usersettings +import SectionBottomSpacer +import SectionDividerSpaced +import SectionView +import itemHPadding +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.BuildConfigCommon +import chat.simplex.common.model.ChatModel import chat.simplex.common.model.CoreVersionInfo import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.platform.appPlatform -import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.platform.chatModel +import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF import chat.simplex.common.views.helpers.AppBarTitle import chat.simplex.res.MR @Composable -fun VersionInfoView(info: CoreVersionInfo) { - ColumnWithScrollBar( - Modifier.padding(horizontal = DEFAULT_PADDING), - ) { - AppBarTitle(stringResource(MR.strings.app_version_title), withPadding = false) - if (appPlatform.isAndroid) { - Text(String.format(stringResource(MR.strings.app_version_name), BuildConfigCommon.ANDROID_VERSION_NAME)) - Text(String.format(stringResource(MR.strings.app_version_code), BuildConfigCommon.ANDROID_VERSION_CODE)) - } else { - Text(String.format(stringResource(MR.strings.app_version_name), BuildConfigCommon.DESKTOP_VERSION_NAME)) - Text(String.format(stringResource(MR.strings.app_version_code), BuildConfigCommon.DESKTOP_VERSION_CODE)) +fun VersionInfoView( + showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + withAuth: (title: String, desc: String, block: () -> Unit) -> Unit, +) { + val versionInfo = remember { mutableStateOf(null) } + LaunchedEffect(Unit) { + versionInfo.value = chatModel.controller.apiGetVersion() + } + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.app_version_title)) + SectionView { + Column(Modifier.padding(horizontal = itemHPadding, vertical = DEFAULT_PADDING_HALF)) { + if (appPlatform.isAndroid) { + Text(String.format(stringResource(MR.strings.app_version_name), BuildConfigCommon.ANDROID_VERSION_NAME)) + Text(String.format(stringResource(MR.strings.app_version_code), BuildConfigCommon.ANDROID_VERSION_CODE)) + } else { + Text(String.format(stringResource(MR.strings.app_version_name), BuildConfigCommon.DESKTOP_VERSION_NAME)) + Text(String.format(stringResource(MR.strings.app_version_code), BuildConfigCommon.DESKTOP_VERSION_CODE)) + } + versionInfo.value?.let { info -> + Text(String.format(stringResource(MR.strings.core_version), info.version)) + val simplexmqCommit = if (info.simplexmqCommit.length >= 7) info.simplexmqCommit.substring(startIndex = 0, endIndex = 7) else info.simplexmqCommit + Text(String.format(stringResource(MR.strings.core_simplexmq_version), info.simplexmqVersion, simplexmqCommit)) + } + } } - Text(String.format(stringResource(MR.strings.core_version), info.version)) - val simplexmqCommit = if (info.simplexmqCommit.length >= 7) info.simplexmqCommit.substring(startIndex = 0, endIndex = 7) else info.simplexmqCommit - Text(String.format(stringResource(MR.strings.core_simplexmq_version), info.simplexmqVersion, simplexmqCommit)) + SectionDividerSpaced() + + AdvancedSettingsAppSection(showSettingsModal, withAuth) + SectionBottomSpacer() } } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index ee79fc0af0..5bca9402a8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -1555,6 +1555,13 @@ Files Send delivery receipts to Contact requests from groups + About + Contact + Support the project + Chat data + Help & support + More privacy + Advanced settings Restart Shutdown Developer tools @@ -2681,7 +2688,7 @@ Don\'t enable You can enable later via Settings Delivery receipts are disabled! - You can enable them later via app Privacy & Security settings. + You can enable them later via app Your privacy settings. Error enabling delivery receipts! diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt index 5b4a044df3..174ad63c7a 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt @@ -1,27 +1,21 @@ package chat.simplex.common.views.usersettings import SectionView -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier +import androidx.compose.runtime.* import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel -import chat.simplex.common.platform.AppUpdatesChannel -import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF +import chat.simplex.common.platform.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @Composable -actual fun SettingsSectionApp( +actual fun AdvancedSettingsAppSection( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - showVersion: () -> Unit, - withAuth: (title: String, desc: String, block: () -> Unit) -> Unit + withAuth: (title: String, desc: String, block: () -> Unit) -> Unit, ) { - SectionView(stringResource(MR.strings.settings_section_title_app)) { + SectionView { SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(withAuth) }) val selectedChannel = remember { appPrefs.appUpdateChannel.state } val values = AppUpdatesChannel.entries.map { it to it.text } @@ -29,6 +23,8 @@ actual fun SettingsSectionApp( appPrefs.appUpdateChannel.set(it) setupUpdateChecker() } - AppVersionItem(showVersion) } } + +@Composable +actual fun AppShutdownItem() {} diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index 3d57fb2b43..53dedf1a91 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -204,7 +204,7 @@ linkCheckThread_ opts env@ServiceState {eventQ} threadDelay $ linkCheckInterval opts * 1000000 u <- readTVarIO $ currentUser cc forM_ u $ \user -> - withDB' "linkCheckThread" cc (\db -> getAllGroupRegs_ db user) >>= \case + withDB' "linkCheckThread" cc (\db -> getAllGroupRegs_ db (storeCxt cc) user) >>= \case Left e -> logError $ "linkCheckThread error: " <> T.pack e Right grs -> forM_ grs $ \(gInfo, gr) -> unless (groupRemoved $ groupRegStatus gr) $ @@ -462,7 +462,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName getOwnerGroupMember :: GroupId -> GroupReg -> IO (Either String GroupMember) getOwnerGroupMember gId GroupReg {dbOwnerMemberId} = case dbOwnerMemberId of - Just mId -> withDB "getGroupMember" cc $ \db -> withExceptT show $ getGroupMember db (vr cc) user gId mId + Just mId -> withDB "getGroupMember" cc $ \db -> withExceptT show $ getGroupMember db (storeCxt cc) user gId mId Nothing -> pure $ Left "no owner member in group registration" deServiceJoinedGroup :: ContactId -> GroupInfo -> GroupMember -> IO () @@ -556,7 +556,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName Right (CRConnectionPlan _ _ (CPGroupLink (GLPKnown {groupInfo = g'}))) -> case dbOwnerMemberId gr of Just ownerGMId -> - withDB "getGroupMember" cc (\db -> withExceptT show $ getGroupMember db (vr cc) user groupId ownerGMId) >>= \case + withDB "getGroupMember" cc (\db -> withExceptT show $ getGroupMember db (storeCxt cc) user groupId ownerGMId) >>= \case Right ownerMember | let GroupMember {memberRole = role} = ownerMember, role >= GROwner -> setGroupStatus notifyAdminUsers st env cc groupId (GRSPendingApproval n') (`updatedNotification` g') @@ -813,7 +813,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName _ -> False checkValidOwner dbOwnerMemberId owners onValid = case dbOwnerMemberId of Just ownerGMId -> - withDB "checkGroupLink" cc (\db -> withExceptT show $ getGroupMember db (vr cc) user groupId ownerGMId) >>= \case + withDB "checkGroupLink" cc (\db -> withExceptT show $ getGroupMember db (storeCxt cc) user groupId ownerGMId) >>= \case Right GroupMember {memberId, memberPubKey} | any (\GroupLinkOwner {memberId = mId, memberKey} -> memberId == mId && memberPubKey == Just memberKey) owners -> onValid _ -> setGroupStatus logError st env cc groupId GRSSuspendedBadRoles $ \gr' -> @@ -985,7 +985,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName addGroupReg notifyAdminUsers st cc ct gInfo GRSProposed $ \_ -> pure () sendChatCmd cc (APIConnectPreparedGroup gId False (Just ownerContact) Nothing) >>= \case Right CRStartedConnectionToGroup {groupInfo = gInfo'} -> - withDB "getGroupMember" cc (\db -> withExceptT show $ getGroupMemberByMemberId db (vr cc) user gInfo' mId) >>= \case + withDB "getGroupMember" cc (\db -> withExceptT show $ getGroupMemberByMemberId db (storeCxt cc) user gInfo' mId) >>= \case Right ownerMember -> void $ setGroupRegOwner cc gId ownerMember Left e -> do @@ -998,7 +998,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName deReregistration ct g@GroupInfo {groupId, groupProfile = GroupProfile {publicGroup = pg_}} profileChanged LinkOwnerSig {ownerId = Just (B64UrlByteString oIdBytes)} = do let mId = MemberId oIdBytes gt = maybe "group" groupTypeStr' pg_ - withDB "getGroupMemberByMemberId" cc (\db -> withExceptT show $ getGroupMemberByMemberId db (vr cc) user g mId) >>= \case + withDB "getGroupMemberByMemberId" cc (\db -> withExceptT show $ getGroupMemberByMemberId db (storeCxt cc) user g mId) >>= \case Right ownerMember@GroupMember {memberRole = role, memberStatus} -> if | role >= GROwner && memberStatus /= GSMemUnknown -> @@ -1451,7 +1451,7 @@ directoryServiceEvent st opts@DirectoryOpts {adminUsers, superUsers, serviceName getOwnersInfo :: [(GroupInfo, GroupReg)] -> IO [((GroupInfo, GroupReg), Maybe (Either String Contact))] getOwnersInfo gs = fmap (either (\e -> map (,Just (Left e)) gs) id) $ withDB' "getOwnersInfo" cc $ \db -> - mapM (\g@(_, gr) -> fmap ((g,) . Just . first show) $ runExceptT $ getContact db (vr cc) user $ dbContactId gr) gs + mapM (\g@(_, gr) -> fmap ((g,) . Just . first show) $ runExceptT $ getContact db (storeCxt cc) user $ dbContactId gr) gs sendGroupsInfo :: Contact -> ChatItemId -> Bool -> ([(GroupInfo, GroupReg)], Int) -> IO () sendGroupsInfo ct ciId isAdmin (gs, n) = do @@ -1519,7 +1519,7 @@ updateGroupListingFiles cc u dir = Left e -> logError $ "generateListing error: failed to read groups: " <> T.pack e getContact' :: ChatController -> User -> ContactId -> IO (Either String Contact) -getContact' cc user ctId = withDB "getContact" cc $ \db -> withExceptT show $ getContact db (vr cc) user ctId +getContact' cc user ctId = withDB "getContact" cc $ \db -> withExceptT show $ getContact db (storeCxt cc) user ctId getGroupLink' :: ChatController -> User -> GroupInfo -> IO (Either String GroupLink) getGroupLink' cc user gInfo = diff --git a/apps/simplex-directory-service/src/Directory/Store.hs b/apps/simplex-directory-service/src/Directory/Store.hs index b5f7220724..4036bd8cf1 100644 --- a/apps/simplex-directory-service/src/Directory/Store.hs +++ b/apps/simplex-directory-service/src/Directory/Store.hs @@ -85,7 +85,6 @@ import Data.Time.Clock.System (systemEpochDay) import Directory.Search import Directory.Util import Simplex.Chat.Controller -import Simplex.Chat.Protocol (supportedChatVRange) import Simplex.Chat.Options.DB (FromField (..), ToField (..)) import Simplex.Chat.Store import Simplex.Chat.Store.Groups @@ -315,28 +314,28 @@ getGroupReg_ db gId = getGroupAndReg :: ChatController -> User -> GroupId -> IO (Either String (GroupInfo, GroupReg)) getGroupAndReg cc user@User {userId, userContactId} gId = withDB "getGroupAndReg" cc $ \db -> - ExceptT $ firstRow (toGroupInfoReg (vr cc) user) ("group " ++ show gId ++ " not found") $ + ExceptT $ firstRow (toGroupInfoReg (storeCxt cc) user) ("group " ++ show gId ++ " not found") $ DB.query db (groupReqQuery <> " AND g.group_id = ?") (userId, userContactId, gId) getUserGroupReg :: ChatController -> User -> ContactId -> UserGroupRegId -> IO (Either String (GroupInfo, GroupReg)) getUserGroupReg cc user@User {userId, userContactId} ctId ugrId = withDB "getUserGroupReg" cc $ \db -> - ExceptT $ firstRow (toGroupInfoReg (vr cc) user) ("group " ++ show ugrId ++ " not found") $ + ExceptT $ firstRow (toGroupInfoReg (storeCxt cc) user) ("group " ++ show ugrId ++ " not found") $ DB.query db (groupReqQuery <> " AND r.contact_id = ? AND r.user_group_reg_id = ?") (userId, userContactId, ctId, ugrId) getUserGroupRegs :: ChatController -> User -> ContactId -> IO (Either String [(GroupInfo, GroupReg)]) getUserGroupRegs cc user@User {userId, userContactId} ctId = withDB' "getUserGroupRegs" cc $ \db -> - map (toGroupInfoReg (vr cc) user) + map (toGroupInfoReg (storeCxt cc) user) <$> DB.query db (groupReqQuery <> " AND r.contact_id = ? ORDER BY r.user_group_reg_id") (userId, userContactId, ctId) getAllListedGroups :: ChatController -> User -> IO (Either String [(GroupInfo, GroupReg, Maybe GroupLink)]) -getAllListedGroups cc user = withDB' "getAllListedGroups" cc $ \db -> getAllListedGroups_ db (vr cc) user +getAllListedGroups cc user = withDB' "getAllListedGroups" cc $ \db -> getAllListedGroups_ db (storeCxt cc) user -getAllListedGroups_ :: DB.Connection -> VersionRangeChat -> User -> IO [(GroupInfo, GroupReg, Maybe GroupLink)] -getAllListedGroups_ db vr' user@User {userId, userContactId} = +getAllListedGroups_ :: DB.Connection -> StoreCxt -> User -> IO [(GroupInfo, GroupReg, Maybe GroupLink)] +getAllListedGroups_ db cxt user@User {userId, userContactId} = DB.query db (groupReqQuery <> " AND r.group_reg_status = ?") (userId, userContactId, GRSActive) - >>= mapM (withGroupLink . toGroupInfoReg vr' user) + >>= mapM (withGroupLink . toGroupInfoReg cxt user) where withGroupLink (g, gr) = (g,gr,) . eitherToMaybe <$> runExceptT (getGroupLink db user g) @@ -382,7 +381,7 @@ searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pa countQuery' = countQuery <> " JOIN group_profiles gp ON gp.group_profile_id = g.group_profile_id WHERE r.group_reg_status = ? " orderBy = " ORDER BY g.summary_current_members_count DESC, r.group_reg_id ASC " where - groups = (map (toGroupInfoReg (vr cc) user) <$>) + groups = (map (toGroupInfoReg (storeCxt cc) user) <$>) count = maybeFirstRow' 0 fromOnly listedGroupQuery = groupReqQuery <> " AND r.group_reg_status = ? " countQuery = "SELECT COUNT(1) FROM groups g JOIN sx_directory_group_regs r ON g.group_id = r.group_id " @@ -395,22 +394,22 @@ searchListedGroups cc user@User {userId, userContactId} searchType lastGroup_ pa ) |] -getAllGroupRegs_ :: DB.Connection -> User -> IO [(GroupInfo, GroupReg)] -getAllGroupRegs_ db user@User {userId, userContactId} = - map (toGroupInfoReg supportedChatVRange user) +getAllGroupRegs_ :: DB.Connection -> StoreCxt -> User -> IO [(GroupInfo, GroupReg)] +getAllGroupRegs_ db cxt user@User {userId, userContactId} = + map (toGroupInfoReg cxt user) <$> DB.query db groupReqQuery (userId, userContactId) getDuplicateGroupRegs :: ChatController -> User -> Text -> IO (Either String [(GroupInfo, GroupReg)]) getDuplicateGroupRegs cc user@User {userId, userContactId} displayName = withDB' "getDuplicateGroupRegs" cc $ \db -> - map (toGroupInfoReg (vr cc) user) + map (toGroupInfoReg (storeCxt cc) user) <$> DB.query db (groupReqQuery <> " AND gp.display_name = ?") (userId, userContactId, displayName) listLastGroups :: ChatController -> User -> Int -> IO (Either String ([(GroupInfo, GroupReg)], Int)) listLastGroups cc user@User {userId, userContactId} count = withDB' "getUserGroupRegs" cc $ \db -> do gs <- - map (toGroupInfoReg (vr cc) user) + map (toGroupInfoReg (storeCxt cc) user) <$> DB.query db (groupReqQuery <> " ORDER BY group_reg_id DESC LIMIT ?") (userId, userContactId, count) n <- maybeFirstRow' 0 fromOnly $ DB.query_ db "SELECT COUNT(1) FROM sx_directory_group_regs" pure (gs, n) @@ -419,14 +418,14 @@ listPendingGroups :: ChatController -> User -> Int -> IO (Either String ([(Group listPendingGroups cc user@User {userId, userContactId} count = withDB' "getUserGroupRegs" cc $ \db -> do gs <- - map (toGroupInfoReg (vr cc) user) + map (toGroupInfoReg (storeCxt cc) user) <$> DB.query db (groupReqQuery <> " AND r.group_reg_status LIKE 'pending_approval%' ORDER BY group_reg_id DESC LIMIT ?") (userId, userContactId, count) n <- maybeFirstRow' 0 fromOnly $ DB.query_ db "SELECT COUNT(1) FROM sx_directory_group_regs WHERE group_reg_status LIKE 'pending_approval%'" pure (gs, n) -toGroupInfoReg :: VersionRangeChat -> User -> (GroupInfoRow :. GroupRegRow) -> (GroupInfo, GroupReg) -toGroupInfoReg vr' User {userContactId} (groupRow :. grRow) = - (toGroupInfo vr' userContactId [] groupRow, rowToGroupReg grRow) +toGroupInfoReg :: StoreCxt -> User -> (GroupInfoRow :. GroupRegRow) -> (GroupInfo, GroupReg) +toGroupInfoReg cxt User {userContactId} (groupRow :. grRow) = + (toGroupInfo cxt userContactId [] groupRow, rowToGroupReg grRow) type GroupRegRow = (GroupId, UserGroupRegId, ContactId, Maybe GroupMemberId, GroupRegStatus, BoolInt, UTCTime) diff --git a/apps/simplex-directory-service/src/Directory/Store/Migrate.hs b/apps/simplex-directory-service/src/Directory/Store/Migrate.hs index aa101d7bf7..d501fbd5c3 100644 --- a/apps/simplex-directory-service/src/Directory/Store/Migrate.hs +++ b/apps/simplex-directory-service/src/Directory/Store/Migrate.hs @@ -18,10 +18,9 @@ import Directory.Listing import Directory.Options import Directory.Store import Simplex.Chat (createChatDatabase) -import Simplex.Chat.Controller (ChatConfig (..), ChatDatabase (..)) +import Simplex.Chat.Controller (ChatConfig (..), ChatDatabase (..), mkStoreCxt) import Simplex.Chat.Options (CoreChatOpts (..)) import Simplex.Chat.Options.DB -import Simplex.Chat.Protocol (supportedChatVRange) import Simplex.Chat.Store.Groups (getHostMember) import Simplex.Chat.Store.Profiles (getUsers) import Simplex.Chat.Store.Shared (getGroupInfo) @@ -62,7 +61,7 @@ checkDirectoryLog opts cfg = runDirectoryMigrations opts cfg st gs <- readDirectoryLogData logFile withActiveUser st $ \user -> withTransaction st $ \db -> do - mapM_ (verifyGroupRegistration db user) gs + mapM_ (verifyGroupRegistration (mkStoreCxt cfg) db user) gs putStrLn $ show (length gs) <> " group registrations OK" importDirectoryLogToDB :: DirectoryOpts -> ChatConfig -> IO () @@ -73,7 +72,7 @@ importDirectoryLogToDB opts cfg = do ctRegs <- TM.emptyIO withActiveUser st $ \user -> withTransaction st $ \db -> do forM_ gs $ \gr -> - whenM (verifyGroupRegistration db user gr) $ do + whenM (verifyGroupRegistration (mkStoreCxt cfg) db user gr) $ do putStrLn $ "importing group " <> show (dbGroupId gr) insertGroupReg db =<< fixUserGroupRegId ctRegs gr renamePath logFile (logFile ++ ".bak") @@ -101,28 +100,28 @@ exportDBToDirectoryLog opts cfg = runDirectoryMigrations opts cfg st withActiveUser st $ \user -> do gs <- withFile logFile WriteMode $ \h -> withTransaction st $ \db -> do - gs <- getAllGroupRegs_ db user + gs <- getAllGroupRegs_ db (mkStoreCxt cfg) user forM_ gs $ \(_, gr) -> - whenM (verifyGroupRegistration db user gr) $ + whenM (verifyGroupRegistration (mkStoreCxt cfg) db user gr) $ B.hPutStrLn h $ strEncode $ GRCreate gr pure gs putStrLn $ show (length gs) <> " group registrations exported" saveGroupListingFiles :: DirectoryOpts -> ChatConfig -> IO () -saveGroupListingFiles opts _cfg = case webFolder opts of +saveGroupListingFiles opts cfg = case webFolder opts of Nothing -> exit "use --web-folder to generate listings" Just dir -> withChatStore opts $ \st -> withActiveUser st $ \user -> withTransaction st $ \db -> - getAllListedGroups_ db supportedChatVRange user >>= generateListing dir + getAllListedGroups_ db (mkStoreCxt cfg) user >>= generateListing dir -verifyGroupRegistration :: DB.Connection -> User -> GroupReg -> IO Bool -verifyGroupRegistration db user GroupReg {dbGroupId = gId, dbContactId = ctId, dbOwnerMemberId, groupRegStatus} = - runExceptT (getGroupInfo db supportedChatVRange user gId) >>= \case +verifyGroupRegistration :: StoreCxt -> DB.Connection -> User -> GroupReg -> IO Bool +verifyGroupRegistration cxt db user GroupReg {dbGroupId = gId, dbContactId = ctId, dbOwnerMemberId, groupRegStatus} = + runExceptT (getGroupInfo db cxt user gId) >>= \case Left e -> False <$ putStrLn ("Error: loading group " <> show gId <> " (skipping): " <> show e) Right GroupInfo {localDisplayName} -> do let groupRef = show gId <> " " <> T.unpack localDisplayName - runExceptT (getHostMember db supportedChatVRange user gId) >>= \case + runExceptT (getHostMember db cxt user gId) >>= \case Left e -> False <$ putStrLn ("Error: loading host member of group " <> groupRef <> " (skipping): " <> show e) Right GroupMember {groupMemberId = mId', memberContactId = ctId'} -> case dbOwnerMemberId of Nothing -> True <$ putStrLn ("Warning: group " <> groupRef <> " has no owner member ID, host member ID is " <> show mId' <> ", registration status: " <> B.unpack (strEncode groupRegStatus)) diff --git a/apps/simplex-directory-service/src/Directory/Util.hs b/apps/simplex-directory-service/src/Directory/Util.hs index a4b79a1bef..52d376a945 100644 --- a/apps/simplex-directory-service/src/Directory/Util.hs +++ b/apps/simplex-directory-service/src/Directory/Util.hs @@ -15,9 +15,9 @@ import Simplex.Messaging.Agent.Store.Common (withTransaction) import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Util (catchAll) -vr :: ChatController -> VersionRangeChat -vr ChatController {config = ChatConfig {chatVRange}} = chatVRange -{-# INLINE vr #-} +storeCxt :: ChatController -> StoreCxt +storeCxt ChatController {config} = mkStoreCxt config +{-# INLINE storeCxt #-} withDB' :: Text -> ChatController -> (DB.Connection -> IO a) -> IO (Either String a) withDB' cxt cc a = withDB cxt cc $ ExceptT . fmap Right . a diff --git a/packages/simplex-chat-nodejs/src/download-libs.js b/packages/simplex-chat-nodejs/src/download-libs.js index db042d48a2..72761a1ac5 100644 --- a/packages/simplex-chat-nodejs/src/download-libs.js +++ b/packages/simplex-chat-nodejs/src/download-libs.js @@ -4,7 +4,7 @@ const path = require('path'); const extract = require('extract-zip'); const GITHUB_REPO = 'simplex-chat/simplex-chat-libs'; -const RELEASE_TAG = 'v6.5.2'; +const RELEASE_TAG = 'v6.5.4'; const BACKEND = (process.env.SIMPLEX_BACKEND || process.env.npm_config_simplex_backend || 'sqlite').toLowerCase(); if (BACKEND !== 'sqlite' && BACKEND !== 'postgres') { diff --git a/plans/2026-05-20-fix-hold-on-long-msg-android.md b/plans/2026-05-20-fix-hold-on-long-msg-android.md new file mode 100644 index 0000000000..f89aa26582 --- /dev/null +++ b/plans/2026-05-20-fix-hold-on-long-msg-android.md @@ -0,0 +1,68 @@ +# Fix chat item long-press menu and ripple shape + +Branch: `nd/fix-hold-on-long-msg-android` · PR [#6997](https://github.com/simplex-chat/simplex-chat/pull/6997) · issue [#6991](https://github.com/simplex-chat/simplex-chat/issues/6991). + +## 1. Problem statement + +Two issues with the chat-item bubble on the multiplatform UI: + +- **Android (#6991):** long-pressing the lower part of a very tall text message did not open the select/copy/reply context menu. Long-press on the top/middle worked. Reproduced with a long multi-line message (~150+ lines — e.g. 5000 random bytes as hex); never reproduced on short messages. Occurs **only with the message tail enabled** (bubble shape); with the tail preference disabled, messages use a plain rounded-rectangle shape and the bug does not reproduce. iOS unaffected. +- **Desktop:** the chat-item press ripple, in some cases, rendered as a rectangle instead of following the rounded bubble shape. + +## 2. Solution summary + +One function — `Modifier.clipChatItem` in `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt`. It clipped the chat item with `Modifier.clip(shape)` for every shape style. It now clips the **bubble** (`GenericShape`) in the draw pass with `drawWithCache` + `clipPath`, and keeps `Modifier.clip` for the **`RoundRect`** shape, which is unaffected by the bug (§3). + +```kotlin +return when (style) { + is ShapeStyle.Bubble -> { + val shape = chatItemShape(cornerRoundness, LocalDensity.current, style.tailVisible, chatItem?.chatDir?.sent == true) + this.drawWithCache { + val path = Path().apply { addOutline(shape.createOutline(size, layoutDirection, this@drawWithCache)) } + onDrawWithContent { clipPath(path) { this@onDrawWithContent.drawContent() } } + } + } + is ShapeStyle.RoundRect -> this.clip(RoundedCornerShape(style.radius * cornerRoundness)) +} +``` + +Net diff: 1 file (`ChatItemView.kt`), +20 / −5 — the `clipChatItem` function restructured plus two imports. + +## 3. Root cause + +`Modifier.clip(shape)` is defined in Compose as `graphicsLayer(shape = shape, clip = true)`. A clipping graphics layer restricts **both** drawing **and** pointer hit-test to the shape. + +`clipChatItem` is the first (outermost) modifier on the chat-bubble `Column`, and that same `Column` carries the `combinedClickable` long-press handler. So the layer's hit-test region gates every press on the bubble. + +- **Android:** for a very tall chat item the layer's hit-test region does not cover the bubble's lower portion — a press there is never delivered to `combinedClickable`, so the long-press menu does not open. This is specific to the bubble's `GenericShape` clip: with the tail disabled the item is clipped with a `RoundedCornerShape`, which hit-tests correctly. The exact reason the `GenericShape` clip's hit-test falls short on tall content was not isolated; the fix does not depend on it (see §4). +- **Desktop:** the layer's clip did not always extend to the `combinedClickable` press ripple, so the ripple drew to its own rectangular bounds instead of the bubble shape. + +## 4. The fix + +For the bubble shape, `clipChatItem` clips with a draw modifier instead of a graphics layer. `drawWithCache` builds the shape's `Path` once per size change; `onDrawWithContent { clipPath(path) { drawContent() } }` wraps the whole content draw — bubble background, text, and the press ripple — in a canvas clip. + +A draw modifier affects **only drawing**. It is not a layout or pointer-input node and has no effect on hit-test. Therefore: + +- the bubble and ripple are still clipped to the shape — visually identical to `Modifier.clip`; +- pointer hit-test is no longer clipped — `combinedClickable` receives presses anywhere in the `Column`'s bounds, fixing the Android long-press; +- the canvas `clipPath` clips the ripple reliably, fixing the rectangular desktop ripple. + +The `RoundRect` shape keeps `Modifier.clip`: it hit-tests correctly (no bug) and keeps its antialiased outline clip. Scoping by shape — rather than draw-clipping every shape — leaves every non-bubble chat item (service/event messages, tails-off messages, old Android) byte-for-byte unchanged. + +## 5. Alternatives rejected + +- **Remove `clipChatItem` from the bubble `Column`.** Fixes the Android long-press, but the press ripple loses its shape and renders as a rectangle. Intermediate state during development; replaced. +- **Draw-pass clip for every shape, unconditionally.** Also correct and a hair simpler (no `when`), but it needlessly moves the `RoundRect` shape off `Modifier.clip`'s antialiased outline clip onto a canvas `clipPath` — a behaviour change with no benefit, since `RoundRect` has no bug. Scoping to the bubble shape keeps `RoundRect` unchanged. +- **Keep `Modifier.clip`, move `combinedClickable` off the clipped `Column`.** A larger structural change to the chat-item layout tree; the draw-pass clip fixes both issues without moving anything. + +## 6. Verification + +- **Android** (debug APK): long-press on the lower half of a 150+-line message opens the context menu; top/middle still work; the tap ripple stays bubble-shaped; swipe-to-reply and link tap/long-press are unaffected. +- **Desktop** (Linux AppImage): the chat-item press ripple follows the bubble shape (rounded corners and tail), not a rectangle — confirmed against a build without the fix. +- The bubble draw-pass clip above was verified on those Android and desktop builds; this revision additionally keeps `Modifier.clip` for the `RoundRect` shape, which is the unchanged pre-fix behaviour. + +## 7. Risk and rollback + +- Blast radius: the `Bubble` branch of `clipChatItem`. The `RoundRect` branch is unchanged (`Modifier.clip` as before), so service/event items, tails-off messages and old-Android items are untouched. For the bubble, drawing is clipped identically; the single behavioural change is that pointer hit-test on the bubble is no longer shape-clipped — benign (bubble corners are transparent; a rectangular hit area is a marginally larger touch target). +- iOS is a separate codebase and is untouched. +- Rollback: revert the fix commit on the branch, or drop it before merge. diff --git a/plans/2026-05-26-fix-video-drag-and-drop.md b/plans/2026-05-26-fix-video-drag-and-drop.md new file mode 100644 index 0000000000..16258dea7f --- /dev/null +++ b/plans/2026-05-26-fix-video-drag-and-drop.md @@ -0,0 +1,44 @@ +# Fix desktop drag-and-drop of videos attached as files + +Branch: `nd/fix-video-drag-and-drop` · base: `master`. + +## Problem + +On desktop, dragging a video file into a chat attaches it as a generic file (paperclip + filename) instead of as a video (thumbnail + duration). Dragging an image works. Picking the same video via "Gallery → Video" attaches it correctly — so only the drag-and-drop routing is wrong. + +## Fix + +One file: `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt`. Recognise videos as media in `onFilesAttached`'s classifier. + +```diff + fun MutableState.onFilesAttached(uris: List) { +- val groups = uris.groupBy { isImage(it) } +- val images = groups[true] ?: emptyList() ++ val groups = uris.groupBy { isImage(it) || isVideoUri(it) } ++ val media = groups[true] ?: emptyList() + val files = groups[false] ?: emptyList() +- if (images.isNotEmpty()) { +- CoroutineScope(Dispatchers.IO).launch { processPickedMedia(images, null) } ++ if (media.isNotEmpty()) { ++ CoroutineScope(Dispatchers.IO).launch { processPickedMedia(media, null) } + } else if (files.isNotEmpty()) { + processPickedFile(uris.first(), null) + } + } ++ ++private fun isVideoUri(uri: URI): Boolean { ++ val name = getFileName(uri)?.lowercase() ?: return false ++ return name.endsWith(".mov") || name.endsWith(".avi") || name.endsWith(".mp4") || ++ name.endsWith(".mpg") || name.endsWith(".mpeg") || name.endsWith(".mkv") ++} +``` + +Total diff: 1 file, +11 / −5. + +## Cause + +`onFilesAttached` classified URIs by `isImage` only — non-images (including videos) fell through to `processPickedFile`, producing a `FilePreview`. The downstream `processPickedMedia` already handles video correctly (its `else` branch builds `UploadContent.Video`); the classifier above it just never reached that branch. The existing `isVideo` in `Videos.desktop.kt` is `desktopMain`-only and not visible from `ComposeView.kt` in `commonMain` — the structural gap that left the classifier video-blind. The inline `isVideoUri` uses the cross-platform `getFileName`, so the same fix also corrects the paste path (`onFilesPasted` at `ComposeView.kt:1378`). + +## Risk + +One file, no interface change. Image and non-media drops are bit-identical. Video extension list is now duplicated with `Videos.desktop.kt`; adding a new format means updating both — accepted as the cost of a single-file fix. iOS unaffected. Rollback: revert the commit. diff --git a/plans/delete-leave-dialog-with-profile-impl.md b/plans/delete-leave-dialog-with-profile-impl.md new file mode 100644 index 0000000000..860d555d36 --- /dev/null +++ b/plans/delete-leave-dialog-with-profile-impl.md @@ -0,0 +1,323 @@ +# Implementation plan — chat name on its own line in delete/leave/clear dialogs + +Follows the product spec in +[`delete-leave-dialog-with-profile.md`](./delete-leave-dialog-with-profile.md). + +Pure code change — zero string additions, zero new helpers, zero +signature changes. Each call site edits one argument: the `text =` / +`message:` value gains `"${displayName}\n\n"` prepended to the +existing localized warning (or, where there is no current body, the +chat name becomes the new body). + +One commit per platform. + +## Commit 1 — Kotlin + +**Files touched:** +- `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt` + — adds `parseHtml: Boolean = true` to `showAlertDialog` and + `showAlertDialogButtonsColumn`. When `false`, the body text is wrapped + as `AnnotatedString` and routed through the existing AnnotatedString + `AlertContent` overload, which does NOT call + `escapedHtmlToAnnotatedString`. Default stays `true` so existing + callers are unaffected. +- `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt` +- `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt` +- `apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt` + — adds the previously-missed `deleteContactConnectionAlert` + dispatcher to the coverage (pending contact connections). + +Every Kotlin call site that prepends the chat name sets +`parseHtml = false`, so `displayName` is never HTML-interpreted. + +### 1.1 — `deleteGroupDialog` (`GroupChatInfoView.kt:182`) + +```diff + fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) { + val chatInfo = chat.chatInfo + val titleId = /* unchanged */ + val messageId = /* unchanged */ + AlertManager.shared.showAlertDialog( + title = generalGetString(titleId), +- text = generalGetString(messageId), ++ text = "${groupInfo.displayName}\n\n${generalGetString(messageId)}", + confirmText = generalGetString(MR.strings.delete_verb), + onConfirm = { /* unchanged */ }, + destructive = true, + ) + } +``` + +### 1.2 — `leaveGroupDialog` (`GroupChatInfoView.kt:222`) + +```diff + fun leaveGroupDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, close: (() -> Unit)? = null) { + val titleId = /* unchanged */ + val messageId = /* unchanged */ + AlertManager.shared.showAlertDialog( + title = generalGetString(titleId), +- text = generalGetString(messageId), ++ text = "${groupInfo.displayName}\n\n${generalGetString(messageId)}", + confirmText = generalGetString(MR.strings.leave_group_button), + onConfirm = { /* unchanged */ }, + destructive = true, + ) + } +``` + +Signature unchanged. No caller updates. `groupInfo.displayName` is +already available on the existing parameter (`ChatModel.kt:2142`). + +### 1.3 — `clearChatDialog` (`ChatInfoView.kt:492`) + +```diff + fun clearChatDialog(chat: Chat, close: (() -> Unit)? = null) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.clear_chat_question), +- text = generalGetString(MR.strings.clear_chat_warning), ++ text = "${chat.chatInfo.displayName}\n\n${generalGetString(MR.strings.clear_chat_warning)}", + confirmText = generalGetString(MR.strings.clear_verb), + onConfirm = { controller.clearChat(chat, close) }, + destructive = true, + ) + } +``` + +### 1.4 — Contact-delete dispatchers (`ChatInfoView.kt`) + +Four functions. `deleteContactOrConversationDialog` (line 248) has +no existing `text =`, so the chat name becomes the new body. The +other three already have a `text =`, so the name is prepended. + +All four already have `contact: Contact` as a parameter, so +`contact.displayName` is used directly (same value as +`chat.chatInfo.displayName` for a direct chat, shorter expression). + +```diff + // deleteContactOrConversationDialog — line 248 + private fun deleteContactOrConversationDialog(chat: Chat, contact: Contact, chatModel: ChatModel, close: (() -> Unit)?) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.delete_contact_question), ++ text = contact.displayName, + buttons = { /* unchanged */ } + ) + } +``` + +```diff + // deleteActiveContactDialog — line 304 + private fun deleteActiveContactDialog(chat: Chat, contact: Contact, chatModel: ChatModel, close: (() -> Unit)? = null) { + val contactDeleteMode = mutableStateOf(ContactDeleteMode.Full()) + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.delete_contact_question), +- text = generalGetString(MR.strings.delete_contact_cannot_undo_warning), ++ text = "${contact.displayName}\n\n${generalGetString(MR.strings.delete_contact_cannot_undo_warning)}", + buttons = { /* unchanged */ } + ) + } +``` + +Same diff for `deleteContactWithoutConversation` (line 361) and +`deleteNotReadyContact` (line 417) — both use +`delete_contact_cannot_undo_warning`. Neither takes `contact` as +a parameter, so the name is read via `chat.chatInfo.displayName` +(which resolves to `contact.displayName` because these dispatchers +are only reached for `ChatInfo.Direct` chats). Their titles +(`confirm_delete_contact_question`) stay unchanged — the +not-ready / no-conversation paths keep their distinct title. + +## Commit 2 — iOS + +**Files touched:** +- `apps/ios/Shared/Views/ChatList/ChatListNavLink.swift` +- `apps/ios/Shared/Views/Chat/ChatInfoView.swift` +- `apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift` + +### 2.1 — `deleteGroupAlert` (two locations) + +`Views/Chat/Group/GroupChatInfoView.swift:835` and +`Views/ChatList/ChatListNavLink.swift:567` get the same diff. +`deleteGroupAlertMessage(_:)` already returns a `Text` containing +the localized warning — concatenate to it. + +```diff + private func deleteGroupAlert() -> Alert { + let label: LocalizedStringKey = /* unchanged */ + return Alert( + title: Text(label), +- message: deleteGroupAlertMessage(groupInfo), ++ message: Text(chat.chatInfo.displayName) + Text(verbatim: "\n\n") + deleteGroupAlertMessage(groupInfo), + primaryButton: .destructive(Text("Delete")) { /* unchanged */ }, + secondaryButton: .cancel() + ) + } +``` + +`Text(chat.chatInfo.displayName)` resolves to `Text(_ content: some StringProtocol)` +(the runtime-string overload — no localization lookup, matches +codebase convention: `ChatView.swift:984`, `ChatInfoToolbar.swift:49`, +`SettingsView.swift:540`). `Text(verbatim: "\n\n")` is the literal +separator, matching the codebase convention that reserves +`verbatim:` for fixed punctuation (`ContextItemView.swift:88` is +the textbook example: `Text(chatLink.displayName) + Text(verbatim: " - ")`). +The third term `Text(messageLabel)` keeps the existing +`LocalizedStringKey` lookup. + +### 2.2 — `leaveGroupAlert` (two locations) + +`Views/Chat/Group/GroupChatInfoView.swift:872` and +`Views/ChatList/ChatListNavLink.swift:622`: + +```diff + private func leaveGroupAlert() -> Alert { + let titleLabel: LocalizedStringKey = /* unchanged */ + let messageLabel: LocalizedStringKey = /* unchanged */ + return Alert( + title: Text(titleLabel), +- message: Text(messageLabel), ++ message: Text(chat.chatInfo.displayName) + Text(verbatim: "\n\n") + Text(messageLabel), + primaryButton: .destructive(Text("Leave")) { /* unchanged */ }, + secondaryButton: .cancel() + ) + } +``` + +### 2.3 — `clearChatAlert` (three locations) + +`Views/Chat/ChatInfoView.swift:577`, +`Views/Chat/Group/GroupChatInfoView.swift:858`, +`Views/ChatList/ChatListNavLink.swift:600`: + +```diff + private func clearChatAlert() -> Alert { + Alert( + title: Text("Clear conversation?"), +- message: Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."), ++ message: Text(chat.chatInfo.displayName) + Text(verbatim: "\n\n") + Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."), + primaryButton: .destructive(Text("Clear")) { /* unchanged */ }, + secondaryButton: .cancel() + ) + } +``` + +### 2.4 — Contact-delete action sheets + +Three functions in `Views/Chat/ChatInfoView.swift`. None currently +pass `message:` to `ActionSheet`; we add it. `ActionSheet`'s +`message:` is an optional second parameter that SwiftUI already +supports. + +All three functions have `contact: Contact` in scope. Use bare +`Text(contact.displayName)` (resolves to the `StringProtocol` +overload, no localization lookup, matches codebase convention). +Add only the name as `message:` — these ActionSheets had no +message before, so adding any additional warning would be new +behavior beyond the stated goal. + +**`deleteContactOrConversationDialog`** (line 1177): + +```diff + private func deleteContactOrConversationDialog( + _ chat: Chat, _ contact: Contact, _ dismissToChatList: Bool, + _ showAlert: @escaping (SomeAlert) -> Void, + _ showActionSheet: @escaping (SomeActionSheet) -> Void, + _ showSheetContent: @escaping (SomeSheet) -> Void + ) { + showActionSheet(SomeActionSheet( + actionSheet: ActionSheet( + title: Text("Delete contact?"), ++ message: Text(contact.displayName), + buttons: [ /* unchanged */ ] + ), + id: "deleteContactOrConversationDialog" + )) + } +``` + +**`deleteContactWithoutConversation`** (line 1324): + +```diff + showActionSheet(SomeActionSheet( + actionSheet: ActionSheet( + title: Text("Confirm contact deletion?"), ++ message: Text(contact.displayName), + buttons: [ /* unchanged */ ] + ), + id: "deleteContactWithoutConversation" + )) +``` + +**`deleteNotReadyContact`** (line 1348) — same: + +```diff + showActionSheet(SomeActionSheet( + actionSheet: ActionSheet( + title: Text("Confirm contact deletion?"), ++ message: Text(contact.displayName), + buttons: [ /* unchanged */ ] + ), + id: "deleteNotReadyContact" + )) +``` + +### 2.5 — `DeleteActiveContactDialog` sheet (line 1282) unchanged + +The secondary multi-option sheet is reached only after the user +confirms "Delete contact" in the previous action sheet — which now +shows the name. The sheet itself remains as-is. + +## Verification + +For each platform, exercise every entry point and confirm the +body reads `` on its own line followed by the existing +warning: + +- Android & Desktop: + - Chat list swipe — direct contact, group, channel, business chat + → delete / clear / leave. (Note folder's clear dialog is + intentionally unchanged — `clearNoteFolderDialog` excluded.) + - Chat info screens — "Delete contact" / "Delete group" / "Delete + channel" / "Clear conversation" / "Leave …" rows. + - Contact list (`ContactListNavView.kt:148`) — "Delete contact" + action shows the name in entry-point dialog and toggle dialog. + - Multi-option contact-delete path: entry dialog (now has a name + body where it had none) → toggle dialog (name above the + warning) → success. +- iOS: + - Same matrix from chat list swipe and chat info screens. + - Action-sheet contact-delete paths show the name as the + `message:` line on iPhone and iPad. + +Edge cases: + +- Long chat name — alert containers wrap automatically; the body + occupies 3+ lines. Confirm with a chat renamed to ~40 characters. +- Special characters (emoji, RTL, double quotes) — render literally + via string interpolation, no format-substitution involved. +- Empty `displayName` — does not occur in practice (`NamedChat` + enforces non-empty via `localAlias.ifEmpty { profile.displayName }`). + +Diff-level checks: + +- `git diff '*strings.xml' '*Localizable.strings'` returns zero + hunks. Pure code change. +- `git diff --stat` shows ~5 files total: two Kotlin dispatcher + files, three iOS view files. +- Cancel/confirm flows behave exactly as before — same API calls, + same model updates, same navigation. + +## Out of scope + +- Profile picture / avatar in dialogs — excluded by product decision. +- Refactoring the iOS duplication between `ChatListNavLink` and + `GroupChatInfoView` / `ChatInfoView` (pre-existing `// TODO` at + `GroupChatInfoView.swift:834`). +- Pre-existing wording divergence between Kotlin's "Clear chat?" + and iOS's "Clear conversation?". Both platforms keep their + titles. +- "Delete invitation" at `ChatListNavLink.swift:236` — has no + confirmation dialog (direct call to `deleteChat(chat)`); nothing + to modify. +- Bolding the chat name. SwiftUI `Text + Text` supports `.bold()` + on the first term, but Jetpack Compose `AlertDialog` text is a + single unstyled string — keeping both unstyled preserves parity. diff --git a/plans/delete-leave-dialog-with-profile.md b/plans/delete-leave-dialog-with-profile.md new file mode 100644 index 0000000000..a05fb66532 --- /dev/null +++ b/plans/delete-leave-dialog-with-profile.md @@ -0,0 +1,249 @@ +# Show chat name in delete / leave / clear confirmation dialogs + +## Goal + +The current delete-contact, delete-group, delete-channel, leave-group, +leave-channel and clear-chat confirmations are generic. From a long +chat list, swiping on a row and triggering one of these actions opens +a dialog whose title is "Delete group?", "Leave channel?", "Clear +conversation?" — with no indication of *which* chat is the target. A +user can easily act on the wrong chat. + +The fix: include the chat's display name in the dialog body, on a line +of its own above the existing warning text. Nothing else changes — +same title, same warning text, same buttons, same colors, same dialog +shape. No profile picture, no layout changes, no new helpers, no new +translation strings. + +We deliberately do NOT reuse the open-chat-link alert layout (centered +profile image + name + open-chat button). That layout is the *invite* +flow's identity; repurposing it for destructive confirmations would +confuse the two flows visually. The minimum change that solves the +"which chat?" problem is putting the name in the body text. + +## Why body, not title; why no new strings + +The title carries the action ("Delete group?", "Leave channel?"). The +body carries the consequences ("Group will be deleted for all +members…"). The chat name belongs with the body — it is the subject +of the consequence, not part of the question. + +Adding the name to the title would require new format-string variants +(`delete_group_named_question` etc.) and per-locale re-translation. +Putting the name on its own line in the body is a pure code change — +the existing translated warnings are concatenated with the chat name +in code: + +``` +Tech Talk + +Group will be deleted for all members - this cannot be undone! +``` + +The display name appears first because the user wants to confirm +*which* chat before reading *what* will happen. The blank line between +the name and the warning makes the name visually distinct. + +## Current state + +### Multiplatform (Kotlin / Android / Desktop) + +All eight dialogs go through `AlertManager.shared.showAlertDialog` or +`showAlertDialogButtonsColumn`: + +- `deleteGroupDialog` — `views/chat/group/GroupChatInfoView.kt:182` +- `leaveGroupDialog` — `views/chat/group/GroupChatInfoView.kt:222` +- `clearChatDialog` — `views/chat/ChatInfoView.kt:492` +- `deleteContactOrConversationDialog` — `views/chat/ChatInfoView.kt:248` +- `deleteActiveContactDialog` — `views/chat/ChatInfoView.kt:304` +- `deleteContactWithoutConversation` — `views/chat/ChatInfoView.kt:361` +- `deleteNotReadyContact` — `views/chat/ChatInfoView.kt:417` +- `deleteContactConnectionAlert` — `views/chatlist/ChatListNavLinkView.kt:772` + (deletes a pending contact connection; takes a `PendingContactConnection` + whose `displayName` reflects any custom name the user set) + +Call sites (chat-info screens, chat-list swipe / overflow, contact +list) funnel through these dispatcher functions. + +### iOS (Swift) + +Two SwiftUI patterns are used: + +- SwiftUI `Alert` with `primaryButton: .destructive` / `.cancel()`: + - `deleteGroupAlert` — `Views/ChatList/ChatListNavLink.swift:567`, + `Views/Chat/Group/GroupChatInfoView.swift:835` + - `leaveGroupAlert` — `Views/ChatList/ChatListNavLink.swift:622`, + `Views/Chat/Group/GroupChatInfoView.swift:872` + - `clearChatAlert` — `Views/ChatList/ChatListNavLink.swift:600`, + `Views/Chat/ChatInfoView.swift:577`, + `Views/Chat/Group/GroupChatInfoView.swift:858` +- SwiftUI `ActionSheet`: + - `deleteContactOrConversationDialog` — + `Views/Chat/ChatInfoView.swift:1177` + - `deleteContactWithoutConversation` — + `Views/Chat/ChatInfoView.swift:1324` + - `deleteNotReadyContact` — `Views/Chat/ChatInfoView.swift:1348` + +`Alert(message:)` accepts `Text`, and `ActionSheet(message:)` (an +existing optional parameter not used today) accepts `Text` too — so +the name can be added by composing the existing message string with +`"\n\n"` and the chat name. No widget changes. + +## Design + +| Dialog | Body today | Body after | +|---|---|---| +| Delete group | `Group will be deleted for all members – this cannot be undone!` | `Tech Talk` + blank line + existing text | +| Delete channel | `Channel will be deleted for all subscribers – this cannot be undone!` | `SimpleX news` + blank line + existing text | +| Leave group | `You will stop receiving messages from this group. …` | `Tech Talk` + blank line + existing text | +| Clear chat | `All messages will be deleted – this cannot be undone! …` | `Alice` + blank line + existing text | +| Delete contact (entry sheet) | *(no body today — title only + buttons)* | `Alice` (becomes the body) | +| Delete contact (active variant) | `Contact will be deleted – this cannot be undone!` | `Alice` + blank line + existing text | +| Confirm contact deletion (not-ready / no-conversation) | `Contact will be deleted – this cannot be undone!` | `Alice` + blank line + existing text | + +Title text is unchanged in every case. Existing titles +(`delete_contact_question`, `confirm_delete_contact_question`, etc.) +keep their semantic distinction — the "Confirm contact deletion?" +title still appears for the not-ready / no-conversation paths. + +### Which name to use: `displayName`, not `chatViewName` + +The chat list row labels chats with `cInfo.chatViewName` +(`ChatPreviewView.kt:87`), defined as: + +```kotlin +val chatViewName: String + get() = localAlias.ifEmpty { displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName") } +``` + +The dialog uses `chatInfo.displayName` (and `groupInfo.displayName` +for the leave dialog). For most chats these are identical: + +- If `localAlias` is set, both resolve to the alias. +- If `displayName == fullName` (or `fullName` is empty), both resolve + to `displayName`. + +For a contact with distinct display name and full name (no alias), +the row would show `alice / Alice Smith` while the dialog shows +`alice`. Acceptable: `displayName` is the recognizable identifier, +shorter, and the dialog format (single line above the warning) +benefits from concision. Two-part identifiers in the dialog would +crowd the layout. + +### `clearNoteFolderDialog` is excluded + +The local notes folder is a single-instance object — there is only +one per user — and its existing warning text already names it +unambiguously. Adding the display name on its own line would be +pure redundancy. Skipped. + +## Changes + +### Multiplatform (Kotlin) + +Each dispatcher function changes one argument: the `text =` parameter +passed to `AlertManager.shared.showAlertDialog` / +`showAlertDialogButtonsColumn`. The new value is the chat name + two +newlines + the existing message text: + +```kotlin +text = "${chatInfo.displayName}\n\n${generalGetString(messageId)}", +parseHtml = false, +``` + +`parseHtml = false` is a new boolean parameter added to both alert +helpers. It bypasses `escapedHtmlToAnnotatedString` so the +user-controlled `displayName` is rendered as literal text, never +interpreted as HTML markup (``, ``, `&`, etc.). The default +remains `true`; only our delete-confirmation dispatchers opt out. + +For `leaveGroupDialog` the source is `groupInfo.displayName` (the +function already takes `groupInfo` — no signature change needed, +no caller updates needed). + +For `deleteGroupDialog`, also `groupInfo.displayName`, for consistency +with `leaveGroupDialog` (both have `groupInfo` already in scope). + +For `deleteContactOrConversationDialog`, which has no `text =` +parameter today, add `text = chatInfo.displayName` (no concatenation +needed — the dialog had no body text before). + +### iOS + +Each of the eight call sites changes one argument: the `message:` +parameter passed to `Alert(…)` or `ActionSheet(…)`. The new value +composes the chat name with the existing localized message string: + +```swift +message: Text("\(chat.chatInfo.displayName)\n\n\(existingMessage)"), +``` + +For the three `ActionSheet` sites that have no `message:` today, add +`message: Text(chat.chatInfo.displayName)`. + +## Out of scope + +- Profile picture / avatar in any of these dialogs — excluded by + decision: the open-chat-link alert owns that layout, and reusing + it for destructive confirmations conflates two semantically + different flows. +- The pre-existing wording divergence between Kotlin's + `clear_chat_question` ("Clear chat?") and iOS's "Clear + conversation?". Both platforms keep their existing titles. +- Refactoring the iOS duplication between `ChatListNavLink` and + `GroupChatInfoView` / `ChatInfoView` (pre-existing `// TODO reuse + this and clearChatAlert with ChatInfoView` at + `GroupChatInfoView.swift:834`). +- "Delete invitation" at `ChatListNavLink.swift:236` — goes through + `deleteChat(chat)` directly with no confirmation dialog. No dialog + to modify. +- Bolding the chat name on its own line. SwiftUI `Text` concatenation + supports `.bold()`; Jetpack Compose `AlertDialog` text is a single + string. Keep both platforms unstyled for parity. + +## Verification + +Per platform, exercise every entry point and confirm the dialog body +reads `` on its own line followed by a blank line followed +by the existing warning: + +- Android & Desktop: + - Chat list swipe — direct contact, group, channel, business chat + → delete / clear / leave actions. (Note folder's clear dialog + is intentionally unchanged.) + - Chat info screens — "Delete contact" / "Delete group" / "Delete + channel" / "Clear conversation" / "Leave …" rows. + - Contact list (`ContactListNavView.kt:148`) — "Delete contact" + action. + - The multi-option contact-delete path: entry dialog (now has a + name body where it had none) → toggle dialog (name above the + warning) → success. +- iOS: + - Same matrix from chat list swipe and chat info screens. + - Action-sheet contact-delete paths show the name as the + `message:` line. + +Edge cases: + +- Long chat name (40+ chars) — alert containers wrap automatically; + body now occupies 3+ lines (name on 2, blank line, warning on 1+). + Confirm via a chat renamed to a long string. +- Special characters in name (emoji, RTL text, double quotes) — + render literally because the substitution is string concatenation, + not format expansion. A contact named `Bob "the builder"` displays + as `Bob "the builder"` on its own line. No quoting/escaping issue. +- Empty `displayName` would render an empty first line above the + warning. In practice `displayName` is non-empty (the `NamedChat` + interface enforces it via `localAlias.ifEmpty { profile.displayName }`); + no defensive trimming added. + +Diff-level checks: + +- `git diff strings.xml` and `git diff '*Localizable.strings'` show + zero hunks. The change is pure code. +- `git diff --stat` shows each platform touched in 2–4 files: + the dispatcher file(s) on Kotlin (`ChatInfoView.kt`, + `GroupChatInfoView.kt`), and the SwiftUI views holding the + alert/sheet builders on iOS. +- Behavior is unchanged. Cancel returns to the prior screen; + confirm performs the same destructive API call as before. diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 067fdc5ea9..657d20cb3f 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -169,6 +169,12 @@ data ChatConfig = ChatConfig chatHooks :: ChatHooks } +-- | Builds the read-only context threaded through store functions from chat config. +-- The single construction point, so new store-wide config (e.g. server keys) is added in one place. +mkStoreCxt :: ChatConfig -> StoreCxt +mkStoreCxt ChatConfig {chatVRange} = StoreCxt chatVRange +{-# INLINE mkStoreCxt #-} + data RandomAgentServers = RandomAgentServers { smpServers :: NonEmpty (ServerCfg 'PSMP), xftpServers :: NonEmpty (ServerCfg 'PXFTP) diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 2e48d7615c..fb7524decb 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -328,8 +328,8 @@ execChatCommand rh s retryNum = execChatCommand' :: ChatCommand -> Int -> CM' (Either ChatError ChatResponse) execChatCommand' cmd retryNum = handleCommandError $ do - vr <- chatVersionRange - processChatCommand vr (NRMInteractive' retryNum) cmd + cxt <- chatStoreCxt + processChatCommand cxt (NRMInteractive' retryNum) cmd execRemoteCommand :: RemoteHostId -> ChatCommand -> ByteString -> Int -> CM' (Either ChatError ChatResponse) execRemoteCommand rhId cmd s retryNum = handleCommandError $ getRemoteHostClient rhId >>= \rh -> processRemoteCommand rhId rh cmd s retryNum @@ -346,8 +346,8 @@ parseChatCommand :: ByteString -> Either String ChatCommand parseChatCommand = A.parseOnly chatCommandP . B.dropWhileEnd isSpace -- | Chat API commands interpreted in context of a local zone -processChatCommand :: VersionRangeChat -> NetworkRequestMode -> ChatCommand -> CM ChatResponse -processChatCommand vr nm = \case +processChatCommand :: StoreCxt -> NetworkRequestMode -> ChatCommand -> CM ChatResponse +processChatCommand cxt nm = \case ShowActiveUser -> withUser' $ pure . CRActiveUser CreateActiveUser NewUser {profile, pastTimestamp, userChatRelay, clientService} -> do forM_ profile $ \Profile {displayName} -> checkValidName displayName @@ -413,26 +413,26 @@ processChatCommand vr nm = \case SetActiveUser uName viewPwd_ -> do tryAllErrors (withFastStore (`getUserIdByName` uName)) >>= \case Left _ -> throwChatError CEUserUnknown - Right userId -> processChatCommand vr nm $ APISetActiveUser userId viewPwd_ + Right userId -> processChatCommand cxt nm $ APISetActiveUser userId viewPwd_ SetAllContactReceipts onOff -> withUser $ \_ -> withFastStore' (`updateAllContactReceipts` onOff) >> ok_ APISetUserContactReceipts userId' settings -> withUser $ \user -> do user' <- privateGetUser userId' validateUserPassword user user' Nothing withFastStore' $ \db -> updateUserContactReceipts db user' settings ok user - SetUserContactReceipts settings -> withUser $ \User {userId} -> processChatCommand vr nm $ APISetUserContactReceipts userId settings + SetUserContactReceipts settings -> withUser $ \User {userId} -> processChatCommand cxt nm $ APISetUserContactReceipts userId settings APISetUserGroupReceipts userId' settings -> withUser $ \user -> do user' <- privateGetUser userId' validateUserPassword user user' Nothing withFastStore' $ \db -> updateUserGroupReceipts db user' settings ok user - SetUserGroupReceipts settings -> withUser $ \User {userId} -> processChatCommand vr nm $ APISetUserGroupReceipts userId settings + SetUserGroupReceipts settings -> withUser $ \User {userId} -> processChatCommand cxt nm $ APISetUserGroupReceipts userId settings APISetUserAutoAcceptMemberContacts userId' onOff -> withUser $ \user -> do user' <- privateGetUser userId' validateUserPassword user user' Nothing withFastStore' $ \db -> updateUserAutoAcceptMemberContacts db user' onOff ok user - SetUserAutoAcceptMemberContacts onOff -> withUser $ \User {userId} -> processChatCommand vr nm $ APISetUserAutoAcceptMemberContacts userId onOff + SetUserAutoAcceptMemberContacts onOff -> withUser $ \User {userId} -> processChatCommand cxt nm $ APISetUserAutoAcceptMemberContacts userId onOff APIHideUser userId' (UserPwd viewPwd) -> withUser $ \user -> do user' <- privateGetUser userId' case viewPwdHash user' of @@ -458,10 +458,10 @@ processChatCommand vr nm = \case setUserPrivacy user user' {viewPwdHash = Nothing, showNtfs = True} APIMuteUser userId' -> setUserNotifications userId' False APIUnmuteUser userId' -> setUserNotifications userId' True - HideUser viewPwd -> withUser $ \User {userId} -> processChatCommand vr nm $ APIHideUser userId viewPwd - UnhideUser viewPwd -> withUser $ \User {userId} -> processChatCommand vr nm $ APIUnhideUser userId viewPwd - MuteUser -> withUser $ \User {userId} -> processChatCommand vr nm $ APIMuteUser userId - UnmuteUser -> withUser $ \User {userId} -> processChatCommand vr nm $ APIUnmuteUser userId + HideUser viewPwd -> withUser $ \User {userId} -> processChatCommand cxt nm $ APIHideUser userId viewPwd + UnhideUser viewPwd -> withUser $ \User {userId} -> processChatCommand cxt nm $ APIUnhideUser userId viewPwd + MuteUser -> withUser $ \User {userId} -> processChatCommand cxt nm $ APIMuteUser userId + UnmuteUser -> withUser $ \User {userId} -> processChatCommand cxt nm $ APIUnmuteUser userId SetClientService userId' name enable -> checkChatStopped $ withUser' $ \currUser@User {userId} -> do user@User {agentUserId = AgentUserId auId, clientService, profile = LocalProfile {displayName}} <- if userId == userId' then pure currUser else privateGetUser userId' @@ -544,7 +544,7 @@ processChatCommand vr nm = \case ExportArchive -> do ts <- liftIO getCurrentTime let filePath = "simplex-chat." <> formatTime defaultTimeLocale "%FT%H%M%SZ" ts <> ".zip" - processChatCommand vr nm $ APIExportArchive $ ArchiveConfig filePath Nothing Nothing + processChatCommand cxt nm $ APIExportArchive $ ArchiveConfig filePath Nothing Nothing APIImportArchive cfg -> checkChatStopped $ do fileErrs <- lift $ importArchive cfg setStoreChanged @@ -573,16 +573,16 @@ processChatCommand vr nm = \case tags <- withFastStore' (`getUserChatTags` user) pure $ CRChatTags user tags APIGetChats {userId, pendingConnections, pagination, query} -> withUserId' userId $ \user -> do - (errs, previews) <- partitionEithers <$> withFastStore' (\db -> getChatPreviews db vr user pendingConnections pagination query) + (errs, previews) <- partitionEithers <$> withFastStore' (\db -> getChatPreviews db cxt user pendingConnections pagination query) unless (null errs) $ toView $ CEvtChatErrors (map ChatErrorStore errs) pure $ CRApiChats user previews APIGetChat (ChatRef cType cId scope_) contentFilter pagination search -> withUser $ \user -> case cType of -- TODO optimize queries calculating ChatStats, currently they're disabled CTDirect -> do - (directChat, navInfo) <- withFastStore (\db -> getDirectChat db vr user cId contentFilter pagination search) + (directChat, navInfo) <- withFastStore (\db -> getDirectChat db cxt user cId contentFilter pagination search) pure $ CRApiChat user (AChat SCTDirect directChat) navInfo CTGroup -> do - (groupChat, navInfo) <- withFastStore (\db -> getGroupChat db vr user cId scope_ contentFilter pagination search) + (groupChat, navInfo) <- withFastStore (\db -> getGroupChat db cxt user cId scope_ contentFilter pagination search) groupChat' <- checkSupportChatAttention user groupChat pure $ CRApiChat user (AChat SCTGroup groupChat') navInfo CTLocal -> do @@ -598,7 +598,7 @@ processChatCommand vr nm = \case case correctedMemAttention (groupMemberId' scopeMem) suppChat chatItems of Just newMemAttention -> do (gInfo', scopeMem') <- - withFastStore' $ \db -> setSupportChatMemberAttention db vr user gInfo scopeMem newMemAttention + withFastStore' $ \db -> setSupportChatMemberAttention db cxt user gInfo scopeMem newMemAttention pure (groupChat {chatInfo = GroupChat gInfo' (Just $ GCSIMemberSupport (Just scopeMem'))} :: Chat 'CTGroup) Nothing -> pure groupChat _ -> pure groupChat @@ -615,11 +615,11 @@ processChatCommand vr nm = \case APIGetChatContentTypes chatRef -> withUser $ \user -> CRChatContentTypes <$> withStore (\db -> getChatContentTypes db user chatRef) APIGetChatItems pagination search -> withUser $ \user -> do - chatItems <- withFastStore $ \db -> getAllChatItems db vr user pagination search + chatItems <- withFastStore $ \db -> getAllChatItems db cxt user pagination search pure $ CRChatItems user Nothing chatItems APIGetChatItemInfo chatRef itemId -> withUser $ \user -> do (aci@(AChatItem cType dir _ ci), versions) <- withFastStore $ \db -> - (,) <$> getAChatItem db vr user chatRef itemId <*> liftIO (getChatItemVersions db itemId) + (,) <$> getAChatItem db cxt user chatRef itemId <*> liftIO (getChatItemVersions db itemId) let itemVersions = if null versions then maybeToList $ mkItemVersion ci else versions memberDeliveryStatuses <- case (cType, dir) of (SCTGroup, SMDSnd) -> L.nonEmpty <$> withFastStore' (`getGroupSndStatuses` itemId) @@ -630,10 +630,10 @@ processChatCommand vr nm = \case getForwardedFromItem :: User -> ChatItem c d -> CM (Maybe AChatItem) getForwardedFromItem user ChatItem {meta = CIMeta {itemForwarded}} = case itemForwarded of Just (CIFFContact _ _ (Just ctId) (Just fwdItemId)) -> - Just <$> withFastStore (\db -> getAChatItem db vr user (ChatRef CTDirect ctId Nothing) fwdItemId) + Just <$> withFastStore (\db -> getAChatItem db cxt user (ChatRef CTDirect ctId Nothing) fwdItemId) Just (CIFFGroup _ _ (Just gId) (Just fwdItemId)) -> -- TODO [knocking] getAChatItem doesn't differentiate how to read based on scope - it should, instead of using group filter - Just <$> withFastStore (\db -> getAChatItem db vr user (ChatRef CTGroup gId Nothing) fwdItemId) + Just <$> withFastStore (\db -> getAChatItem db cxt user (ChatRef CTGroup gId Nothing) fwdItemId) _ -> pure Nothing APISendMessages sendRef live itemTTL cms -> withUser $ \user -> mapM_ assertAllowedContent' cms >> case sendRef of SRDirect chatId -> do @@ -646,7 +646,7 @@ processChatCommand vr nm = \case Nothing -> pure () withGroupLock "sendMessage" chatId $ do (gInfo, cmrs) <- withFastStore $ \db -> do - g <- getGroupInfo db vr user chatId + g <- getGroupInfo db cxt user chatId (g,) <$> mapM (composedMessageReqMentions db user g) cms sendGroupContentMessages user gInfo gsScope asGroup live itemTTL cmrs APICreateChatTag (ChatTagData emoji text) -> withUser $ \user -> withFastStore' $ \db -> do @@ -674,18 +674,18 @@ processChatCommand vr nm = \case createNoteFolderContentItems user folderId (L.map composedMessageReq cms) APIReportMessage gId reportedItemId reportReason reportText -> withUser $ \user -> withGroupLock "reportMessage" gId $ do - gInfo <- withFastStore $ \db -> getGroupInfo db vr user gId + gInfo <- withFastStore $ \db -> getGroupInfo db cxt user gId let mc = MCReport reportText reportReason cm = ComposedMessage {fileSource = Nothing, quotedItemId = Just reportedItemId, msgContent = mc, mentions = M.empty} sendGroupContentMessages user gInfo (Just $ GCSMemberSupport Nothing) False False Nothing [composedMessageReq cm] ReportMessage {groupName, contactName_, reportReason, reportedMessage} -> withUser $ \user -> do gId <- withFastStore $ \db -> getGroupIdByName db user groupName reportedItemId <- withFastStore $ \db -> getGroupChatItemIdByText db user gId contactName_ reportedMessage - processChatCommand vr nm $ APIReportMessage gId reportedItemId reportReason "" + processChatCommand cxt nm $ APIReportMessage gId reportedItemId reportReason "" APIUpdateChatItem (ChatRef cType chatId scope) itemId live (UpdatedMessage mc mentions) -> withUser $ \user -> assertAllowedContent mc >> case cType of CTDirect -> withContactLock "updateChatItem" chatId $ do unless (null mentions) $ throwCmdError "mentions are not supported in this chat" - ct@Contact {contactId} <- withFastStore $ \db -> getContact db vr user chatId + ct@Contact {contactId} <- withFastStore $ \db -> getContact db cxt user chatId assertDirectAllowed user MDSnd ct XMsgUpdate_ cci <- withFastStore $ \db -> getDirectCIWithReactions db user ct itemId case cci of @@ -709,7 +709,7 @@ processChatCommand vr nm = \case _ -> throwChatError CEInvalidChatItemUpdate CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate CTGroup -> withGroupLock "updateChatItem" chatId $ do - gInfo@GroupInfo {groupId, membership} <- withFastStore $ \db -> getGroupInfo db vr user chatId + gInfo@GroupInfo {groupId, membership} <- withFastStore $ \db -> getGroupInfo db cxt user chatId when (isNothing scope) $ assertUserGroupRole gInfo GRAuthor let (_, ft_) = msgContentTexts mc if prohibitedSimplexLinks gInfo membership mc ft_ @@ -721,8 +721,8 @@ processChatCommand vr nm = \case CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable, showGroupAsSender}, content = ciContent} -> do case (ciContent, itemSharedMsgId, editable) of (CISndMsgContent oldMC, Just itemSharedMId, True) -> do - chatScopeInfo <- mapM (getChatScopeInfo vr user) scope - recipients <- getGroupRecipients vr user gInfo chatScopeInfo groupKnockingVersion + chatScopeInfo <- mapM (getChatScopeInfo cxt user) scope + recipients <- getGroupRecipients cxt user gInfo chatScopeInfo groupKnockingVersion let changed = mc /= oldMC if changed || fromMaybe False itemLive then do @@ -778,7 +778,7 @@ processChatCommand vr nm = \case CTGroup -> withGroupLock "deleteChatItem" chatId $ do (gInfo, items) <- getCommandGroupChatItems user chatId itemIds -- TODO [knocking] check scope for all items? - chatScopeInfo <- mapM (getChatScopeInfo vr user) scope + chatScopeInfo <- mapM (getChatScopeInfo cxt user) scope deletions <- case mode of CIDMInternal | publicGroupEditor gInfo (membership gInfo) -> throwChatError CEInvalidChatItemDelete @@ -786,7 +786,7 @@ processChatCommand vr nm = \case CIDMInternalMark -> do markGroupCIsDeleted user gInfo chatScopeInfo items Nothing =<< liftIO getCurrentTime CIDMBroadcast -> do - recipients <- getGroupRecipients vr user gInfo chatScopeInfo groupKnockingVersion + recipients <- getGroupRecipients cxt user gInfo chatScopeInfo groupKnockingVersion assertDeletable items assertUserGroupRole gInfo GRObserver -- can still delete messages sent earlier let msgIds = itemsMsgIds items @@ -795,7 +795,7 @@ processChatCommand vr nm = \case delGroupChatItems user gInfo chatScopeInfo items False CIDMHistory -> do unless (publicGroupEditor gInfo (membership gInfo)) $ throwChatError CEInvalidChatItemDelete - recipients <- getGroupRecipients vr user gInfo chatScopeInfo groupKnockingVersion + recipients <- getGroupRecipients cxt user gInfo chatScopeInfo groupKnockingVersion let msgIds = itemsMsgIds items events = L.nonEmpty $ map (\msgId -> XMsgDel msgId Nothing (toMsgScope gInfo <$> chatScopeInfo) True) msgIds mapM_ (sendGroupMessages user gInfo Nothing False recipients) events @@ -823,12 +823,12 @@ processChatCommand vr nm = \case APIDeleteMemberChatItem gId itemIds -> withUser $ \user -> withGroupLock "deleteChatItem" gId $ do (gInfo, items) <- getCommandGroupChatItems user gId itemIds -- TODO [knocking] check scope is Nothing for all items? (prohibit moderation in support chats?) - ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo + ms <- withFastStore' $ \db -> getGroupMembers db cxt user gInfo let recipients = filter memberCurrent ms deletions <- delGroupChatItemsForMembers user gInfo Nothing recipients items pure $ CRChatItemsDeleted user deletions True False APIArchiveReceivedReports gId -> withUser $ \user -> withFastStore $ \db -> do - g <- getGroupInfo db vr user gId + g <- getGroupInfo db cxt user gId deleteTs <- liftIO getCurrentTime ciIds <- liftIO $ markReceivedGroupReportsDeleted db user g deleteTs pure $ CRGroupChatItemsDeleted user g ciIds True (Just $ membership g) @@ -842,7 +842,7 @@ processChatCommand vr nm = \case CIDMInternalMark -> markGroupCIsDeleted user gInfo Nothing items Nothing =<< liftIO getCurrentTime CIDMHistory -> throwChatError CEInvalidChatItemDelete CIDMBroadcast -> do - ms <- withFastStore' $ \db -> getGroupModerators db vr user gInfo + ms <- withFastStore' $ \db -> getGroupModerators db cxt user gInfo let recipients = filter memberCurrent ms delGroupChatItemsForMembers user gInfo Nothing recipients items pure $ CRChatItemsDeleted user deletions True False @@ -853,7 +853,7 @@ processChatCommand vr nm = \case APIChatItemReaction (ChatRef cType chatId scope) itemId add reaction -> withUser $ \user -> case cType of CTDirect -> withContactLock "chatItemReaction" chatId $ - withFastStore (\db -> (,) <$> getContact db vr user chatId <*> getDirectChatItem db user chatId itemId) >>= \case + withFastStore (\db -> (,) <$> getContact db cxt user chatId <*> getDirectChatItem db user chatId itemId) >>= \case (ct, CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}}) -> do unless (featureAllowed SCFReactions forUser ct) $ throwCmdError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFReactions) @@ -874,10 +874,10 @@ processChatCommand vr nm = \case withGroupLock "chatItemReaction" chatId $ do -- TODO [knocking] check chat item scope? (g@GroupInfo {membership}, CChatItem md ci) <- withFastStore $ \db -> do - g <- getGroupInfo db vr user chatId + g <- getGroupInfo db cxt user chatId (g,) <$> getGroupCIWithReactions db user g itemId - chatScopeInfo <- mapM (getChatScopeInfo vr user) scope - recipients <- getGroupRecipients vr user g chatScopeInfo groupKnockingVersion + chatScopeInfo <- mapM (getChatScopeInfo cxt user) scope + recipients <- getGroupRecipients cxt user g chatScopeInfo groupKnockingVersion case ci of ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}} -> do unless (groupFeatureAllowed SGFReactions g) $ @@ -908,7 +908,7 @@ processChatCommand vr nm = \case APIGetReactionMembers userId groupId itemId reaction -> withUserId userId $ \user -> do memberReactions <- withStore $ \db -> do CChatItem _ ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}} <- getGroupChatItem db user groupId itemId - liftIO $ getReactionMembers db vr user groupId itemSharedMId reaction + liftIO $ getReactionMembers db cxt user groupId itemSharedMId reaction pure $ CRReactionMembers user memberReactions -- TODO [knocking] forward from scope? APIPlanForwardChatItems (ChatRef fromCType fromChatId _scope) itemIds -> withUser $ \user -> case fromCType of @@ -972,7 +972,7 @@ processChatCommand vr nm = \case case L.nonEmpty cmrs of Just cmrs' -> withGroupLock "forwardChatItem, to group" toChatId $ do - gInfo <- withFastStore $ \db -> getGroupInfo db vr user toChatId + gInfo <- withFastStore $ \db -> getGroupInfo db cxt user toChatId sendGroupContentMessages user gInfo toScope sendAsGroup False itemTTL cmrs' Nothing -> pure $ CRNewChatItems user [] CTLocal -> do @@ -1106,7 +1106,7 @@ processChatCommand vr nm = \case pure $ prefix <> formattedDate <> ext APIShareChatMsgContent (ChatRef CTGroup groupId _) toSendRef -> withUser $ \user -> do GroupInfo {groupProfile = gp@GroupProfile {publicGroup}, membership = GroupMember {memberId, memberRole}, groupKeys} <- - withFastStore $ \db -> getGroupInfo db vr user groupId + withFastStore $ \db -> getGroupInfo db cxt user groupId case publicGroup of Nothing -> throwCmdError "not a public group" Just PublicGroupProfile {groupLink} -> do @@ -1128,11 +1128,11 @@ processChatCommand vr nm = \case shareChatBinding :: User -> SendRef -> CM (Maybe (ChatBinding, ByteString)) shareChatBinding u = \case SRDirect contactId -> do - ct <- withFastStore $ \db -> getContact db vr u contactId + ct <- withFastStore $ \db -> getContact db cxt u contactId forM (contactConn ct) $ \conn -> (CBDirect,) <$> withAgent (`getConnectionRatchetAdHash` aConnId conn) SRGroup toGroupId _ asGroup -> do - GroupInfo {groupProfile = GroupProfile {publicGroup}, membership = m} <- withFastStore $ \db -> getGroupInfo db vr u toGroupId + GroupInfo {groupProfile = GroupProfile {publicGroup}, membership = m} <- withFastStore $ \db -> getGroupInfo db cxt u toGroupId pure $ mkBinding m <$> publicGroup where mkBinding GroupMember {memberId} PublicGroupProfile {publicGroupId = pgId} @@ -1140,7 +1140,7 @@ processChatCommand vr nm = \case | otherwise = (CBGroup, smpEncode (pgId, memberId)) APIShareChatMsgContent _ _ -> throwCmdError "sharing is only supported for public groups" APIUserRead userId -> withUserId userId $ \user -> withFastStore' (`setUserChatsRead` user) >> ok user - UserRead -> withUser $ \User {userId} -> processChatCommand vr nm $ APIUserRead userId + UserRead -> withUser $ \User {userId} -> processChatCommand cxt nm $ APIUserRead userId APIChatRead chatRef@(ChatRef cType chatId scope_) -> withUser $ \_ -> case cType of CTDirect -> do user <- withFastStore $ \db -> getUserByContactId db chatId @@ -1154,7 +1154,7 @@ processChatCommand vr nm = \case CTGroup -> do (user, gInfo) <- withFastStore $ \db -> do user <- getUserByGroupId db chatId - gInfo <- getGroupInfo db vr user chatId + gInfo <- getGroupInfo db cxt user chatId pure (user, gInfo) ts <- liftIO getCurrentTime case scope_ of @@ -1166,10 +1166,10 @@ processChatCommand vr nm = \case forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt ok user Just scope -> do - scopeInfo <- getChatScopeInfo vr user scope + scopeInfo <- getChatScopeInfo cxt user scope (gInfo', m', timedItems) <- withFastStore' $ \db -> do timedItems <- getGroupUnreadTimedItems db user chatId (Just scope) - (gInfo', m') <- updateSupportChatItemsRead db vr user gInfo scopeInfo + (gInfo', m') <- updateSupportChatItemsRead db cxt user gInfo scopeInfo timedItems' <- setGroupChatItemsDeleteAt db user chatId timedItems ts pure (gInfo', m', timedItems') forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt @@ -1184,7 +1184,7 @@ processChatCommand vr nm = \case CTDirect -> do (user, ct) <- withFastStore $ \db -> do user <- getUserByContactId db chatId - ct <- getContact db vr user chatId + ct <- getContact db cxt user chatId pure (user, ct) timedItems <- withFastStore' $ \db -> do timedItems <- updateDirectChatItemsReadList db user chatId itemIds @@ -1194,11 +1194,11 @@ processChatCommand vr nm = \case CTGroup -> do (user, gInfo) <- withFastStore $ \db -> do user <- getUserByGroupId db chatId - gInfo <- getGroupInfo db vr user chatId + gInfo <- getGroupInfo db cxt user chatId pure (user, gInfo) - chatScopeInfo <- mapM (getChatScopeInfo vr user) scope + chatScopeInfo <- mapM (getChatScopeInfo cxt user) scope (timedItems, gInfo') <- withFastStore $ \db -> do - (timedItems, gInfo') <- updateGroupChatItemsReadList db vr user gInfo chatScopeInfo itemIds + (timedItems, gInfo') <- updateGroupChatItemsReadList db cxt user gInfo chatScopeInfo itemIds timedItems' <- liftIO $ setGroupChatItemsDeleteAt db user chatId timedItems =<< getCurrentTime pure (timedItems', gInfo') forM_ timedItems $ \(itemId, deleteAt) -> startProximateTimedItemThread user (chatRef, itemId) deleteAt @@ -1209,13 +1209,13 @@ processChatCommand vr nm = \case APIChatUnread (ChatRef cType chatId scope) unreadChat -> withUser $ \user -> case cType of CTDirect -> do withFastStore $ \db -> do - ct <- getContact db vr user chatId + ct <- getContact db cxt user chatId liftIO $ updateContactUnreadChat db user ct unreadChat ok user -- TODO [knocking] set support chat as unread? CTGroup | isNothing scope -> do withFastStore $ \db -> do - gInfo <- getGroupInfo db vr user chatId + gInfo <- getGroupInfo db cxt user chatId liftIO $ updateGroupUnreadChat db user gInfo unreadChat ok user CTLocal -> do @@ -1226,7 +1226,7 @@ processChatCommand vr nm = \case _ -> throwCmdError "not supported" APIDeleteChat cRef@(ChatRef cType chatId scope) cdm -> withUser $ \user@User {userId} -> case cType of CTDirect -> do - ct <- withFastStore $ \db -> getContact db vr user chatId + ct <- withFastStore $ \db -> getContact db cxt user chatId filesInfo <- withFastStore' $ \db -> getContactFileInfo db user ct withContactLock "deleteChat direct" chatId $ case cdm of @@ -1246,17 +1246,17 @@ processChatCommand vr nm = \case ct' <- withFastStore $ \db -> do liftIO $ deleteContactConnections db user ct liftIO $ void $ updateContactStatus db user ct CSDeletedByUser - getContact db vr user chatId + getContact db cxt user chatId pure $ CRContactDeleted user ct' CDMMessages -> do - void $ processChatCommand vr nm $ APIClearChat cRef + void $ processChatCommand cxt nm $ APIClearChat cRef withFastStore' $ \db -> setContactChatDeleted db user ct True pure $ CRContactDeleted user ct {chatDeleted = True} where sendDelDeleteConns ct notify = do let doSendDel = contactReady ct && contactActive ct && notify when doSendDel $ void (sendDirectContactMessage user ct XDirectDel) `catchAllErrors` const (pure ()) - contactConnIds <- map aConnId <$> withFastStore' (\db -> getContactConnections db vr userId ct) + contactConnIds <- map aConnId <$> withFastStore' (\db -> getContactConnections db cxt userId ct) deleteAgentConnectionsAsync' contactConnIds doSendDel CTContactConnection -> withConnectionLock "deleteChat contactConnection" chatId $ do conn@PendingContactConnection {pccAgentConnId = AgentConnId acId} <- withFastStore $ \db -> getPendingContactConnection db userId chatId @@ -1264,7 +1264,7 @@ processChatCommand vr nm = \case withFastStore' $ \db -> deletePendingContactConnection db userId chatId pure $ CRContactConnectionDeleted user conn CTGroup | isNothing scope -> do - gInfo@GroupInfo {membership} <- withFastStore $ \db -> getGroupInfo db vr user chatId + gInfo@GroupInfo {membership} <- withFastStore $ \db -> getGroupInfo db cxt user chatId let isOwner = memberRole' membership == GROwner canDelete = isOwner || not (memberCurrent membership) unless canDelete $ throwChatError $ CEGroupUserRole gInfo GROwner @@ -1288,25 +1288,25 @@ processChatCommand vr nm = \case where getRecipients gInfo | useRelays' gInfo = do - relays <- withFastStore' $ \db -> getGroupRelayMembers db vr user gInfo + relays <- withFastStore' $ \db -> getGroupRelayMembers db cxt user gInfo pure (relays, relays) | otherwise = do - ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo + ms <- withFastStore' $ \db -> getGroupMembers db cxt user gInfo pure (ms, filter memberCurrentOrPending ms) _ -> throwCmdError "not supported" APIClearChat (ChatRef cType chatId scope) -> withUser $ \user@User {userId} -> case cType of CTDirect -> do - ct <- withFastStore $ \db -> getContact db vr user chatId + ct <- withFastStore $ \db -> getContact db cxt user chatId filesInfo <- withFastStore' $ \db -> getContactFileInfo db user ct deleteCIFiles user filesInfo withFastStore' $ \db -> deleteContactCIs db user ct pure $ CRChatCleared user (AChatInfo SCTDirect $ DirectChat ct) CTGroup | isNothing scope -> do - gInfo <- withFastStore $ \db -> getGroupInfo db vr user chatId + gInfo <- withFastStore $ \db -> getGroupInfo db cxt user chatId filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo deleteCIFiles user filesInfo withFastStore' $ \db -> deleteGroupChatItemsMessages db user gInfo - membersToDelete <- withFastStore' $ \db -> getGroupMembersForExpiration db vr user gInfo + membersToDelete <- withFastStore' $ \db -> getGroupMembersForExpiration db cxt user gInfo forM_ membersToDelete $ \m -> withFastStore' $ \db -> deleteGroupMember db user m pure $ CRChatCleared user (AChatInfo SCTGroup $ GroupChat gInfo Nothing) CTLocal -> do @@ -1367,7 +1367,7 @@ processChatCommand vr nm = \case withFastStore $ \db -> do cReq@UserContactRequest {contactId_} <- getContactRequest db user connReqId ct_ <- forM contactId_ $ \contactId -> do - ct <- getContact db vr user contactId + ct <- getContact db cxt user contactId deleteContact db user ct pure ct liftIO $ deleteContactRequest db user connReqId @@ -1376,7 +1376,7 @@ processChatCommand vr nm = \case pure $ CRContactRequestRejected user cReq ct_ APISendCallInvitation contactId callType -> withUser $ \user -> do -- party initiating call - ct <- withFastStore $ \db -> getContact db vr user contactId + ct <- withFastStore $ \db -> getContact db cxt user contactId assertDirectAllowed user MDSnd ct XCallInv_ if featureAllowed SCFCalls forUser ct then do @@ -1398,7 +1398,7 @@ processChatCommand vr nm = \case else throwCmdError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFCalls) SendCallInvitation cName callType -> withUser $ \user -> do contactId <- withFastStore $ \db -> getContactIdByName db user cName - processChatCommand vr nm $ APISendCallInvitation contactId callType + processChatCommand cxt nm $ APISendCallInvitation contactId callType APIRejectCall contactId -> -- party accepting call withCurrentCall contactId $ \user ct Call {chatItemId, callState} -> case callState of @@ -1465,23 +1465,23 @@ processChatCommand vr nm = \case _ -> Nothing rcvCallInvitation (contactId, callUUID, callTs, peerCallType, sharedKey) = runExceptT . withFastStore $ \db -> do user <- getUserByContactId db contactId - contact <- getContact db vr user contactId + contact <- getContact db cxt user contactId pure RcvCallInvitation {user, contact, callType = peerCallType, sharedKey, callUUID, callTs} APICallStatus contactId receivedStatus -> withCurrentCall contactId $ \user ct call -> updateCallItemStatus user ct call receivedStatus Nothing $> Just call APIUpdateProfile userId profile -> withUserId userId (`updateProfile` profile) APISetContactPrefs contactId prefs' -> withUser $ \user -> do - ct <- withFastStore $ \db -> getContact db vr user contactId + ct <- withFastStore $ \db -> getContact db cxt user contactId updateContactPrefs user ct prefs' APISetContactAlias contactId localAlias -> withUser $ \user@User {userId} -> do ct' <- withFastStore $ \db -> do - ct <- getContact db vr user contactId + ct <- getContact db cxt user contactId liftIO $ updateContactAlias db userId ct localAlias pure $ CRContactAliasUpdated user ct' APISetGroupAlias gId localAlias -> withUser $ \user@User {userId} -> do gInfo' <- withFastStore $ \db -> do - gInfo <- getGroupInfo db vr user gId + gInfo <- getGroupInfo db cxt user gId liftIO $ updateGroupAlias db userId gInfo localAlias pure $ CRGroupAliasUpdated user gInfo' APISetConnectionAlias connId localAlias -> withUser $ \user@User {userId} -> do @@ -1499,23 +1499,23 @@ processChatCommand vr nm = \case APISetChatUIThemes (ChatRef cType chatId scope) uiThemes -> withUser $ \user -> case cType of CTDirect -> do withFastStore $ \db -> do - ct <- getContact db vr user chatId + ct <- getContact db cxt user chatId liftIO $ setContactUIThemes db user ct uiThemes ok user CTGroup | isNothing scope -> do withFastStore $ \db -> do - g <- getGroupInfo db vr user chatId + g <- getGroupInfo db cxt user chatId liftIO $ setGroupUIThemes db user g uiThemes ok user _ -> throwCmdError "not supported" APISetGroupCustomData groupId customData_ -> withUser $ \user -> do withFastStore $ \db -> do - g <- getGroupInfo db vr user groupId + g <- getGroupInfo db cxt user groupId liftIO $ setGroupCustomData db user g customData_ ok user APISetContactCustomData contactId customData_ -> withUser $ \user -> do withFastStore $ \db -> do - ct <- getContact db vr user contactId + ct <- getContact db cxt user contactId liftIO $ setContactCustomData db user ct customData_ ok user APIGetNtfToken -> withUser' $ \_ -> crNtfToken <$> withAgent getNtfToken @@ -1536,7 +1536,7 @@ processChatCommand vr nm = \case let agentConnId = AgentConnId ntfConnId mkNtfConn user connEntity = NtfConn {user, agentConnId, agentDbQueueId = ntfDbQueueId, connEntity, expectedMsg_ = expectedMsgInfo <$> nMsgMeta} getUserByAConnId db agentConnId - $>>= \user -> fmap (mkNtfConn user) . eitherToMaybe <$> runExceptT (getConnectionEntity db vr user agentConnId) + $>>= \user -> fmap (mkNtfConn user) . eitherToMaybe <$> runExceptT (getConnectionEntity db cxt user agentConnId) APIGetConnNtfMessages connMsgs -> withUser $ \_ -> do msgs <- lift $ withAgent' (`getConnectionMessages` connMsgs) let ntfMsgs = L.map receivedMsgInfo msgs @@ -1552,7 +1552,7 @@ processChatCommand vr nm = \case [] -> throwCmdError "no servers" _ -> do srvs' <- mapM aUserServer srvs - processChatCommand vr nm $ APISetUserServers userId $ L.map (updatedServers p srvs') userServers + processChatCommand cxt nm $ APISetUserServers userId $ L.map (updatedServers p srvs') userServers where aUserServer :: AProtoServerWithAuth -> CM (AUserServer p) aUserServer (AProtoServerWithAuth p' srv) = case testEquality p p' of @@ -1561,7 +1561,7 @@ processChatCommand vr nm = \case APITestProtoServer userId srv@(AProtoServerWithAuth _ server) -> withUserId userId $ \user -> lift $ CRServerTestResult user srv <$> withAgent' (\a -> testProtocolServer a nm (aUserId user) server) TestProtoServer srv -> withUser $ \User {userId} -> - processChatCommand vr nm $ APITestProtoServer userId srv + processChatCommand cxt nm $ APITestProtoServer userId srv APITestChatRelay userId address -> withUserId userId $ \user -> do let failAt step e = pure $ CRChatRelayTestResult user Nothing (Just $ RelayTestFailure step e) r <- tryAllErrors $ getShortLinkConnReq nm user address @@ -1581,7 +1581,7 @@ processChatCommand vr nm = \case subMode <- chatReadVar subscriptionMode connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq PQSupportOff conn@Connection {connId = testCId} <- withFastStore $ \db -> - createRelayTestConnection db vr user connId ConnPrepared chatV subMode + createRelayTestConnection db cxt user connId ConnPrepared chatV subMode challenge <- drgRandomBytes 32 testVar <- newEmptyTMVarIO let acId = aConnId conn @@ -1601,9 +1601,9 @@ processChatCommand vr nm = \case Right (Just Nothing) -> pure $ CRChatRelayTestResult user (Just relayProfile) Nothing Right (Just (Just failure)) -> pure $ CRChatRelayTestResult user (Just relayProfile) (Just failure) TestChatRelay address -> withUser $ \User {userId} -> - processChatCommand vr nm $ APITestChatRelay userId address + processChatCommand cxt nm $ APITestChatRelay userId address APIAllowRelayGroup groupId -> withUser $ \user -> do - gInfo' <- withStore $ \db -> allowRelayGroup db vr user groupId + gInfo' <- withStore $ \db -> allowRelayGroup db cxt user groupId pure $ CRRelayGroupAllowed user gInfo' GetUserChatRelays -> withUser $ \user -> do srvs <- withFastStore (`getUserServers` user) @@ -1616,7 +1616,7 @@ processChatCommand vr nm = \case [] -> throwCmdError "no relays" _ -> do let relays' = map aUserRelay relays - processChatCommand vr nm $ APISetUserServers userId $ L.map (updatedRelays relays') userServers + processChatCommand cxt nm $ APISetUserServers userId $ L.map (updatedRelays relays') userServers where aUserRelay :: CLINewRelay -> AUserChatRelay aUserRelay CLINewRelay {address, name} = AUCR SDBNew $ newChatRelay (mkRelayProfile name Nothing) [""] address @@ -1645,7 +1645,7 @@ processChatCommand vr nm = \case SetServerOperators operatorsRoles -> do ops <- serverOperators <$> withFastStore getServerOperators ops' <- mapM (updateOp ops) operatorsRoles - processChatCommand vr nm $ APISetServerOperators ops' + processChatCommand cxt nm $ APISetServerOperators ops' where updateOp :: [ServerOperator] -> ServerOperatorRoles -> CM ServerOperator updateOp ops r = @@ -1710,14 +1710,14 @@ processChatCommand vr nm = \case expireChat user globalTTL = do currentTs <- liftIO getCurrentTime case cType of - CTDirect -> expireContactChatItems user vr globalTTL chatId + CTDirect -> expireContactChatItems user cxt globalTTL chatId CTGroup | isNothing scope -> let createdAtCutoff = addUTCTime (-43200 :: NominalDiffTime) currentTs - in expireGroupChatItems user vr globalTTL createdAtCutoff chatId + in expireGroupChatItems user cxt globalTTL createdAtCutoff chatId _ -> throwCmdError "not supported" SetChatTTL chatName newTTL -> withUser' $ \user@User {userId} -> do chatRef <- getChatRef user chatName - processChatCommand vr nm $ APISetChatTTL userId chatRef newTTL + processChatCommand cxt nm $ APISetChatTTL userId chatRef newTTL GetChatTTL chatName -> withUser' $ \user -> do -- TODO [knocking] support scope in CLI apis ChatRef cType chatId _ <- getChatRef user chatName @@ -1737,18 +1737,18 @@ processChatCommand vr nm = \case lift $ setChatItemsExpiration user newTTL ttlCount ok user SetChatItemTTL newTTL_ -> withUser' $ \User {userId} -> do - processChatCommand vr nm $ APISetChatItemTTL userId newTTL_ + processChatCommand cxt nm $ APISetChatItemTTL userId newTTL_ APIGetChatItemTTL userId -> withUserId' userId $ \user -> do ttl <- withFastStore' (`getChatItemTTL` user) pure $ CRChatItemTTL user (Just ttl) GetChatItemTTL -> withUser' $ \User {userId} -> do - processChatCommand vr nm $ APIGetChatItemTTL userId + processChatCommand cxt nm $ APIGetChatItemTTL userId APISetNetworkConfig cfg -> withUser' $ \_ -> withAgent (`setNetworkConfig` cfg) >> ok_ APIGetNetworkConfig -> withUser' $ \_ -> CRNetworkConfig <$> lift getNetworkConfig SetNetworkConfig simpleNetCfg -> do cfg <- (`updateNetworkConfig` simpleNetCfg) <$> lift getNetworkConfig - void . processChatCommand vr nm $ APISetNetworkConfig cfg + void . processChatCommand cxt nm $ APISetNetworkConfig cfg pure $ CRNetworkConfig cfg APISetNetworkInfo info -> lift (withAgent' (`setUserNetworkInfo` info)) >> ok_ ReconnectAllServers -> withUser' $ \_ -> lift (withAgent' reconnectAllServers) >> ok_ @@ -1758,7 +1758,7 @@ processChatCommand vr nm = \case APISetChatSettings (ChatRef cType chatId scope) chatSettings -> withUser $ \user -> case cType of CTDirect -> do ct <- withFastStore $ \db -> do - ct <- getContact db vr user chatId + ct <- getContact db cxt user chatId liftIO $ updateContactSettings db user chatId chatSettings pure ct forM_ (contactConnId ct) $ \connId -> @@ -1766,7 +1766,7 @@ processChatCommand vr nm = \case ok user CTGroup | isNothing scope -> do ms <- withFastStore $ \db -> do - gInfo <- getGroupInfo db vr user chatId + gInfo <- getGroupInfo db cxt user chatId ms <- liftIO $ getMembers db gInfo liftIO $ updateGroupSettings db user chatId chatSettings pure ms @@ -1775,19 +1775,19 @@ processChatCommand vr nm = \case ok user where getMembers db gInfo - | useRelays' gInfo = getGroupRelayMembers db vr user gInfo - | otherwise = getGroupMembers db vr user gInfo + | useRelays' gInfo = getGroupRelayMembers db cxt user gInfo + | otherwise = getGroupMembers db cxt user gInfo _ -> throwCmdError "not supported" APISetMemberSettings gId gMemberId settings -> withUser $ \user -> do m <- withFastStore $ \db -> do liftIO $ updateGroupMemberSettings db user gId gMemberId settings - getGroupMember db vr user gId gMemberId + getGroupMember db cxt user gId gMemberId let ntfOn = not (memberBlocked m) toggleNtf m ntfOn ok user APIContactInfo contactId -> withUser $ \user@User {userId} -> do -- [incognito] print user's incognito profile for this contact - ct@Contact {activeConn} <- withFastStore $ \db -> getContact db vr user contactId + ct@Contact {activeConn} <- withFastStore $ \db -> getContact db cxt user contactId incognitoProfile <- case activeConn of Nothing -> pure Nothing Just Connection {customUserProfileId} -> @@ -1795,14 +1795,14 @@ processChatCommand vr nm = \case connectionStats <- mapM (withAgent . flip getConnectionServers) (contactConnId ct) pure $ CRContactInfo user ct connectionStats (fmap fromLocalProfile incognitoProfile) APIContactQueueInfo contactId -> withUser $ \user -> do - ct@Contact {activeConn} <- withFastStore $ \db -> getContact db vr user contactId + ct@Contact {activeConn} <- withFastStore $ \db -> getContact db cxt user contactId case activeConn of Just conn -> getConnQueueInfo user conn Nothing -> throwChatError $ CEContactNotActive ct APIGroupInfo gId -> withUser $ \user -> - CRGroupInfo user <$> withFastStore (\db -> getGroupInfo db vr user gId) + CRGroupInfo user <$> withFastStore (\db -> getGroupInfo db cxt user gId) APIGetUpdatedGroupLinkData groupId -> withUser $ \user -> do - gInfo@GroupInfo {groupProfile = p, groupSummary = GroupSummary {publicMemberCount = localCount}} <- withFastStore $ \db -> getGroupInfo db vr user groupId + gInfo@GroupInfo {groupProfile = p, groupSummary = GroupSummary {publicMemberCount = localCount}} <- withFastStore $ \db -> getGroupInfo db cxt user groupId case p of GroupProfile {publicGroup = Just PublicGroupProfile {groupLink = sLnk}} | useRelays' gInfo -> do (_, cData@(ContactLinkData _ UserContactData {relays = currentRelayLinks})) <- getShortLinkConnReq' nm user sLnk @@ -1816,44 +1816,44 @@ processChatCommand vr nm = \case pure $ CRGroupInfo user gInfo' _ -> throwCmdError "group link data not available" APIGroupMemberInfo gId gMemberId -> withUser $ \user -> do - (g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId + (g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db cxt user gId <*> getGroupMember db cxt user gId gMemberId connectionStats <- mapM (withAgent . flip getConnectionServers) (memberConnId m) pure $ CRGroupMemberInfo user g m connectionStats APIGroupMemberQueueInfo gId gMemberId -> withUser $ \user -> do - GroupMember {activeConn} <- withFastStore $ \db -> getGroupMember db vr user gId gMemberId + GroupMember {activeConn} <- withFastStore $ \db -> getGroupMember db cxt user gId gMemberId case activeConn of Just conn -> getConnQueueInfo user conn Nothing -> throwChatError CEGroupMemberNotActive APISwitchContact contactId -> withUser $ \user -> do - ct <- withFastStore $ \db -> getContact db vr user contactId + ct <- withFastStore $ \db -> getContact db cxt user contactId case contactConnId ct of Just connId -> do connectionStats <- withAgent $ \a -> switchConnectionAsync a "" connId pure $ CRContactSwitchStarted user ct connectionStats Nothing -> throwChatError $ CEContactNotActive ct APISwitchGroupMember gId gMemberId -> withUser $ \user -> do - (g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId + (g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db cxt user gId <*> getGroupMember db cxt user gId gMemberId case memberConnId m of Just connId -> do connectionStats <- withAgent (\a -> switchConnectionAsync a "" connId) pure $ CRGroupMemberSwitchStarted user g m connectionStats _ -> throwChatError CEGroupMemberNotActive APIAbortSwitchContact contactId -> withUser $ \user -> do - ct <- withFastStore $ \db -> getContact db vr user contactId + ct <- withFastStore $ \db -> getContact db cxt user contactId case contactConnId ct of Just connId -> do connectionStats <- withAgent $ \a -> abortConnectionSwitch a connId pure $ CRContactSwitchAborted user ct connectionStats Nothing -> throwChatError $ CEContactNotActive ct APIAbortSwitchGroupMember gId gMemberId -> withUser $ \user -> do - (g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId + (g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db cxt user gId <*> getGroupMember db cxt user gId gMemberId case memberConnId m of Just connId -> do connectionStats <- withAgent $ \a -> abortConnectionSwitch a connId pure $ CRGroupMemberSwitchAborted user g m connectionStats _ -> throwChatError CEGroupMemberNotActive APISyncContactRatchet contactId force -> withUser $ \user -> withContactLock "syncContactRatchet" contactId $ do - ct <- withFastStore $ \db -> getContact db vr user contactId + ct <- withFastStore $ \db -> getContact db cxt user contactId case contactConn ct of Just conn@Connection {pqSupport} -> do cStats@ConnectionStats {ratchetSyncState = rss} <- withAgent $ \a -> synchronizeRatchet a (aConnId conn) pqSupport force @@ -1861,7 +1861,7 @@ processChatCommand vr nm = \case pure $ CRContactRatchetSyncStarted user ct cStats Nothing -> throwChatError $ CEContactNotActive ct APISyncGroupMemberRatchet gId gMemberId force -> withUser $ \user -> withGroupLock "syncGroupMemberRatchet" gId $ do - (g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId + (g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db cxt user gId <*> getGroupMember db cxt user gId gMemberId case memberConnId m of Just connId -> do cStats@ConnectionStats {ratchetSyncState = rss} <- withAgent $ \a -> synchronizeRatchet a connId PQSupportOff force @@ -1870,7 +1870,7 @@ processChatCommand vr nm = \case pure $ CRGroupMemberRatchetSyncStarted user g' m' cStats _ -> throwChatError CEGroupMemberNotActive APIGetContactCode contactId -> withUser $ \user -> do - ct@Contact {activeConn} <- withFastStore $ \db -> getContact db vr user contactId + ct@Contact {activeConn} <- withFastStore $ \db -> getContact db cxt user contactId case activeConn of Just conn@Connection {connId} -> do code <- getConnectionCode $ aConnId conn @@ -1884,7 +1884,7 @@ processChatCommand vr nm = \case pure $ CRContactCode user ct' code Nothing -> throwChatError $ CEContactNotActive ct APIGetGroupMemberCode gId gMemberId -> withUser $ \user -> do - (g, m@GroupMember {activeConn}) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId + (g, m@GroupMember {activeConn}) <- withFastStore $ \db -> (,) <$> getGroupInfo db cxt user gId <*> getGroupMember db cxt user gId gMemberId case activeConn of Just conn@Connection {connId} -> do code <- getConnectionCode $ aConnId conn @@ -1898,24 +1898,24 @@ processChatCommand vr nm = \case pure $ CRGroupMemberCode user g m' code _ -> throwChatError CEGroupMemberNotActive APIVerifyContact contactId code -> withUser $ \user -> do - ct@Contact {activeConn} <- withFastStore $ \db -> getContact db vr user contactId + ct@Contact {activeConn} <- withFastStore $ \db -> getContact db cxt user contactId case activeConn of Just conn -> verifyConnectionCode user conn code Nothing -> throwChatError $ CEContactNotActive ct APIVerifyGroupMember gId gMemberId code -> withUser $ \user -> do - GroupMember {activeConn} <- withFastStore $ \db -> getGroupMember db vr user gId gMemberId + GroupMember {activeConn} <- withFastStore $ \db -> getGroupMember db cxt user gId gMemberId case activeConn of Just conn -> verifyConnectionCode user conn code _ -> throwChatError CEGroupMemberNotActive APIEnableContact contactId -> withUser $ \user -> do - ct@Contact {activeConn} <- withFastStore $ \db -> getContact db vr user contactId + ct@Contact {activeConn} <- withFastStore $ \db -> getContact db cxt user contactId case activeConn of Just conn -> do withFastStore' $ \db -> setAuthErrCounter db user conn 0 ok user Nothing -> throwChatError $ CEContactNotActive ct APIEnableGroupMember gId gMemberId -> withUser $ \user -> do - GroupMember {activeConn} <- withFastStore $ \db -> getGroupMember db vr user gId gMemberId + GroupMember {activeConn} <- withFastStore $ \db -> getGroupMember db cxt user gId gMemberId case activeConn of Just conn -> do withFastStore' $ \db -> setAuthErrCounter db user conn 0 @@ -1925,16 +1925,16 @@ processChatCommand vr nm = \case SetSendReceipts cName rcptsOn_ -> updateChatSettings cName (\cs -> cs {sendRcpts = rcptsOn_}) SetShowMemberMessages gName mName showMessages -> withUser $ \user -> do (gId, mId) <- getGroupAndMemberId user gName mName - gInfo <- withFastStore $ \db -> getGroupInfo db vr user gId - m <- withFastStore $ \db -> getGroupMember db vr user gId mId + gInfo <- withFastStore $ \db -> getGroupInfo db cxt user gId + m <- withFastStore $ \db -> getGroupMember db cxt user gId mId let GroupInfo {membership = GroupMember {memberRole = membershipRole}} = gInfo when (membershipRole >= GRModerator) $ throwChatError $ CECantBlockMemberForSelf gInfo m showMessages let settings = (memberSettings m) {showMessages} - processChatCommand vr nm $ APISetMemberSettings gId mId settings + processChatCommand cxt nm $ APISetMemberSettings gId mId settings ContactInfo cName -> withContactName cName APIContactInfo ShowGroupInfo gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand vr nm $ APIGroupInfo groupId + processChatCommand cxt nm $ APIGroupInfo groupId GroupMemberInfo gName mName -> withMemberName gName mName APIGroupMemberInfo ContactQueueInfo cName -> withContactName cName APIContactQueueInfo GroupMemberQueueInfo gName mName -> withMemberName gName mName APIGroupMemberQueueInfo @@ -1964,7 +1964,7 @@ processChatCommand vr nm = \case conn <- withFastStore' $ \db -> createDirectConnection db user connId ccLink' Nothing ConnNew incognitoProfile subMode initialChatVersion PQSupportOn pure $ CRInvitation user ccLink' conn AddContact incognito -> withUser $ \User {userId} -> - processChatCommand vr nm $ APIAddContact userId incognito + processChatCommand cxt nm $ APIAddContact userId incognito APISetConnectionIncognito connId incognito -> withUser $ \user@User {userId} -> do conn <- withFastStore $ \db -> getPendingContactConnection db userId connId let PendingContactConnection {pccConnStatus, customUserProfileId} = conn @@ -2021,7 +2021,7 @@ processChatCommand vr nm = \case groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs preferences groupProfile = businessGroupProfile profile groupPreferences gVar <- asks random - (gInfo, hostMember_) <- withStore $ \db -> createPreparedGroup db gVar vr user groupProfile True ccLink welcomeSharedMsgId False GRMember Nothing Nothing + (gInfo, hostMember_) <- withStore $ \db -> createPreparedGroup db gVar cxt user groupProfile True ccLink welcomeSharedMsgId False GRMember Nothing Nothing hostMember <- maybe (throwCmdError "no host member") pure hostMember_ void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing (Just epochStart) let cd = CDGroupRcv gInfo Nothing hostMember @@ -2034,7 +2034,7 @@ processChatCommand vr nm = \case _ -> Chat cInfo [] emptyChatStats pure $ CRNewPreparedChat user $ AChat SCTGroup chat ACCL _ (CCLink cReq _) -> do - (ct, displaced_) <- withStore $ \db -> createPreparedContact db vr user profile accLink welcomeSharedMsgId Nothing + (ct, displaced_) <- withStore $ \db -> createPreparedContact db cxt user profile accLink welcomeSharedMsgId Nothing let Profile {simplexName = pSimplexName} = profile Contact {localDisplayName = newLDN} = ct surfaceSimplexNameConflict user pSimplexName displaced_ SNCEContact newLDN @@ -2056,7 +2056,7 @@ processChatCommand vr nm = \case let useRelays = not direct subRole <- if useRelays then asks $ channelSubscriberRole . config else pure GRMember gVar <- asks random - (gInfo, hostMember_) <- withStore $ \db -> createPreparedGroup db gVar vr user gp False ccLink welcomeSharedMsgId useRelays subRole publicMemberCount_ Nothing + (gInfo, hostMember_) <- withStore $ \db -> createPreparedGroup db gVar cxt user gp False ccLink welcomeSharedMsgId useRelays subRole publicMemberCount_ Nothing void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing (Just epochStart) let cd = maybe (CDChannelRcv gInfo Nothing) (CDGroupRcv gInfo Nothing) hostMember_ cInfo = GroupChat gInfo Nothing @@ -2067,40 +2067,40 @@ processChatCommand vr nm = \case _ -> Chat cInfo [] emptyChatStats pure $ CRNewPreparedChat user $ AChat SCTGroup chat APIChangePreparedContactUser contactId newUserId -> withUser $ \user -> do - ct@Contact {preparedContact} <- withFastStore $ \db -> getContact db vr user contactId + ct@Contact {preparedContact} <- withFastStore $ \db -> getContact db cxt user contactId when (isNothing preparedContact) $ throwCmdError "contact doesn't have link to connect" when (isJust $ contactConn ct) $ throwCmdError "contact already has connection" newUser <- privateGetUser newUserId - ct' <- withFastStore $ \db -> updatePreparedContactUser db vr user ct newUser + ct' <- withFastStore $ \db -> updatePreparedContactUser db cxt user ct newUser -- create changed feature items (new user may have different preferences) lift $ createContactChangedFeatureItems user ct ct' pure $ CRContactUserChanged user ct newUser ct' APIChangePreparedGroupUser groupId newUserId -> withUser $ \user -> do - gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId + gInfo <- withFastStore $ \db -> getGroupInfo db cxt user groupId when (isNothing $ preparedGroup gInfo) $ throwCmdError "group doesn't have link to connect" hostMember_ <- if useRelays' gInfo then pure Nothing else do - hostMember <- withFastStore $ \db -> getHostMember db vr user groupId + hostMember <- withFastStore $ \db -> getHostMember db cxt user groupId when (isJust $ memberConn hostMember) $ throwCmdError "host member already has connection" pure $ Just hostMember newUser <- privateGetUser newUserId - gInfo' <- withFastStore $ \db -> updatePreparedGroupUser db vr user gInfo hostMember_ newUser + gInfo' <- withFastStore $ \db -> updatePreparedGroupUser db cxt user gInfo hostMember_ newUser pure $ CRGroupUserChanged user gInfo newUser gInfo' APIConnectPreparedContact contactId incognito msgContent_ -> withUser $ \user -> do - ct@Contact {preparedContact} <- withFastStore $ \db -> getContact db vr user contactId + ct@Contact {preparedContact} <- withFastStore $ \db -> getContact db cxt user contactId case preparedContact of Nothing -> throwCmdError "contact doesn't have link to connect" Just PreparedContact {connLinkToConnect = ACCL SCMInvitation ccLink} -> do (_, customUserProfile) <- connectViaInvitation user incognito ccLink (Just contactId) `catchAllErrors` \e -> do -- get updated contact, in case connection was started - in UI it would lock ability to change -- user or incognito profile for contact, in case server received request while client got network error - ct' <- withFastStore $ \db -> getContact db vr user contactId + ct' <- withFastStore $ \db -> getContact db cxt user contactId toView $ CEvtChatInfoUpdated user (AChatInfo SCTDirect $ DirectChat ct') throwError e -- get updated contact with connection - ct' <- withFastStore $ \db -> getContact db vr user contactId + ct' <- withFastStore $ \db -> getContact db cxt user contactId -- create changed feature items (connecting incognito sends default preferences, instead of user preferences) lift . when incognito $ createContactChangedFeatureItems user ct ct' forM_ msgContent_ $ \mc -> do @@ -2119,13 +2119,13 @@ processChatCommand vr nm = \case r <- connectViaContact user (Just $ PCEContact ct) incognito ccLink welcomeSharedMsgId msg_ `catchAllErrors` \e -> do -- get updated contact, in case connection was started - in UI it would lock ability to change -- user or incognito profile for contact, in case server received request while client got network error - ct' <- withFastStore $ \db -> getContact db vr user contactId + ct' <- withFastStore $ \db -> getContact db cxt user contactId toView $ CEvtChatInfoUpdated user (AChatInfo SCTDirect $ DirectChat ct') throwError e case r of CVRSentInvitation _conn customUserProfile -> do -- get updated contact with connection - ct' <- withFastStore $ \db -> getContact db vr user contactId + ct' <- withFastStore $ \db -> getContact db cxt user contactId -- create changed feature items (connecting incognito sends default preferences, instead of user preferences) lift . when incognito $ createContactChangedFeatureItems user ct ct' forM_ msg_ $ \(sharedMsgId, mc) -> do @@ -2134,7 +2134,7 @@ processChatCommand vr nm = \case pure $ CRStartedConnectionToContact user ct' customUserProfile CVRConnectedContact ct' -> pure $ CRContactAlreadyExists user ct' APIConnectPreparedGroup {groupId, incognito, ownerContact, msgContent_} -> withUser $ \user -> do - gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId + gInfo <- withFastStore $ \db -> getGroupInfo db cxt user groupId case gInfo of GroupInfo {preparedGroup = Nothing} -> throwCmdError "group doesn't have link to connect" GroupInfo {useRelays = BoolDef True, preparedGroup = Just PreparedGroup {connLinkToConnect}} -> do @@ -2157,14 +2157,14 @@ processChatCommand vr nm = \case gVar <- asks random (_, memberPrivKey) <- liftIO $ atomically $ C.generateKeyPair gVar gInfo' <- withFastStore $ \db -> do - gInfo' <- updatePreparedRelayedGroup db vr user gInfo mainCReq cReqHash incognitoProfile rootKey memberPrivKey publicMemberCount_ + gInfo' <- updatePreparedRelayedGroup db cxt user gInfo mainCReq cReqHash incognitoProfile rootKey memberPrivKey publicMemberCount_ -- Pre-emptively create owner members with trusted keys from link data forM_ owners $ \OwnerAuth {ownerId, ownerKey} -> do let ctId_ = case ownerContact of Just GroupOwnerContact {contactId, memberId} | memberId == MemberId ownerId -> Just contactId _ -> Nothing - void $ createLinkOwnerMember db vr user gInfo' ctId_ (MemberId ownerId) ownerKey + void $ createLinkOwnerMember db cxt user gInfo' ctId_ (MemberId ownerId) ownerKey pure gInfo' rs <- withGroupLock "connectPreparedGroup" groupId $ mapConcurrently (connectToRelay user gInfo') relays @@ -2182,7 +2182,7 @@ processChatCommand vr nm = \case else do gInfo'' <- withFastStore $ \db -> do liftIO $ setPreparedGroupStartedConnection db groupId - getGroupInfo db vr user groupId + getGroupInfo db cxt user groupId -- Async retry failed relays with temporary errors let retryable = [(l, m) | r@(l, m, _) <- failed, isTempErr r] void $ mapConcurrently (uncurry $ retryRelayConnectionAsync gInfo') retryable @@ -2202,7 +2202,7 @@ processChatCommand vr nm = \case newConnIds <- getAgentConnShortLinkAsync user CFGetRelayDataJoin Nothing relayLink withStore' $ \db -> createRelayMemberConnectionAsync db user gInfo' relayMember relayLink newConnIds subMode GroupInfo {preparedGroup = Just PreparedGroup {connLinkToConnect, welcomeSharedMsgId, requestSharedMsgId}} -> do - hostMember <- withFastStore $ \db -> getHostMember db vr user groupId + hostMember <- withFastStore $ \db -> getHostMember db cxt user groupId msg_ <- forM msgContent_ $ \mc -> case requestSharedMsgId of Just smId -> pure (smId, mc) Nothing -> do @@ -2212,7 +2212,7 @@ processChatCommand vr nm = \case r <- connectViaContact user (Just $ PCEGroup gInfo hostMember) incognito connLinkToConnect welcomeSharedMsgId msg_ `catchAllErrors` \e -> do -- get updated group info, in case connection was started (connLinkPreparedConnection) - in UI it would lock ability to change -- user or incognito profile for group or business chat, in case server received request while client got network error - gInfo' <- withFastStore $ \db -> getGroupInfo db vr user groupId + gInfo' <- withFastStore $ \db -> getGroupInfo db cxt user groupId toView $ CEvtChatInfoUpdated user (AChatInfo SCTGroup $ GroupChat gInfo' Nothing) throwError e case r of @@ -2220,7 +2220,7 @@ processChatCommand vr nm = \case -- get updated group info (connLinkStartedConnection and incognito membership) gInfo' <- withFastStore $ \db -> do liftIO $ setPreparedGroupStartedConnection db groupId - getGroupInfo db vr user groupId + getGroupInfo db cxt user groupId forM_ msg_ $ \(sharedMsgId, mc) -> do ci <- createChatItem user (CDGroupSnd gInfo' Nothing) False (CISndMsgContent mc) (Just sharedMsgId) Nothing toView $ CEvtNewChatItems user [ci] @@ -2250,7 +2250,7 @@ processChatCommand vr nm = \case Connect _ Nothing -> throwChatError CEInvalidConnReq APIVerifySimplexName chatRef -> withUser $ \user -> apiVerifySimplexName user chatRef APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do - ct@Contact {profile = LocalProfile {contactLink}} <- withFastStore $ \db -> getContact db vr user contactId + ct@Contact {profile = LocalProfile {contactLink}} <- withFastStore $ \db -> getContact db cxt user contactId ccLink <- case contactLink of Just (CLFull cReq) -> pure $ CCLink cReq Nothing Just (CLShort sLnk) -> do @@ -2260,7 +2260,7 @@ processChatCommand vr nm = \case connectContactViaAddress user incognito ct ccLink `catchAllErrors` \e -> do -- get updated contact, in case connection was started - in UI it would lock ability to change incognito choice -- on next connection attempt, in case server received request while client got network error - ct' <- withFastStore $ \db -> getContact db vr user contactId + ct' <- withFastStore $ \db -> getContact db cxt user contactId toView $ CEvtChatInfoUpdated user (AChatInfo SCTDirect $ DirectChat ct') throwError e ConnectSimplex incognito -> withUser $ \user -> do @@ -2269,9 +2269,9 @@ processChatCommand vr nm = \case DeleteContact cName cdm -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId Nothing) cdm ClearContact cName -> withContactName cName $ \chatId -> APIClearChat $ ChatRef CTDirect chatId Nothing APIListContacts userId -> withUserId userId $ \user -> - CRContactsList user <$> withFastStore' (\db -> getUserContacts db vr user) + CRContactsList user <$> withFastStore' (\db -> getUserContacts db cxt user) ListContacts -> withUser $ \User {userId} -> - processChatCommand vr nm $ APIListContacts userId + processChatCommand cxt nm $ APIListContacts userId APICreateMyAddress userId -> withUserId userId $ \user@User {userChatRelay} -> do withFastStore' (\db -> runExceptT $ getUserAddress db user) >>= \case Left SEUserContactLinkNotFound -> pure () @@ -2289,9 +2289,9 @@ processChatCommand vr nm = \case withFastStore $ \db -> createUserContactLink db user connId ccLink'' subMode pure $ CRUserContactLinkCreated user ccLink'' CreateMyAddress -> withUser $ \User {userId} -> - processChatCommand vr nm $ APICreateMyAddress userId + processChatCommand cxt nm $ APICreateMyAddress userId APIDeleteMyAddress userId -> withUserId userId $ \user@User {profile = p} -> do - conn <- withFastStore $ \db -> getUserAddressConnection db vr user + conn <- withFastStore $ \db -> getUserAddressConnection db cxt user withChatLock "deleteMyAddress" $ do deleteAgentConnectionAsync $ aConnId conn withFastStore' (`deleteUserAddress` user) @@ -2302,11 +2302,11 @@ processChatCommand vr nm = \case _ -> user pure $ CRUserContactLinkDeleted user' DeleteMyAddress -> withUser $ \User {userId} -> - processChatCommand vr nm $ APIDeleteMyAddress userId + processChatCommand cxt nm $ APIDeleteMyAddress userId APIShowMyAddress userId -> withUserId' userId $ \user -> CRUserContactLink user <$> withFastStore (`getUserAddress` user) ShowMyAddress -> withUser' $ \User {userId} -> - processChatCommand vr nm $ APIShowMyAddress userId + processChatCommand cxt nm $ APIShowMyAddress userId APIAddMyAddressShortLink userId -> withUserId' userId $ \user -> CRUserContactLink user <$> (withFastStore (`getUserAddress` user) >>= setMyAddressData user) APISetProfileAddress userId False -> withUserId userId $ \user@User {profile = p} -> do @@ -2318,7 +2318,7 @@ processChatCommand vr nm = \case let p' = (fromLocalProfile p :: Profile) {contactLink = Just $ profileContactLink ucl} updateProfile_ user p' True $ withFastStore' $ \db -> setUserProfileContactLink db user $ Just ucl SetProfileAddress onOff -> withUser $ \User {userId} -> - processChatCommand vr nm $ APISetProfileAddress userId onOff + processChatCommand cxt nm $ APISetProfileAddress userId onOff APISetAddressSettings userId settings@AddressSettings {businessAddress, autoAccept} -> withUserId userId $ \user -> do ucl@UserContactLink {userContactLinkId, shortLinkDataSet, addressSettings} <- withFastStore (`getUserAddress` user) forM_ autoAccept $ \AutoAccept {acceptIncognito} -> do @@ -2332,43 +2332,43 @@ processChatCommand vr nm = \case withFastStore' $ \db -> updateUserAddressSettings db userContactLinkId settings pure $ CRUserContactLinkUpdated user ucl'' SetAddressSettings settings -> withUser $ \User {userId} -> - processChatCommand vr nm $ APISetAddressSettings userId settings + processChatCommand cxt nm $ APISetAddressSettings userId settings AcceptContact incognito cName -> withUser $ \User {userId} -> do connReqId <- withFastStore $ \db -> getContactRequestIdByName db userId cName - processChatCommand vr nm $ APIAcceptContact incognito connReqId + processChatCommand cxt nm $ APIAcceptContact incognito connReqId RejectContact cName -> withUser $ \User {userId} -> do connReqId <- withFastStore $ \db -> getContactRequestIdByName db userId cName - processChatCommand vr nm $ APIRejectContact connReqId + processChatCommand cxt nm $ APIRejectContact connReqId ForwardMessage toChatName fromContactName forwardedMsg -> withUser $ \user -> do contactId <- withFastStore $ \db -> getContactIdByName db user fromContactName forwardedItemId <- withFastStore $ \db -> getDirectChatItemIdByText' db user contactId forwardedMsg toChatRef <- getChatRef user toChatName asGroup <- getSendAsGroup user toChatRef - processChatCommand vr nm $ APIForwardChatItems toChatRef asGroup (ChatRef CTDirect contactId Nothing) (forwardedItemId :| []) Nothing + processChatCommand cxt nm $ APIForwardChatItems toChatRef asGroup (ChatRef CTDirect contactId Nothing) (forwardedItemId :| []) Nothing ForwardGroupMessage toChatName fromGroupName fromMemberName_ forwardedMsg -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user fromGroupName forwardedItemId <- withFastStore $ \db -> getGroupChatItemIdByText db user groupId fromMemberName_ forwardedMsg toChatRef <- getChatRef user toChatName asGroup <- getSendAsGroup user toChatRef - processChatCommand vr nm $ APIForwardChatItems toChatRef asGroup (ChatRef CTGroup groupId Nothing) (forwardedItemId :| []) Nothing + processChatCommand cxt nm $ APIForwardChatItems toChatRef asGroup (ChatRef CTGroup groupId Nothing) (forwardedItemId :| []) Nothing ForwardLocalMessage toChatName forwardedMsg -> withUser $ \user -> do folderId <- withFastStore (`getUserNoteFolderId` user) forwardedItemId <- withFastStore $ \db -> getLocalChatItemIdByText' db user folderId forwardedMsg toChatRef <- getChatRef user toChatName asGroup <- getSendAsGroup user toChatRef - processChatCommand vr nm $ APIForwardChatItems toChatRef asGroup (ChatRef CTLocal folderId Nothing) (forwardedItemId :| []) Nothing + processChatCommand cxt nm $ APIForwardChatItems toChatRef asGroup (ChatRef CTLocal folderId Nothing) (forwardedItemId :| []) Nothing SharePublicGroup shareGroupName toChatName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user shareGroupName toChatRef <- getChatRef user toChatName sendRef <- case toChatRef of ChatRef CTDirect ctId _ -> pure $ SRDirect ctId ChatRef CTGroup gId scope_ -> do - gInfo <- withFastStore $ \db -> getGroupInfo db vr user gId + gInfo <- withFastStore $ \db -> getGroupInfo db cxt user gId pure $ SRGroup gId scope_ (useRelays' gInfo) _ -> throwCmdError "unsupported share target" - processChatCommand vr nm (APIShareChatMsgContent (ChatRef CTGroup groupId Nothing) sendRef) >>= \case + processChatCommand cxt nm (APIShareChatMsgContent (ChatRef CTGroup groupId Nothing) sendRef) >>= \case CRChatMsgContent _ mc -> - processChatCommand vr nm $ APISendMessages sendRef False Nothing [composedMessage Nothing mc] + processChatCommand cxt nm $ APISendMessages sendRef False Nothing [composedMessage Nothing mc] r -> pure r SendMessage sendName msg -> withUser $ \user -> do let mc = MCText msg @@ -2377,57 +2377,57 @@ processChatCommand vr nm = \case withFastStore' (\db -> runExceptT $ getContactIdByName db user name) >>= \case Right ctId -> do let sendRef = SRDirect ctId - processChatCommand vr nm $ APISendMessages sendRef False Nothing [composedMessage Nothing mc] + processChatCommand cxt nm $ APISendMessages sendRef False Nothing [composedMessage Nothing mc] Left _ -> - withFastStore' (\db -> runExceptT $ getActiveMembersByName db vr user name) >>= \case + withFastStore' (\db -> runExceptT $ getActiveMembersByName db cxt user name) >>= \case Right [(gInfo, member)] -> do let GroupInfo {localDisplayName = gName} = gInfo GroupMember {localDisplayName = mName} = member - processChatCommand vr nm $ SendMemberContactMessage gName mName msg + processChatCommand cxt nm $ SendMemberContactMessage gName mName msg Right (suspectedMember : _) -> throwChatError $ CEContactNotFound name (Just suspectedMember) _ -> throwChatError $ CEContactNotFound name Nothing SNGroup name scope_ -> do (gInfo, cScope_, mentions) <- withFastStore $ \db -> do - gInfo <- getGroupInfoByName db vr user name + gInfo <- getGroupInfoByName db cxt user name let gId = groupId' gInfo cScope_ <- forM scope_ $ \(GSNMemberSupport mName_) -> GCSMemberSupport <$> mapM (getGroupMemberIdByName db user gId) mName_ (gInfo, cScope_,) <$> liftIO (getMessageMentions db user gId msg) let sendRef = SRGroup (groupId' gInfo) cScope_ (sendAsGroup' gInfo cScope_) - processChatCommand vr nm $ APISendMessages sendRef False Nothing [ComposedMessage Nothing Nothing mc mentions] + processChatCommand cxt nm $ APISendMessages sendRef False Nothing [ComposedMessage Nothing Nothing mc mentions] SNLocal -> do folderId <- withFastStore (`getUserNoteFolderId` user) - processChatCommand vr nm $ APICreateChatItems folderId [composedMessage Nothing mc] + processChatCommand cxt nm $ APICreateChatItems folderId [composedMessage Nothing mc] SendMemberContactMessage gName mName msg -> withUser $ \user -> do (gId, mId) <- getGroupAndMemberId user gName mName - m <- withFastStore $ \db -> getGroupMember db vr user gId mId + m <- withFastStore $ \db -> getGroupMember db cxt user gId mId let mc = MCText msg case memberContactId m of Nothing -> do - g <- withFastStore $ \db -> getGroupInfo db vr user gId + g <- withFastStore $ \db -> getGroupInfo db cxt user gId unless (groupFeatureUserAllowed SGFDirectMessages g) $ throwCmdError "direct messages not allowed" toView $ CEvtNoMemberContactCreating user g m - processChatCommand vr nm (APICreateMemberContact gId mId) >>= \case + processChatCommand cxt nm (APICreateMemberContact gId mId) >>= \case CRNewMemberContact _ ct@Contact {contactId} _ _ -> do toViewTE $ TENewMemberContact user ct g m - processChatCommand vr nm $ APISendMemberContactInvitation contactId (Just mc) + processChatCommand cxt nm $ APISendMemberContactInvitation contactId (Just mc) cr -> pure cr Just ctId -> do let sendRef = SRDirect ctId - processChatCommand vr nm $ APISendMessages sendRef False Nothing [composedMessage Nothing mc] + processChatCommand cxt nm $ APISendMessages sendRef False Nothing [composedMessage Nothing mc] AcceptMemberContact cName -> withUser $ \user -> do contactId <- withFastStore $ \db -> getContactIdByName db user cName - processChatCommand vr nm $ APIAcceptMemberContact contactId + processChatCommand cxt nm $ APIAcceptMemberContact contactId SendLiveMessage chatName msg -> withUser $ \user -> do (chatRef, mentions) <- getChatRefAndMentions user chatName msg withSendRef user chatRef $ \sendRef -> do let mc = MCText msg - processChatCommand vr nm $ APISendMessages sendRef True Nothing [ComposedMessage Nothing Nothing mc mentions] + processChatCommand cxt nm $ APISendMessages sendRef True Nothing [ComposedMessage Nothing Nothing mc mentions] SendMessageBroadcast mc -> withUser $ \user -> do - contacts <- withFastStore' $ \db -> getUserContacts db vr user + contacts <- withFastStore' $ \db -> getUserContacts db cxt user withChatLock "sendMessageBroadcast" $ do let ctConns_ = L.nonEmpty $ foldr addContactConn [] contacts case ctConns_ of @@ -2470,28 +2470,28 @@ processChatCommand vr nm = \case contactId <- withFastStore $ \db -> getContactIdByName db user cName quotedItemId <- withFastStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir quotedMsg let mc = MCText msg - processChatCommand vr nm $ APISendMessages (SRDirect contactId) False Nothing [ComposedMessage Nothing (Just quotedItemId) mc M.empty] + processChatCommand cxt nm $ APISendMessages (SRDirect contactId) False Nothing [ComposedMessage Nothing (Just quotedItemId) mc M.empty] DeleteMessage chatName deletedMsg -> withUser $ \user -> do chatRef <- getChatRef user chatName deletedItemId <- getSentChatItemIdByText user chatRef deletedMsg - processChatCommand vr nm $ APIDeleteChatItem chatRef (deletedItemId :| []) CIDMBroadcast + processChatCommand cxt nm $ APIDeleteChatItem chatRef (deletedItemId :| []) CIDMBroadcast DeleteMemberMessage gName mName deletedMsg -> withUser $ \user -> do gId <- withFastStore $ \db -> getGroupIdByName db user gName deletedItemId <- withFastStore $ \db -> getGroupChatItemIdByText db user gId (Just mName) deletedMsg - processChatCommand vr nm $ APIDeleteMemberChatItem gId (deletedItemId :| []) + processChatCommand cxt nm $ APIDeleteMemberChatItem gId (deletedItemId :| []) EditMessage chatName editedMsg msg -> withUser $ \user -> do (chatRef, mentions) <- getChatRefAndMentions user chatName msg editedItemId <- getSentChatItemIdByText user chatRef editedMsg let mc = MCText msg - processChatCommand vr nm $ APIUpdateChatItem chatRef editedItemId False $ UpdatedMessage mc mentions + processChatCommand cxt nm $ APIUpdateChatItem chatRef editedItemId False $ UpdatedMessage mc mentions UpdateLiveMessage chatName chatItemId live msg -> withUser $ \user -> do (chatRef, mentions) <- getChatRefAndMentions user chatName msg let mc = MCText msg - processChatCommand vr nm $ APIUpdateChatItem chatRef chatItemId live $ UpdatedMessage mc mentions + processChatCommand cxt nm $ APIUpdateChatItem chatRef chatItemId live $ UpdatedMessage mc mentions ReactToMessage add reaction chatName msg -> withUser $ \user -> do chatRef <- getChatRef user chatName chatItemId <- getChatItemIdByText user chatRef msg - processChatCommand vr nm $ APIChatItemReaction chatRef chatItemId add reaction + processChatCommand cxt nm $ APIChatItemReaction chatRef chatItemId add reaction APINewGroup userId incognito gProfile -> withUserId userId $ \user -> do g <- asks random memberId <- liftIO $ MemberId <$> encodedRandomBytes g 12 @@ -2499,7 +2499,7 @@ processChatCommand vr nm = \case createNewGroupItems user gInfo pure $ CRGroupCreated user gInfo NewGroup incognito gProfile -> withUser $ \User {userId} -> - processChatCommand vr nm $ APINewGroup userId incognito gProfile + processChatCommand cxt nm $ APINewGroup userId incognito gProfile APINewPublicGroup userId incognito relayIds groupProfile -> withUserId userId $ \user -> do (gProfile', memberId, groupKeys, setupLink) <- prepareGroupLink user gInfo <- newGroup user incognito gProfile' True memberId (Just groupKeys) (Just 1) @@ -2559,16 +2559,16 @@ processChatCommand vr nm = \case pure (gLink, results) pure (groupProfile', memberId, groupKeys, setupLink) NewPublicGroup incognito relayIds gProfile -> withUser $ \User {userId} -> - processChatCommand vr nm $ APINewPublicGroup userId incognito relayIds gProfile + processChatCommand cxt nm $ APINewPublicGroup userId incognito relayIds gProfile APIGetGroupRelays groupId -> withUser $ \user -> do (gInfo, relays) <- withFastStore $ \db -> do - gInfo <- getGroupInfo db vr user groupId + gInfo <- getGroupInfo db cxt user groupId relays <- liftIO $ getGroupRelays db gInfo pure (gInfo, relays) pure $ CRGroupRelays user gInfo relays APIAddGroupRelays groupId relayIds -> withUser $ \user -> withGroupLock "addGroupRelays" groupId $ do (gInfo, existingRelays) <- withFastStore $ \db -> do - gi <- getGroupInfo db vr user groupId + gi <- getGroupInfo db cxt user groupId rs <- liftIO $ getGroupRelays db gi pure (gi, rs) assertUserGroupRole gInfo GROwner @@ -2599,7 +2599,7 @@ processChatCommand vr nm = \case _ -> False APIAddMember groupId contactId memRole -> withUser $ \user -> withGroupLock "addMember" groupId $ do -- TODO for large groups: no need to load all members to determine if contact is a member - (group, contact) <- withFastStore $ \db -> (,) <$> getGroup db vr user groupId <*> getContact db vr user contactId + (group, contact) <- withFastStore $ \db -> (,) <$> getGroup db cxt user groupId <*> getContact db cxt user contactId let Group gInfo members = group Contact {localDisplayName = cName} = contact when (useRelays' gInfo) $ throwCmdError "can't invite contact to channel" @@ -2630,8 +2630,8 @@ processChatCommand vr nm = \case APIJoinGroup groupId enableNtfs -> withUser $ \user@User {userId} -> do withGroupLock "joinGroup" groupId $ do (invitation, ct) <- withFastStore $ \db -> do - inv@ReceivedGroupInvitation {fromMember} <- getGroupInvitation db vr user groupId - (inv,) <$> getContactViaMember db vr user fromMember + inv@ReceivedGroupInvitation {fromMember} <- getGroupInvitation db cxt user groupId + (inv,) <$> getContactViaMember db cxt user fromMember let ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership, chatSettings}} = invitation GroupMember {memberId = membershipMemId} = membership Contact {activeConn} = ct @@ -2642,7 +2642,7 @@ processChatCommand vr nm = \case agentConnId <- case memberConn fromMember of Nothing -> do agentConnId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True connRequest PQSupportOff - let chatV = vr `peerConnChatVersion` peerChatVRange + let chatV = vr cxt `peerConnChatVersion` peerChatVRange void $ withFastStore' $ \db -> createMemberConnection db userId fromMember agentConnId chatV peerChatVRange subMode pure agentConnId Just conn -> pure $ aConnId conn @@ -2661,7 +2661,7 @@ processChatCommand vr nm = \case pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing Nothing -> throwChatError $ CEContactNotActive ct APIAcceptMember groupId gmId role -> withUser $ \user@User {userId} -> do - (gInfo, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user groupId <*> getGroupMemberById db vr user gmId + (gInfo, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db cxt user groupId <*> getGroupMemberById db cxt user gmId assertUserGroupRole gInfo $ max GRModerator role case memberStatus m of GSMemPendingApproval | memberCategory m == GCInviteeMember -> do -- only host can approve @@ -2670,14 +2670,14 @@ processChatCommand vr nm = \case Just mConn -> case memberAdmission >>= review of Just MCAll -> do - introduceToModerators vr user gInfo m + introduceToModerators cxt user gInfo m withFastStore' $ \db -> updateGroupMemberStatus db userId m GSMemPendingReview let m' = m {memberStatus = GSMemPendingReview} pure $ CRMemberAccepted user gInfo m' Nothing -> do let msg = XGrpLinkAcpt GAAccepted role (memberId' m) void $ sendDirectMemberMessage mConn msg groupId - introduceToRemaining vr user gInfo m {memberRole = role} + introduceToRemaining cxt user gInfo m {memberRole = role} when (groupFeatureAllowed SGFHistory gInfo) $ sendHistory user gInfo m (m', gInfo') <- withFastStore' $ \db -> do m' <- updateGroupMemberAccepted db user m GSMemConnected role @@ -2692,7 +2692,7 @@ processChatCommand vr nm = \case Nothing -> throwChatError CEGroupMemberNotActive GSMemPendingReview -> do let scope = Just $ GCSMemberSupport $ Just (groupMemberId' m) - modMs <- withFastStore' $ \db -> getGroupModerators db vr user gInfo + modMs <- withFastStore' $ \db -> getGroupModerators db cxt user gInfo let rcpModMs' = filter memberCurrent modMs msg = XGrpLinkAcpt GAAccepted role (memberId' m) void $ sendGroupMessage user gInfo scope ([m] <> rcpModMs') msg @@ -2701,7 +2701,7 @@ processChatCommand vr nm = \case let msg2 = XMsgNew $ mcSimple (MCText acceptedToGroupMessage) void $ sendDirectMemberMessage mConn msg2 groupId when (memberCategory m == GCInviteeMember) $ do - introduceToRemaining vr user gInfo m {memberRole = role} + introduceToRemaining cxt user gInfo m {memberRole = role} when (groupFeatureAllowed SGFHistory gInfo) $ sendHistory user gInfo m (m', gInfo') <- withFastStore' $ \db -> do m' <- updateGroupMemberAccepted db user m newMemberStatus role @@ -2719,7 +2719,7 @@ processChatCommand vr nm = \case _ -> GSMemAnnounced _ -> throwCmdError "member should be pending approval and invitee, or pending review and not invitee" APIDeleteMemberSupportChat groupId gmId -> withUser $ \user -> do - (gInfo, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user groupId <*> getGroupMemberById db vr user gmId + (gInfo, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db cxt user groupId <*> getGroupMemberById db cxt user gmId when (isNothing $ supportChat m) $ throwCmdError "member has no support chat" when (memberPending m) $ throwCmdError "member is pending" (gInfo', m') <- withFastStore' $ \db -> do @@ -2733,7 +2733,7 @@ processChatCommand vr nm = \case APIMembersRole groupId memberIds newRole -> withUser $ \user -> withGroupLock "memberRole" groupId $ do -- TODO [relays] possible optimization is to read only required members + relays - g@(Group gInfo members) <- withFastStore $ \db -> getGroup db vr user groupId + g@(Group gInfo members) <- withFastStore $ \db -> getGroup db cxt user groupId when (selfSelected gInfo) $ throwCmdError "can't change role for self" let (invitedMems, currentMems, unchangedMems, maxRole, anyAdmin, anyPending) = selectMembers members when (length invitedMems + length currentMems + length unchangedMems /= length memberIds) $ throwChatError CEGroupMemberNotFound @@ -2771,7 +2771,7 @@ processChatCommand vr nm = \case where changeRole :: GroupMember -> CM GroupMember changeRole m@GroupMember {groupMemberId, memberContactId, localDisplayName = cName} = do - withFastStore (\db -> (,) <$> mapM (getContact db vr user) memberContactId <*> liftIO (getMemberInvitation db user groupMemberId)) >>= \case + withFastStore (\db -> (,) <$> mapM (getContact db cxt user) memberContactId <*> liftIO (getMemberInvitation db user groupMemberId)) >>= \case (Just ct, Just cReq) -> do sendGrpInvitation user ct gInfo (m :: GroupMember) {memberRole = newRole} cReq withFastStore' $ \db -> updateGroupMemberRole db user m newRole @@ -2803,7 +2803,7 @@ processChatCommand vr nm = \case APIBlockMembersForAll groupId memberIds blockFlag -> withUser $ \user -> withGroupLock "blockForAll" groupId $ do -- TODO [relays] possible optimization is to read only required members + relays - Group gInfo members <- withFastStore $ \db -> getGroup db vr user groupId + Group gInfo members <- withFastStore $ \db -> getGroup db cxt user groupId when (selfSelected gInfo) $ throwCmdError "can't block/unblock self" -- TODO [relays] consider sending restriction to all members (remove filtering), as we do in delivery jobs let (blockMems, remainingMems, maxRole, anyAdmin, anyPending) = selectMembers members @@ -2852,7 +2852,7 @@ processChatCommand vr nm = \case APIRemoveMembers {groupId, groupMemberIds, withMessages} -> withUser $ \user -> withGroupLock "removeMembers" groupId $ do -- TODO [relays] possible optimization is to read only required members + relays - Group gInfo members <- withFastStore $ \db -> getGroup db vr user groupId + Group gInfo members <- withFastStore $ \db -> getGroup db cxt user groupId let (count, invitedMems, pendingApprvMems, pendingRvwMems, currentMems, maxRole, anyAdmin) = selectMembers gmIds members gmIds = S.fromList $ L.toList groupMemberIds memCount = length groupMemberIds @@ -2875,7 +2875,7 @@ processChatCommand vr nm = \case gInfo' <- if useRelays' gInfo then updatePublicGroupData user gInfo - else withFastStore $ \db -> getGroupInfo db vr user groupId + else withFastStore $ \db -> getGroupInfo db cxt user groupId let acis' = map (updateACIGroupInfo gInfo') acis unless (null acis') $ toView $ CEvtNewChatItems user acis' unless (null errs) $ toView $ CEvtChatErrors errs @@ -2951,7 +2951,7 @@ processChatCommand vr nm = \case | groupFeatureUserAllowed SGFFullDelete gInfo = deleteGroupMembersCIs user gInfo ms | otherwise = markGroupMembersCIsDeleted user gInfo ms membership APILeaveGroup groupId -> withUser $ \user@User {userId} -> do - gInfo@GroupInfo {membership} <- withFastStore $ \db -> getGroupInfo db vr user groupId + gInfo@GroupInfo {membership} <- withFastStore $ \db -> getGroupInfo db cxt user groupId filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo withGroupLock "leaveGroup" groupId $ do cancelFilesInProgress user filesInfo @@ -2990,26 +2990,26 @@ processChatCommand vr nm = \case pure msg getRecipients user gInfo | useRelays' gInfo = do - relays <- withFastStore' $ \db -> getGroupRelayMembers db vr user gInfo + relays <- withFastStore' $ \db -> getGroupRelayMembers db cxt user gInfo pure (relays, relays) | otherwise = do - ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo + ms <- withFastStore' $ \db -> getGroupMembers db cxt user gInfo pure (ms, filter memberCurrentOrPending ms) APIListMembers groupId -> withUser $ \user -> - CRGroupMembers user <$> withFastStore (\db -> getGroup db vr user groupId) + CRGroupMembers user <$> withFastStore (\db -> getGroup db cxt user groupId) -- -- validate: prohibit to delete/archive if member is pending (has to communicate approval or rejection) -- APIDeleteGroupConversations groupId _gcId -> withUser $ \user -> do - -- _gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId + -- _gInfo <- withFastStore $ \db -> getGroupInfo db cxt user groupId -- ok_ -- CRGroupConversationsArchived -- APIArchiveGroupConversations groupId _gcId -> withUser $ \user -> do - -- _gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId + -- _gInfo <- withFastStore $ \db -> getGroupInfo db cxt user groupId -- ok_ -- CRGroupConversationsDeleted AddMember gName cName memRole -> withUser $ \user -> do (groupId, contactId) <- withFastStore $ \db -> (,) <$> getGroupIdByName db user gName <*> getContactIdByName db user cName - processChatCommand vr nm $ APIAddMember groupId contactId memRole + processChatCommand cxt nm $ APIAddMember groupId contactId memRole JoinGroup gName enableNtfs -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand vr nm $ APIJoinGroup groupId enableNtfs + processChatCommand cxt nm $ APIJoinGroup groupId enableNtfs AcceptMember gName gMemberName memRole -> withMemberName gName gMemberName $ \gId gMemberId -> APIAcceptMember gId gMemberId memRole MemberRole gName gMemberName memRole -> withMemberName gName gMemberName $ \gId gMemberId -> APIMembersRole gId [gMemberId] memRole BlockForAll gName gMemberName blocked -> withMemberName gName gMemberName $ \gId gMemberId -> APIBlockMembersForAll gId [gMemberId] blocked @@ -3018,45 +3018,45 @@ processChatCommand vr nm = \case gId <- getGroupIdByName db user gName gMemberIds <- mapM (getGroupMemberIdByName db user gId) gMemberNames pure (gId, gMemberIds) - processChatCommand vr nm $ APIRemoveMembers gId gMemberIds withMessages + processChatCommand cxt nm $ APIRemoveMembers gId gMemberIds withMessages LeaveGroup gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand vr nm $ APILeaveGroup groupId + processChatCommand cxt nm $ APILeaveGroup groupId AllowRelayGroup gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand vr nm $ APIAllowRelayGroup groupId + processChatCommand cxt nm $ APIAllowRelayGroup groupId DeleteGroup gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand vr nm $ APIDeleteChat (ChatRef CTGroup groupId Nothing) (CDMFull True) + processChatCommand cxt nm $ APIDeleteChat (ChatRef CTGroup groupId Nothing) (CDMFull True) ClearGroup gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand vr nm $ APIClearChat (ChatRef CTGroup groupId Nothing) + processChatCommand cxt nm $ APIClearChat (ChatRef CTGroup groupId Nothing) ListMembers gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand vr nm $ APIListMembers groupId + processChatCommand cxt nm $ APIListMembers groupId ListMemberSupportChats gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - (Group gInfo members) <- withFastStore $ \db -> getGroup db vr user groupId + (Group gInfo members) <- withFastStore $ \db -> getGroup db cxt user groupId let memberSupportChats = filter (isJust . supportChat) members pure $ CRMemberSupportChats user gInfo memberSupportChats APIListGroups userId contactId_ search_ -> withUserId userId $ \user -> - CRGroupsList user <$> withFastStore' (\db -> getBaseGroupDetails db vr user contactId_ search_) + CRGroupsList user <$> withFastStore' (\db -> getBaseGroupDetails db cxt user contactId_ search_) ListGroups cName_ search_ -> withUser $ \user@User {userId} -> do - ct_ <- forM cName_ $ \cName -> withFastStore $ \db -> getContactByName db vr user cName - processChatCommand vr nm $ APIListGroups userId (contactId' <$> ct_) search_ + ct_ <- forM cName_ $ \cName -> withFastStore $ \db -> getContactByName db cxt user cName + processChatCommand cxt nm $ APIListGroups userId (contactId' <$> ct_) search_ APIUpdateGroupProfile groupId p' -> withUser $ \user -> do - gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId + gInfo <- withFastStore $ \db -> getGroupInfo db cxt user groupId runUpdateGroupProfile user gInfo p' UpdateGroupNames gName GroupProfile {displayName, fullName, shortDescr} -> updateGroupProfileByName gName $ \p -> p {displayName, fullName, shortDescr} ShowGroupProfile gName -> withUser $ \user -> - CRGroupProfile user <$> withFastStore (\db -> getGroupInfoByName db vr user gName) + CRGroupProfile user <$> withFastStore (\db -> getGroupInfoByName db cxt user gName) UpdateGroupDescription gName description -> updateGroupProfileByName gName $ \p -> p {description} ShowGroupDescription gName -> withUser $ \user -> - CRGroupDescription user <$> withFastStore (\db -> getGroupInfoByName db vr user gName) + CRGroupDescription user <$> withFastStore (\db -> getGroupInfoByName db cxt user gName) APICreateGroupLink groupId mRole -> withUser $ \user -> withGroupLock "createGroupLink" groupId $ do - gInfo@GroupInfo {groupProfile} <- withFastStore $ \db -> getGroupInfo db vr user groupId + gInfo@GroupInfo {groupProfile} <- withFastStore $ \db -> getGroupInfo db cxt user groupId assertUserGroupRole gInfo GRAdmin when (mRole > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole groupLinkId <- GroupLinkId <$> drgRandomBytes 16 @@ -3070,7 +3070,7 @@ processChatCommand vr nm = \case gLink <- withFastStore $ \db -> createGroupLink db gVar user gInfo connId ccLink' groupLinkId mRole subMode pure $ CRGroupLinkCreated user gInfo gLink APIGroupLinkMemberRole groupId mRole' -> withUser $ \user -> withGroupLock "groupLinkMemberRole" groupId $ do - gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId + gInfo <- withFastStore $ \db -> getGroupInfo db cxt user groupId gLnk@GroupLink {acceptMemberRole} <- withFastStore $ \db -> getGroupLink db user gInfo assertUserGroupRole gInfo GRAdmin when (mRole' > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole' @@ -3080,22 +3080,22 @@ processChatCommand vr nm = \case else pure gLnk pure $ CRGroupLink user gInfo gLnk' APIDeleteGroupLink groupId -> withUser $ \user -> withGroupLock "deleteGroupLink" groupId $ do - gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId + gInfo <- withFastStore $ \db -> getGroupInfo db cxt user groupId deleteGroupLink' user gInfo pure $ CRGroupLinkDeleted user gInfo APIGetGroupLink groupId -> withUser $ \user -> do - gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId + gInfo <- withFastStore $ \db -> getGroupInfo db cxt user groupId gLnk <- withFastStore $ \db -> getGroupLink db user gInfo pure $ CRGroupLink user gInfo gLnk APIAddGroupShortLink groupId -> withUser $ \user -> do (gInfo, gLink) <- withFastStore $ \db -> do - gInfo <- getGroupInfo db vr user groupId + gInfo <- getGroupInfo db cxt user groupId gLink <- getGroupLink db user gInfo pure (gInfo, gLink) gLink' <- setGroupLinkData nm user gInfo gLink pure $ CRGroupLink user gInfo gLink' APICreateMemberContact gId gMemberId -> withUser $ \user -> do - (g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId + (g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db cxt user gId <*> getGroupMember db cxt user gId gMemberId assertUserGroupRole g GRAuthor unless (groupFeatureUserAllowed SGFDirectMessages g) $ throwCmdError "direct messages not allowed" case memberConn m of @@ -3112,7 +3112,7 @@ processChatCommand vr nm = \case pure $ CRNewMemberContact user ct g m _ -> throwChatError CEGroupMemberNotActive APISendMemberContactInvitation contactId msgContent_ -> withUser $ \user -> do - (g@GroupInfo {groupId}, m, ct, cReq) <- withFastStore $ \db -> getMemberContact db vr user contactId + (g@GroupInfo {groupId}, m, ct, cReq) <- withFastStore $ \db -> getMemberContact db cxt user contactId when (contactGrpInvSent ct) $ throwCmdError "x.grp.direct.inv already sent" case memberConn m of Just mConn -> do @@ -3127,17 +3127,17 @@ processChatCommand vr nm = \case pure $ CRNewMemberContactSentInv user ct' g m _ -> throwChatError CEGroupMemberNotActive APIAcceptMemberContact contactId -> withUser $ \user -> do - (g, mConn, ct, groupDirectInv) <- withFastStore $ \db -> getMemberContactInvited db vr user contactId + (g, mConn, ct, groupDirectInv) <- withFastStore $ \db -> getMemberContactInvited db cxt user contactId when (groupDirectInvStartedConnection groupDirectInv) $ throwCmdError "connection already started" connectMemberContact user g mConn ct groupDirectInv `catchAllErrors` \e -> do -- get updated contact, in case connection was started - ct' <- withFastStore $ \db -> getContact db vr user contactId + ct' <- withFastStore $ \db -> getContact db cxt user contactId toView $ CEvtChatInfoUpdated user (AChatInfo SCTDirect $ DirectChat ct') throwError e -- get updated contact (groupDirectInvStartedConnection) with connection ct' <- withFastStore $ \db -> do liftIO $ setMemberContactStartedConnection db ct - getContact db vr user contactId + getContact db cxt user contactId pure $ CRMemberContactAccepted user ct' where connectMemberContact user gInfo mConn Contact {activeConn} GroupDirectInvitation {groupDirectInvLink = cReq} = @@ -3159,7 +3159,7 @@ processChatCommand vr nm = \case acId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq PQSupportOff conn <- withStore $ \db -> do connId <- liftIO $ createMemberContactConn db user acId Nothing gInfo mConn ConnPrepared contactId subMode - getConnectionById db vr user connId + getConnectionById db cxt user connId joinPreparedConn subMode conn joinPreparedConn subMode conn = do -- [incognito] send membership incognito profile @@ -3170,66 +3170,66 @@ processChatCommand vr nm = \case void $ withFastStore' $ \db -> updateConnectionStatusFromTo db conn ConnPrepared newStatus CreateGroupLink gName mRole -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand vr nm $ APICreateGroupLink groupId mRole + processChatCommand cxt nm $ APICreateGroupLink groupId mRole GroupLinkMemberRole gName mRole -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand vr nm $ APIGroupLinkMemberRole groupId mRole + processChatCommand cxt nm $ APIGroupLinkMemberRole groupId mRole DeleteGroupLink gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand vr nm $ APIDeleteGroupLink groupId + processChatCommand cxt nm $ APIDeleteGroupLink groupId ShowGroupLink gName -> withUser $ \user -> do groupId <- withFastStore $ \db -> getGroupIdByName db user gName - processChatCommand vr nm $ APIGetGroupLink groupId + processChatCommand cxt nm $ APIGetGroupLink groupId SendGroupMessageQuote gName cName quotedMsg msg -> withUser $ \user -> do (gInfo, quotedItemId, mentions) <- withFastStore $ \db -> do - gInfo <- getGroupInfoByName db vr user gName + gInfo <- getGroupInfoByName db cxt user gName let gId = groupId' gInfo qiId <- getGroupChatItemIdByText db user gId cName quotedMsg (gInfo, qiId,) <$> liftIO (getMessageMentions db user gId msg) let mc = MCText msg - processChatCommand vr nm $ APISendMessages (SRGroup (groupId' gInfo) Nothing (sendAsGroup' gInfo Nothing)) False Nothing [ComposedMessage Nothing (Just quotedItemId) mc mentions] + processChatCommand cxt nm $ APISendMessages (SRGroup (groupId' gInfo) Nothing (sendAsGroup' gInfo Nothing)) False Nothing [ComposedMessage Nothing (Just quotedItemId) mc mentions] ClearNoteFolder -> withUser $ \user -> do folderId <- withFastStore (`getUserNoteFolderId` user) - processChatCommand vr nm $ APIClearChat (ChatRef CTLocal folderId Nothing) + processChatCommand cxt nm $ APIClearChat (ChatRef CTLocal folderId Nothing) LastChats count_ -> withUser' $ \user -> do let count = fromMaybe 5000 count_ - (errs, previews) <- partitionEithers <$> withFastStore' (\db -> getChatPreviews db vr user False (PTLast count) clqNoFilters) + (errs, previews) <- partitionEithers <$> withFastStore' (\db -> getChatPreviews db cxt user False (PTLast count) clqNoFilters) unless (null errs) $ toView $ CEvtChatErrors (map ChatErrorStore errs) pure $ CRChats previews LastMessages (Just chatName) count search -> withUser $ \user -> do chatRef <- getChatRef user chatName - chatResp <- processChatCommand vr nm $ APIGetChat chatRef Nothing (CPLast count) search + chatResp <- processChatCommand cxt nm $ APIGetChat chatRef Nothing (CPLast count) search pure $ CRChatItems user (Just chatName) (aChatItems . chat $ chatResp) LastMessages Nothing count search -> withUser $ \user -> do - chatItems <- withFastStore $ \db -> getAllChatItems db vr user (CPLast count) search + chatItems <- withFastStore $ \db -> getAllChatItems db cxt user (CPLast count) search pure $ CRChatItems user Nothing chatItems LastChatItemId (Just chatName) index -> withUser $ \user -> do chatRef <- getChatRef user chatName - chatResp <- processChatCommand vr nm $ APIGetChat chatRef Nothing (CPLast $ index + 1) Nothing + chatResp <- processChatCommand cxt nm $ APIGetChat chatRef Nothing (CPLast $ index + 1) Nothing pure $ CRChatItemId user (fmap aChatItemId . listToMaybe . aChatItems . chat $ chatResp) LastChatItemId Nothing index -> withUser $ \user -> do - chatItems <- withFastStore $ \db -> getAllChatItems db vr user (CPLast $ index + 1) Nothing + chatItems <- withFastStore $ \db -> getAllChatItems db cxt user (CPLast $ index + 1) Nothing pure $ CRChatItemId user (fmap aChatItemId . listToMaybe $ chatItems) ShowChatItem (Just itemId) -> withUser $ \user -> do chatItem <- withFastStore $ \db -> do chatRef <- getChatRefViaItemId db user itemId - getAChatItem db vr user chatRef itemId + getAChatItem db cxt user chatRef itemId pure $ CRChatItems user Nothing ((: []) chatItem) ShowChatItem Nothing -> withUser $ \user -> do - chatItems <- withFastStore $ \db -> getAllChatItems db vr user (CPLast 1) Nothing + chatItems <- withFastStore $ \db -> getAllChatItems db cxt user (CPLast 1) Nothing pure $ CRChatItems user Nothing chatItems ShowChatItemInfo chatName msg -> withUser $ \user -> do chatRef <- getChatRef user chatName itemId <- getChatItemIdByText user chatRef msg - processChatCommand vr nm $ APIGetChatItemInfo chatRef itemId + processChatCommand cxt nm $ APIGetChatItemInfo chatRef itemId ShowLiveItems on -> withUser $ \_ -> asks showLiveItems >>= atomically . (`writeTVar` on) >> ok_ SendFile chatName f -> withUser $ \user -> do chatRef <- getChatRef user chatName case chatRef of - ChatRef CTLocal folderId _ -> processChatCommand vr nm $ APICreateChatItems folderId [composedMessage (Just f) (MCFile "")] - _ -> withSendRef user chatRef $ \sendRef -> processChatCommand vr nm $ APISendMessages sendRef False Nothing [composedMessage (Just f) (MCFile "")] + ChatRef CTLocal folderId _ -> processChatCommand cxt nm $ APICreateChatItems folderId [composedMessage (Just f) (MCFile "")] + _ -> withSendRef user chatRef $ \sendRef -> processChatCommand cxt nm $ APISendMessages sendRef False Nothing [composedMessage (Just f) (MCFile "")] SendImage chatName f@(CryptoFile fPath _) -> withUser $ \user -> do chatRef <- getChatRef user chatName withSendRef user chatRef $ \sendRef -> do @@ -3238,7 +3238,7 @@ processChatCommand vr nm = \case fileSize <- getFileSize filePath unless (fileSize <= maxImageSize) $ throwChatError CEFileImageSize {filePath} -- TODO include file description for preview - processChatCommand vr nm $ APISendMessages sendRef False Nothing [composedMessage (Just f) (MCImage "" fixedImagePreview)] + processChatCommand cxt nm $ APISendMessages sendRef False Nothing [composedMessage (Just f) (MCImage "" fixedImagePreview)] ForwardFile chatName fileId -> forwardFile chatName fileId SendFile ForwardImage chatName fileId -> forwardFile chatName fileId SendImage SendFileDescription _chatName _f -> throwCmdError "TODO" @@ -3265,18 +3265,18 @@ processChatCommand vr nm = \case | otherwise -> do cancelSndFile user ftm fts True cref_ <- withFastStore' $ \db -> lookupChatRefByFileId db user fileId - aci_ <- withFastStore $ \db -> lookupChatItemByFileId db vr user fileId + aci_ <- withFastStore $ \db -> lookupChatItemByFileId db cxt user fileId case (cref_, aci_) of (Nothing, _) -> pure $ CRSndFileCancelled user Nothing ftm fts (Just (ChatRef CTDirect contactId _), Just aci) -> do - (contact, sharedMsgId) <- withFastStore $ \db -> (,) <$> getContact db vr user contactId <*> getSharedMsgIdByFileId db userId fileId + (contact, sharedMsgId) <- withFastStore $ \db -> (,) <$> getContact db cxt user contactId <*> getSharedMsgIdByFileId db userId fileId void . sendDirectContactMessage user contact $ XFileCancel sharedMsgId pure $ CRSndFileCancelled user (Just aci) ftm fts (Just (ChatRef CTGroup groupId scope), Just aci) -> do - (gInfo, sharedMsgId) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user groupId <*> getSharedMsgIdByFileId db userId fileId - chatScopeInfo <- mapM (getChatScopeInfo vr user) scope - recipients <- getGroupRecipients vr user gInfo chatScopeInfo groupKnockingVersion + (gInfo, sharedMsgId) <- withFastStore $ \db -> (,) <$> getGroupInfo db cxt user groupId <*> getSharedMsgIdByFileId db userId fileId + chatScopeInfo <- mapM (getChatScopeInfo cxt user) scope + recipients <- getGroupRecipients cxt user gInfo chatScopeInfo groupKnockingVersion void . sendGroupMessage user gInfo scope recipients $ XFileCancel sharedMsgId pure $ CRSndFileCancelled user (Just aci) ftm fts (Just _, _) -> throwChatError $ CEFileInternal "invalid chat ref for file transfer" @@ -3289,7 +3289,7 @@ processChatCommand vr nm = \case | otherwise -> case xftpRcvFile of Nothing -> do cancelRcvFileTransfer user ftr - ci <- withFastStore $ \db -> lookupChatItemByFileId db vr user fileId + ci <- withFastStore $ \db -> lookupChatItemByFileId db cxt user fileId pure $ CRRcvFileCancelled user ci ftr Just XFTPRcvFile {agentRcvFileId} -> do forM_ (liveRcvFileTransferPath ftr) $ \filePath -> do @@ -3300,7 +3300,7 @@ processChatCommand vr nm = \case aci_ <- resetRcvCIFileStatus user fileId CIFSRcvInvitation pure $ CRRcvFileCancelled user aci_ ftr FileStatus fileId -> withUser $ \user -> do - withFastStore (\db -> lookupChatItemByFileId db vr user fileId) >>= \case + withFastStore (\db -> lookupChatItemByFileId db cxt user fileId) >>= \case Nothing -> do fileStatus <- withFastStore $ \db -> getFileTransferProgress db user fileId pure $ CRFileTransferStatus user fileStatus @@ -3329,7 +3329,7 @@ processChatCommand vr nm = \case let p = (fromLocalProfile profile :: Profile) {preferences = Just . setPreference f (Just allowed) $ preferences' user} updateProfile user p SetContactFeature (ACF f) cName allowed_ -> withUser $ \user -> do - ct@Contact {userPreferences} <- withFastStore $ \db -> getContactByName db vr user cName + ct@Contact {userPreferences} <- withFastStore $ \db -> getContactByName db cxt user cName let prefs' = setPreference f allowed_ $ Just userPreferences updateContactPrefs user ct prefs' SetGroupFeature (AGFNR f) gName enabled -> @@ -3349,7 +3349,7 @@ processChatCommand vr nm = \case p = (fromLocalProfile profile :: Profile) {preferences = Just . setPreference' SCFTimedMessages (Just pref) $ preferences' user} updateProfile user p SetContactTimedMessages cName timedMessagesEnabled_ -> withUser $ \user -> do - ct@Contact {userPreferences = userPreferences@Preferences {timedMessages}} <- withFastStore $ \db -> getContactByName db vr user cName + ct@Contact {userPreferences = userPreferences@Preferences {timedMessages}} <- withFastStore $ \db -> getContactByName db cxt user cName let currentTTL = timedMessages >>= \TimedMessagesPreference {ttl} -> ttl pref_ = tmeToPref currentTTL <$> timedMessagesEnabled_ prefs' = setPreference' SCFTimedMessages pref_ $ Just userPreferences @@ -3464,7 +3464,7 @@ processChatCommand vr nm = \case _ -> throwCmdError "not supported" pure $ ChatRef cType chatId Nothing getSendAsGroup :: User -> ChatRef -> CM ShowGroupAsSender - getSendAsGroup user' (ChatRef CTGroup chatId scope) = (`sendAsGroup'` scope) <$> withFastStore (\db -> getGroupInfo db vr user' chatId) + getSendAsGroup user' (ChatRef CTGroup chatId scope) = (`sendAsGroup'` scope) <$> withFastStore (\db -> getGroupInfo db cxt user' chatId) getSendAsGroup _ _ = pure False getChatRefAndMentions :: User -> ChatName -> Text -> CM (ChatRef, Map MemberName GroupMemberId) getChatRefAndMentions user cName msg = do @@ -3483,13 +3483,13 @@ processChatCommand vr nm = \case checkStoreNotChanged :: CM ChatResponse -> CM ChatResponse checkStoreNotChanged = ifM (asks chatStoreChanged >>= readTVarIO) (throwChatError CEChatStoreChanged) withUserName :: UserName -> (UserId -> ChatCommand) -> CM ChatResponse - withUserName uName cmd = withFastStore (`getUserIdByName` uName) >>= processChatCommand vr nm . cmd + withUserName uName cmd = withFastStore (`getUserIdByName` uName) >>= processChatCommand cxt nm . cmd withContactName :: ContactName -> (ContactId -> ChatCommand) -> CM ChatResponse withContactName cName cmd = withUser $ \user -> - withFastStore (\db -> getContactIdByName db user cName) >>= processChatCommand vr nm . cmd + withFastStore (\db -> getContactIdByName db user cName) >>= processChatCommand cxt nm . cmd withMemberName :: GroupName -> ContactName -> (GroupId -> GroupMemberId -> ChatCommand) -> CM ChatResponse withMemberName gName mName cmd = withUser $ \user -> - getGroupAndMemberId user gName mName >>= processChatCommand vr nm . uncurry cmd + getGroupAndMemberId user gName mName >>= processChatCommand cxt nm . uncurry cmd getConnectionCode :: ConnId -> CM Text getConnectionCode connId = verificationCode <$> withAgent (`getConnectionRatchetAdHash` connId) verifyConnectionCode :: User -> Connection -> Maybe Text -> CM ChatResponse @@ -3523,7 +3523,7 @@ processChatCommand vr nm = \case -- TODO PQ the error above should be CEIncompatibleConnReqVersion, also the same API should be called in Plan Just (agentV, pqSup') -> do let chatV = agentToChatVersion agentV - withFastStore' (\db -> getConnectionEntityByConnReq db vr user cReqs) >>= \case + withFastStore' (\db -> getConnectionEntityByConnReq db cxt user cReqs) >>= \case Nothing -> joinNewConn chatV Just (RcvDirectMsgConnection conn@Connection {connStatus, contactConnInitiated, customUserProfileId} _ct_) | connStatus == ConnNew && contactConnInitiated -> joinNewConn chatV -- own connection link @@ -3567,7 +3567,7 @@ processChatCommand vr nm = \case ConnPrepared -> joinPreparedConn' xContactId conn (Just $ Just gInfo) _ -> connect' groupLinkId xContactId (Just $ Just gInfo) -- why not "already connected" for host member? Nothing -> - withFastStore' (\db -> getConnReqContactXContactId db vr user cReqHash1 cReqHash2) >>= \case + withFastStore' (\db -> getConnReqContactXContactId db cxt user cReqHash1 cReqHash2) >>= \case Right ct@Contact {activeConn} -> case groupLinkId of Nothing -> case activeConn of Just conn@Connection {connStatus = ConnPrepared, xContactId} -> joinPreparedConn' xContactId conn Nothing @@ -3622,7 +3622,7 @@ processChatCommand vr nm = \case let cReqHash = ConnReqUriHash . C.sha256Hash $ strEncode cReq conn <- withFastStore' $ \db -> createConnReqConnection db userId connId (Just $ PCEContact ct) cReq cReqHash shortLink newXContactId (NewIncognito <$> incognitoProfile) Nothing subMode chatV pqSup void $ joinContact user conn cReq incognitoProfile newXContactId Nothing Nothing Nothing pqSup - ct' <- withStore $ \db -> getContact db vr user contactId + ct' <- withStore $ \db -> getContact db cxt user contactId pure $ CRSentInvitationToContact user ct' incognitoProfile Just conn@Connection {connStatus, xContactId = xContactId_, customUserProfileId} -> case connStatus of ConnPrepared -> do @@ -3631,14 +3631,14 @@ processChatCommand vr nm = \case localIncognitoProfile <- forM customUserProfileId $ \pId -> withFastStore $ \db -> getProfileById db userId pId let incognitoProfile = fromLocalProfile <$> localIncognitoProfile void $ joinContact user conn cReq incognitoProfile xContactId Nothing Nothing Nothing PQSupportOn - ct' <- withStore $ \db -> getContact db vr user contactId + ct' <- withStore $ \db -> getContact db cxt user contactId pure $ CRSentInvitationToContact user ct' incognitoProfile _ -> throwCmdError "contact already has connection" connectToRelay :: User -> GroupInfo -> ShortLinkContact -> CM (ShortLinkContact, GroupMember, Either ChatError ()) connectToRelay user gInfo relayLink = do gVar <- asks random -- Save relayLink to re-use relay member record on retry (check by relayLink) - relayMember <- withFastStore $ \db -> getCreateRelayForMember db vr gVar user gInfo relayLink + relayMember <- withFastStore $ \db -> getCreateRelayForMember db cxt gVar user gInfo relayLink r <- tryAllErrors $ do (fd@FixedLinkData {rootKey = relayKey, linkEntityId}, cData) <- getShortLinkConnReq nm user relayLink relayLinkData_ <- liftIO $ decodeLinkUserData cData @@ -3649,11 +3649,11 @@ processChatCommand vr nm = \case let cReq = linkConnReq fd relayLinkToConnect = CCLink cReq (Just relayLink) void $ connectViaContact user (Just $ PCEGroup gInfo relayMember) (incognitoMembership gInfo) relayLinkToConnect Nothing Nothing - relayMember' <- withFastStore $ \db -> getGroupMember db vr user (groupId' gInfo) (groupMemberId' relayMember) + relayMember' <- withFastStore $ \db -> getGroupMember db cxt user (groupId' gInfo) (groupMemberId' relayMember) pure (relayLink, relayMember', r) syncSubscriberRelays :: User -> GroupInfo -> [ShortLinkContact] -> CM () syncSubscriberRelays user gInfo currentRelayLinks = void . tryAllErrors $ do - localRelayMembers <- withFastStore' $ \db -> getGroupRelayMembers db vr user gInfo + localRelayMembers <- withFastStore' $ \db -> getGroupRelayMembers db cxt user gInfo let activeRelayMembers = filter memberCurrent localRelayMembers memberRelayLink GroupMember {relayLink = rl} = rl localRelayLinks = mapMaybe memberRelayLink activeRelayMembers @@ -3723,7 +3723,7 @@ processChatCommand vr nm = \case | otherwise = do when (n /= n') $ checkValidName n' -- read contacts before user update to correctly merge preferences - contacts <- withFastStore' $ \db -> getUserContacts db vr user + contacts <- withFastStore' $ \db -> getUserContacts db cxt user user' <- updateUser asks currentUser >>= atomically . (`writeTVar` Just user') withChatLock "updateProfile" $ do @@ -3775,7 +3775,7 @@ processChatCommand vr nm = \case (conn, MsgFlags {notification = hasNotification XInfo_}, (vrValue msgBody, [msgId])) setMyAddressData :: User -> UserContactLink -> CM UserContactLink setMyAddressData user@User {userChatRelay} ucl@UserContactLink {userContactLinkId, connLinkContact = CCLink connFullLink _sLnk_, addressSettings} = do - conn <- withFastStore $ \db -> getUserAddressConnection db vr user + conn <- withFastStore $ \db -> getUserAddressConnection db cxt user let shortLinkProfile = userProfileDirect user Nothing Nothing True -- TODO [short links] do not save address to server if data did not change, spinners, error handling userData @@ -3809,12 +3809,12 @@ processChatCommand vr nm = \case gInfo' <- withStore $ \db -> updateGroupProfile db user gInfo p' msg <- case businessChat of Just BusinessChatInfo {businessId} -> do - ms <- withStore' $ \db -> getGroupMembers db vr user gInfo' + ms <- withStore' $ \db -> getGroupMembers db cxt user gInfo' let (newMs, oldMs) = partition (\m -> maxVersion (memberChatVRange m) >= businessChatPrefsVersion) ms -- this is a fallback to send the members with the old version correct profile of the business when preferences change unless (null oldMs) $ do GroupMember {memberProfile = LocalProfile {displayName, fullName, shortDescr, image}} <- - withStore $ \db -> getGroupMemberByMemberId db vr user gInfo' businessId + withStore $ \db -> getGroupMemberByMemberId db cxt user gInfo' businessId let p'' = p' {displayName, fullName, shortDescr, image} :: GroupProfile recipients = filter memberCurrentOrPending oldMs void $ sendGroupMessage user gInfo' Nothing recipients (XGrpInfo p'') @@ -3827,9 +3827,9 @@ processChatCommand vr nm = \case sendGroupMessage user gInfo' Nothing recipients (XGrpInfo p') where getRecipients - | useRelays' gInfo' = withFastStore' $ \db -> getGroupRelayMembers db vr user gInfo' + | useRelays' gInfo' = withFastStore' $ \db -> getGroupRelayMembers db cxt user gInfo' | otherwise = do - ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo' + ms <- withFastStore' $ \db -> getGroupMembers db cxt user gInfo' pure $ filter memberCurrentOrPending ms let cd = CDGroupSnd gInfo' Nothing unless (sameGroupProfileInfo p p') $ do @@ -3890,13 +3890,13 @@ processChatCommand vr nm = \case updateGroupProfileByName :: GroupName -> (GroupProfile -> GroupProfile) -> CM ChatResponse updateGroupProfileByName gName update = withUser $ \user -> do gInfo@GroupInfo {groupProfile = p} <- withStore $ \db -> - getGroupIdByName db user gName >>= getGroupInfo db vr user + getGroupIdByName db user gName >>= getGroupInfo db cxt user runUpdateGroupProfile user gInfo $ update p withCurrentCall :: ContactId -> (User -> Contact -> Call -> CM (Maybe Call)) -> CM ChatResponse withCurrentCall ctId action = do (user, ct) <- withStore $ \db -> do user <- getUserByContactId db ctId - (user,) <$> getContact db vr user ctId + (user,) <$> getContact db cxt user ctId calls <- asks currentCalls withContactLock "currentCall" ctId $ atomically (TM.lookup ctId calls) >>= \case @@ -3938,7 +3938,7 @@ processChatCommand vr nm = \case FTSnd {fileTransferMeta = FileTransferMeta {filePath, xftpSndFile}} -> forward filePath $ xftpSndFile >>= \XFTPSndFile {cryptoArgs} -> cryptoArgs _ -> throwChatError CEFileNotReceived {fileId} where - forward path cfArgs = processChatCommand vr nm $ sendCommand chatName $ CryptoFile path cfArgs + forward path cfArgs = processChatCommand cxt nm $ sendCommand chatName $ CryptoFile path cfArgs getGroupAndMemberId :: User -> GroupName -> ContactName -> CM (GroupId, GroupMemberId) getGroupAndMemberId user gName groupMemberName = withStore $ \db -> do @@ -3950,7 +3950,7 @@ processChatCommand vr nm = \case checkValidName displayName -- [incognito] generate incognito profile for group membership incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing - withFastStore $ \db -> createNewGroup db vr user gProfile incognitoProfile useRelays memberId groupKeys_ publicMemberCount_ + withFastStore $ \db -> createNewGroup db cxt user gProfile incognitoProfile useRelays memberId groupKeys_ publicMemberCount_ createNewGroupItems :: User -> GroupInfo -> CM () createNewGroupItems user gInfo = do let cd = CDGroupSnd gInfo Nothing @@ -3993,9 +3993,9 @@ processChatCommand vr nm = \case subMode <- chatReadVar subscriptionMode connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq PQSupportOff (relayMember, conn, groupRelay) <- withFastStore $ \db -> do - relayMember <- createRelayForOwner db vr gVar user gInfo relay + relayMember <- createRelayForOwner db cxt gVar user gInfo relay groupRelay <- createGroupRelayRecord db gInfo relayMember relay - conn <- createRelayConnection db vr user (groupMemberId' relayMember) connId ConnPrepared chatV subMode + conn <- createRelayConnection db cxt user (groupMemberId' relayMember) connId ConnPrepared chatV subMode pure (relayMember, conn, groupRelay) let GroupMember {memberRole = userRole, memberId = userMemberId} = membership allowSimplexLinks = groupFeatureUserAllowed SGFSimplexLinks gInfo @@ -4067,15 +4067,15 @@ processChatCommand vr nm = \case (chatId, chatSettings) <- case cType of CTDirect -> withFastStore $ \db -> do ctId <- getContactIdByName db user name - Contact {chatSettings} <- getContact db vr user ctId + Contact {chatSettings} <- getContact db cxt user ctId pure (ctId, chatSettings) CTGroup -> withFastStore $ \db -> do gId <- getGroupIdByName db user name - GroupInfo {chatSettings} <- getGroupInfo db vr user gId + GroupInfo {chatSettings} <- getGroupInfo db cxt user gId pure (gId, chatSettings) _ -> throwCmdError "not supported" - processChatCommand vr nm $ APISetChatSettings (ChatRef cType chatId Nothing) $ updateSettings chatSettings + processChatCommand cxt nm $ APISetChatSettings (ChatRef cType chatId Nothing) $ updateSettings chatSettings connectPlan :: User -> ConnectTarget -> Bool -> Maybe LinkOwnerSig -> CM (ACreatedConnLink, ConnectionPlan) connectPlan user ct resolveKnown sig_ = case ct of CTLink l -> connectPlanLink user l resolveKnown sig_ @@ -4095,10 +4095,10 @@ processChatCommand vr nm = \case where knownLinkPlans l' = withFastStore $ \db -> do let inv cReq = ACCL SCMInvitation $ CCLink cReq (Just l') - liftIO (getConnectionEntityViaShortLink db vr user l') >>= \case + liftIO (getConnectionEntityViaShortLink db cxt user l') >>= \case Just (cReq, ent) -> pure $ Just (inv cReq, invitationEntityPlan Nothing Nothing ent) -- deleted contact is returned as known, as invitation link cannot be re-used too connect anyway - Nothing -> bimap inv (CPInvitationLink . ILPKnown) <$$> getContactViaShortLinkToConnect db vr user l' + Nothing -> bimap inv (CPInvitationLink . ILPKnown) <$$> getContactViaShortLinkToConnect db cxt user l' invitationReqAndPlan cReq sLnk_ cld ov = do plan <- invitationRequestPlan user cReq cld ov `catchAllErrors` (pure . CPError) pure (ACCL SCMInvitation (CCLink cReq sLnk_), plan) @@ -4113,7 +4113,7 @@ processChatCommand vr nm = \case Just r -> pure r Nothing -> do (FixedLinkData {linkConnReq = cReq, rootKey}, cData) <- getShortLinkConnReq nm user l' - withFastStore' (\db -> getContactWithoutConnViaShortAddress db vr user l') >>= \case + withFastStore' (\db -> getContactWithoutConnViaShortAddress db cxt user l') >>= \case Just ct' | not (contactDeleted ct') -> pure (con cReq, CPContactAddress (CAPContactViaAddress ct')) _ -> do contactSLinkData_ <- liftIO $ decodeLinkUserData cData @@ -4126,9 +4126,9 @@ processChatCommand vr nm = \case liftIO (getUserContactLinkViaShortLink db user l') >>= \case Just UserContactLink {connLinkContact = CCLink cReq _} -> pure $ Just (con cReq, CPContactAddress CAPOwnLink) Nothing -> - getContactViaShortLinkToConnect db vr user l' >>= \case + getContactViaShortLinkToConnect db cxt user l' >>= \case Just (cReq, ct') -> pure $ if contactDeleted ct' then Nothing else Just (con cReq, CPContactAddress (CAPKnown ct')) - Nothing -> (gPlan =<<) <$> getGroupViaShortLinkToConnect db vr user l' + Nothing -> (gPlan =<<) <$> getGroupViaShortLinkToConnect db cxt user l' CCTGroup -> groupShortLinkPlan CCTChannel -> groupShortLinkPlan CCTRelay -> throwCmdError "chat relay links are not supported in this version" @@ -4164,9 +4164,9 @@ processChatCommand vr nm = \case Just GroupShortLinkData {groupProfile = GroupProfile {publicGroup = Just PublicGroupProfile {groupType}}} -> groupType /= GTChannel _ -> False knownLinkPlans = withFastStore $ \db -> - liftIO (getGroupInfoViaUserShortLink db vr user l') >>= \case + liftIO (getGroupInfoViaUserShortLink db cxt user l') >>= \case Just (cReq, g) -> pure $ Just (con cReq, CPGroupLink (GLPOwnLink g)) - Nothing -> (gPlan =<<) <$> getGroupViaShortLinkToConnect db vr user l' + Nothing -> (gPlan =<<) <$> getGroupViaShortLinkToConnect db cxt user l' resolveKnownGroup g = do (fd@FixedLinkData {rootKey = rk}, cData@(ContactLinkData _ UserContactData {owners})) <- getShortLinkConnReq' nm user l' groupSLinkData_ <- liftIO $ decodeLinkUserData cData @@ -4182,7 +4182,7 @@ processChatCommand vr nm = \case -- bytes via strEncode, so an `@contact` lookup can never match a group -- row (and vice versa). Dispatch on nameType up front to skip a probe. NTContact -> do - ct_ <- withFastStore $ \db -> getContactBySimplexName db vr user ni + ct_ <- withFastStore $ \db -> getContactBySimplexName db cxt user ni case ct_ of Just ct -> case preparedContact ct of Just PreparedContact {connLinkToConnect} -> pure (connLinkToConnect, CPContactAddress (CAPKnown ct)) @@ -4191,7 +4191,7 @@ processChatCommand vr nm = \case Nothing -> throwChatError $ CESimplexNameUnprepared ni Nothing -> resolveAndDispatch NTPublicGroup -> do - g_ <- withFastStore $ \db -> getGroupInfoBySimplexName db vr user ni + g_ <- withFastStore $ \db -> getGroupInfoBySimplexName db cxt user ni case g_ of -- Mirror gPlan at line ~4133 in the link-based path: a removed member is not a -- known-and-reconnectable group; treat as "not found" so the caller can try elsewhere. @@ -4205,7 +4205,7 @@ processChatCommand vr nm = \case resolveAndDispatch :: CM (ACreatedConnLink, ConnectionPlan) resolveAndDispatch = resolveOnUserServers user nameDomain >>= \case - Right nr -> dispatchResolvedRecord vr nm user ni nr + Right nr -> dispatchResolvedRecord cxt nm user ni nr Left re -> throwError $ resolveErrorToChatError ni re connectWithPlan :: User -> IncognitoEnabled -> ACreatedConnLink -> ConnectionPlan -> CM ChatResponse connectWithPlan user@User {userId} incognito ccLink plan @@ -4213,13 +4213,13 @@ processChatCommand vr nm = \case case plan of CPError e -> eToView e; _ -> pure () case plan of CPContactAddress (CAPContactViaAddress Contact {contactId}) -> - processChatCommand vr nm $ APIConnectContactViaAddress userId incognito contactId - _ -> processChatCommand vr nm $ APIConnect userId incognito $ Just ccLink + processChatCommand cxt nm $ APIConnectContactViaAddress userId incognito contactId + _ -> processChatCommand cxt nm $ APIConnect userId incognito $ Just ccLink | otherwise = pure $ CRConnectionPlan user ccLink plan invitationRequestPlan :: User -> ConnReqInvitation -> Maybe ContactShortLinkData -> Maybe OwnerVerification -> CM ConnectionPlan invitationRequestPlan user cReq cld ov = do maybe (CPInvitationLink (ILPOk cld ov)) (invitationEntityPlan cld ov) - <$> withFastStore' (\db -> getConnectionEntityByConnReq db vr user $ invCReqSchemas cReq) + <$> withFastStore' (\db -> getConnectionEntityByConnReq db cxt user $ invCReqSchemas cReq) where invCReqSchemas :: ConnReqInvitation -> (ConnReqInvitation, ConnReqInvitation) invCReqSchemas (CRInvitationUri crData e2e) = @@ -4251,9 +4251,9 @@ processChatCommand vr nm = \case withFastStore' (\db -> getUserContactLinkByConnReq db user cReqSchemas) >>= \case Just _ -> pure $ CPContactAddress CAPOwnLink Nothing -> - withFastStore' (\db -> getContactConnEntityByConnReqHash db vr user cReqHashes) >>= \case + withFastStore' (\db -> getContactConnEntityByConnReqHash db cxt user cReqHashes) >>= \case Nothing -> - withFastStore' (\db -> getContactWithoutConnViaAddress db vr user cReqSchemas) >>= \case + withFastStore' (\db -> getContactWithoutConnViaAddress db cxt user cReqSchemas) >>= \case Just ct | not (contactDeleted ct) -> pure $ CPContactAddress (CAPContactViaAddress ct) _ -> pure $ CPContactAddress (CAPOk cld ov) Just (RcvDirectMsgConnection Connection {connStatus} Nothing) @@ -4270,11 +4270,11 @@ processChatCommand vr nm = \case groupJoinRequestPlan user (CRContactUri crData) linkInfo gld ov = do let cReqSchemas = contactCReqSchemas crData cReqHashes = bimap contactCReqHash contactCReqHash cReqSchemas - withFastStore' (\db -> getGroupInfoByUserContactLinkConnReq db vr user cReqSchemas) >>= \case + withFastStore' (\db -> getGroupInfoByUserContactLinkConnReq db cxt user cReqSchemas) >>= \case Just g -> pure $ CPGroupLink (GLPOwnLink g) Nothing -> do - connEnt_ <- withFastStore' $ \db -> getContactConnEntityByConnReqHash db vr user cReqHashes - gInfo_ <- withFastStore' $ \db -> getGroupInfoByGroupLinkHash db vr user cReqHashes + connEnt_ <- withFastStore' $ \db -> getContactConnEntityByConnReqHash db cxt user cReqHashes + gInfo_ <- withFastStore' $ \db -> getGroupInfoByGroupLinkHash db cxt user cReqHashes case (gInfo_, connEnt_) of (Nothing, Nothing) -> pure $ CPGroupLink (GLPOk linkInfo gld ov) -- TODO [short links] RcvDirectMsgConnection branches are deprecated? (old group link protocol?) @@ -4329,7 +4329,7 @@ processChatCommand vr nm = \case shortenShortLink' =<< withAgent (\a -> setConnShortLink a nm (aConnId' conn) SCMInvitation userLinkData Nothing) updateCIGroupInvitationStatus :: User -> GroupInfo -> CIGroupInvitationStatus -> CM () updateCIGroupInvitationStatus user GroupInfo {groupId} newStatus = do - AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withFastStore $ \db -> getChatItemByGroupId db vr user groupId + AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withFastStore $ \db -> getChatItemByGroupId db cxt user groupId case (cInfo, content) of (DirectChat ct@Contact {contactId}, CIRcvGroupInvitation ciGroupInv@CIGroupInvitation {status} memRole) | status == CIGISPending -> do @@ -4352,7 +4352,7 @@ processChatCommand vr nm = \case sendContactContentMessages :: User -> ContactId -> Bool -> Maybe Int -> NonEmpty ComposedMessageReq -> CM ChatResponse sendContactContentMessages user contactId live itemTTL cmrs = do assertMultiSendable live cmrs - ct <- withFastStore $ \db -> getContact db vr user contactId + ct <- withFastStore $ \db -> getContact db cxt user contactId assertDirectAllowed user MDSnd ct XMsgNew_ assertVoiceAllowed ct processComposedMessages ct @@ -4409,8 +4409,8 @@ processChatCommand vr nm = \case sendGroupContentMessages :: User -> GroupInfo -> Maybe GroupChatScope -> ShowGroupAsSender -> Bool -> Maybe Int -> NonEmpty ComposedMessageReq -> CM ChatResponse sendGroupContentMessages user gInfo scope showGroupAsSender live itemTTL cmrs = do assertMultiSendable live cmrs - chatScopeInfo <- mapM (getChatScopeInfo vr user) scope - recipients <- getGroupRecipients vr user gInfo chatScopeInfo modsCompatVersion + chatScopeInfo <- mapM (getChatScopeInfo cxt user) scope + recipients <- getGroupRecipients cxt user gInfo chatScopeInfo modsCompatVersion sendGroupContentMessages_ user gInfo scope showGroupAsSender chatScopeInfo recipients live itemTTL cmrs where hasReport = any (\(ComposedMessage {msgContent}, _, _, _) -> isReport msgContent) cmrs @@ -4555,7 +4555,7 @@ processChatCommand vr nm = \case throwError err getCommandDirectChatItems :: User -> Int64 -> NonEmpty ChatItemId -> CM (Contact, [CChatItem 'CTDirect]) getCommandDirectChatItems user ctId itemIds = do - ct <- withFastStore $ \db -> getContact db vr user ctId + ct <- withFastStore $ \db -> getContact db cxt user ctId (errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getDirectCI db) (L.toList itemIds)) unless (null errs) $ toView $ CEvtChatErrors errs pure (ct, items) @@ -4564,7 +4564,7 @@ processChatCommand vr nm = \case getDirectCI db itemId = runExceptT . withExceptT ChatErrorStore $ getDirectChatItem db user ctId itemId getCommandGroupChatItems :: User -> Int64 -> NonEmpty ChatItemId -> CM (GroupInfo, [CChatItem 'CTGroup]) getCommandGroupChatItems user gId itemIds = do - gInfo <- withFastStore $ \db -> getGroupInfo db vr user gId + gInfo <- withFastStore $ \db -> getGroupInfo db cxt user gId (errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getGroupCI db gInfo) (L.toList itemIds)) unless (null errs) $ toView $ CEvtChatErrors errs pure (gInfo, items) @@ -4623,7 +4623,7 @@ processChatCommand vr nm = \case withSendRef user chatRef a = case chatRef of ChatRef CTDirect cId _ -> a $ SRDirect cId ChatRef CTGroup gId scope -> do - gInfo <- withFastStore $ \db -> getGroupInfo db vr user gId + gInfo <- withFastStore $ \db -> getGroupInfo db cxt user gId a $ SRGroup gId scope (sendAsGroup' gInfo scope) _ -> throwCmdError "not supported" getSharedMsgId :: CM SharedMsgId @@ -4677,8 +4677,8 @@ resolveOnUserServers user@User {userId} domain = do -- via the existing 'createPreparedContact' / 'createPreparedGroup' simplex_name -- parameter (introduced for the local-prepare path, see commit c6f26150), so -- the resolver hit reuses the same DB write path as a local-prepare hit. -dispatchResolvedRecord :: VersionRangeChat -> NetworkRequestMode -> User -> SimplexNameInfo -> NameRecord -> CM (ACreatedConnLink, ConnectionPlan) -dispatchResolvedRecord vr nm user ni@SimplexNameInfo {nameType} NameRecord {nrSimplexChannel, nrSimplexContact} = do +dispatchResolvedRecord :: StoreCxt -> NetworkRequestMode -> User -> SimplexNameInfo -> NameRecord -> CM (ACreatedConnLink, ConnectionPlan) +dispatchResolvedRecord cxt nm user ni@SimplexNameInfo {nameType} NameRecord {nrSimplexChannel, nrSimplexContact} = do lnk <- liftEither $ firstNameLink nameType nrSimplexChannel nrSimplexContact ni acl <- liftEither $ first (chatErrorAgent . AGENT . A_LINK) $ strDecode (encodeUtf8 lnk) prepareAndPlan acl @@ -4701,7 +4701,7 @@ dispatchResolvedRecord vr nm user ni@SimplexNameInfo {nameType} NameRecord {nrSi liftIO (decodeLinkUserData cData) >>= maybe (throwError $ chatErrorAgent $ AGENT $ A_LINK "could not decode contact profile from RSLV link") pure let ccLink = CCLink cReq (Just l') accLink = ACCL SCMContact ccLink - (ct, displaced_) <- withStore $ \db -> createPreparedContact db vr user profile accLink Nothing (Just ni) + (ct, displaced_) <- withStore $ \db -> createPreparedContact db cxt user profile accLink Nothing (Just ni) let Profile {simplexName = pSimplexName} = profile Contact {localDisplayName = newLDN} = ct surfaceSimplexNameConflict user pSimplexName displaced_ SNCEContact newLDN @@ -4717,7 +4717,7 @@ dispatchResolvedRecord vr nm user ni@SimplexNameInfo {nameType} NameRecord {nrSi subRole <- if useRelays then asks $ channelSubscriberRole . config else pure GRMember gVar <- asks random let ccLink = CCLink cReq (Just l') - (g, _hostMember_) <- withStore $ \db -> createPreparedGroup db gVar vr user groupProfile False ccLink Nothing useRelays subRole publicMemberCount_ (Just ni) + (g, _hostMember_) <- withStore $ \db -> createPreparedGroup db gVar cxt user groupProfile False ccLink Nothing useRelays subRole publicMemberCount_ (Just ni) pure (ACCL SCMContact ccLink, CPGroupLink (GLPKnown g (BoolDef False) Nothing (ListDef []))) -- Mirror the inline 'serverShortLink' helper defined in 'processChatCommand' -- where this dispatch is invoked: RSLV-supplied short links may carry the @@ -4779,8 +4779,8 @@ linksMatch resolved stored = case strDecode (encodeUtf8 resolved) :: Either Stri -- Throws CESimplexNameNotFound when the row has no claim to verify. apiVerifySimplexName :: User -> ChatRef -> CM ChatResponse apiVerifySimplexName user chatRef = do - vr <- chatVersionRange - (claim, storedLink, persistVerified) <- loadClaimAndLink vr + cxt <- chatStoreCxt + (claim, storedLink, persistVerified) <- loadClaimAndLink cxt let domain = (\SimplexNameInfo {nameDomain} -> nameDomain) claim nameType' = (\SimplexNameInfo {nameType} -> nameType) claim resolveOnUserServers user domain >>= \case @@ -4805,16 +4805,16 @@ apiVerifySimplexName user chatRef = do -- Returns the claim to verify, the peer's stored link, and a callback that -- persists the verified_at timestamp to the appropriate table. Throws a -- command error when the row has no claim or no link (nothing to verify). - loadClaimAndLink :: VersionRangeChat -> CM (SimplexNameInfo, ConnLinkContact, DB.Connection -> UTCTime -> IO ()) - loadClaimAndLink vr = case chatRef of + loadClaimAndLink :: StoreCxt -> CM (SimplexNameInfo, ConnLinkContact, DB.Connection -> UTCTime -> IO ()) + loadClaimAndLink cxt = case chatRef of ChatRef CTDirect cId _ -> do - ct <- withFastStore $ \db -> getContact db vr user cId + ct <- withFastStore $ \db -> getContact db cxt user cId let Contact {contactId, simplexName = ctSimplexName, profile = LocalProfile {contactLink}} = ct claim <- maybe (throwCmdError "contact has no simplex_name to verify") pure ctSimplexName lnk <- maybe (throwCmdError "contact has no stored link to verify against") pure contactLink pure (claim, lnk, \db ts -> setContactSimplexNameVerifiedAt db user contactId ts) ChatRef CTGroup gId _ -> do - g <- withFastStore $ \db -> getGroupInfo db vr user gId + g <- withFastStore $ \db -> getGroupInfo db cxt user gId let GroupInfo {groupId, simplexName = gSimplexName, preparedGroup} = g claim <- maybe (throwCmdError "group has no simplex_name to verify") pure gSimplexName PreparedGroup {connLinkToConnect = CCLink cReq shortLink_} <- @@ -5033,17 +5033,17 @@ cleanupManager = do timedItems <- withStore' $ \db -> getTimedItems db user startTimedThreadCutoff forM_ timedItems $ \(itemRef, deleteAt) -> startTimedItemThread user itemRef deleteAt `catchAllErrors` const (pure ()) cleanupDeletedContacts user = do - vr <- chatVersionRange - contacts <- withStore' $ \db -> getDeletedContacts db vr user + cxt <- chatStoreCxt + contacts <- withStore' $ \db -> getDeletedContacts db cxt user forM_ contacts $ \ct -> withStore (\db -> deleteContactWithoutGroups db user ct) `catchAllErrors` eToView cleanupInProgressGroups user = do - vr <- chatVersionRange + cxt <- chatStoreCxt ts <- liftIO getCurrentTime -- older than 30 minutes to avoid deleting a newly created group let cutoffTs = addUTCTime (- 1800) ts - inProgressGroups <- withStore' $ \db -> getInProgressGroups db vr user cutoffTs + inProgressGroups <- withStore' $ \db -> getInProgressGroups db cxt user cutoffTs forM_ inProgressGroups $ \gInfo -> deleteInProgressGroup user gInfo `catchAllErrors` eToView cleanupStaleRelayTestConns user = do @@ -5054,10 +5054,10 @@ cleanupManager = do deleteAgentConnectionAsync acId withStore' $ \db -> deleteConnectionByAgentConnId db user acId cleanupRemovedMembers user = do - vr <- chatVersionRange + cxt <- chatStoreCxt ts <- liftIO getCurrentTime let cutoffTs = addUTCTime (-nominalDay) ts - removedMembers <- withStore' $ \db -> getRemovedMembersToCleanup db vr user cutoffTs + removedMembers <- withStore' $ \db -> getRemovedMembersToCleanup db cxt user cutoffTs forM_ removedMembers $ \m -> withStore' (\db -> deleteGroupMember db user m) `catchAllErrors` eToView cleanupMessages = do @@ -5095,8 +5095,8 @@ runRelayGroupLinkChecks user = do liftIO $ threadDelay' $ diffToMicroseconds interval where checkRelayServedGroups = do - vr <- chatVersionRange - relayGroups <- withStore' $ \db -> getRelayServedGroups db vr user + cxt <- chatStoreCxt + relayGroups <- withStore' $ \db -> getRelayServedGroups db cxt user forM_ relayGroups $ \gInfo@GroupInfo {groupProfile = gp} -> flip catchAllErrors eToView $ do case publicGroup gp of Just PublicGroupProfile {groupLink = sLnk} -> do @@ -5114,24 +5114,24 @@ runRelayGroupLinkChecks user = do _ -> pure () _ -> pure () checkRelayInactiveGroups = do - vr <- chatVersionRange + cxt <- chatStoreCxt ttl <- asks (relayInactiveTTL . config) - inactiveGroups <- withStore' $ \db -> getRelayInactiveGroups db vr user ttl + inactiveGroups <- withStore' $ \db -> getRelayInactiveGroups db cxt user ttl forM_ inactiveGroups $ \gInfo -> flip catchAllErrors eToView $ deleteGroupConnections user gInfo False expireChatItems :: User -> Int64 -> Bool -> CM () expireChatItems user@User {userId} globalTTL sync = do currentTs <- liftIO getCurrentTime - vr <- chatVersionRange + cxt <- chatStoreCxt -- this is to keep group messages created during last 12 hours even if they're expired according to item_ts let createdAtCutoff = addUTCTime (-43200 :: NominalDiffTime) currentTs lift waitChatStartedAndActivated contactIds <- withStore' $ \db -> getUserContactsToExpire db user globalTTL - loop contactIds $ expireContactChatItems user vr globalTTL + loop contactIds $ expireContactChatItems user cxt globalTTL lift waitChatStartedAndActivated groupIds <- withStore' $ \db -> getUserGroupsToExpire db user globalTTL - loop groupIds $ expireGroupChatItems user vr globalTTL createdAtCutoff + loop groupIds $ expireGroupChatItems user cxt globalTTL createdAtCutoff where loop :: [Int64] -> (Int64 -> CM ()) -> CM () loop [] _ = pure () @@ -5147,11 +5147,11 @@ expireChatItems user@User {userId} globalTTL sync = do expire <- atomically $ TM.lookup userId expireFlags when (expire == Just True) $ threadDelay 100000 >> a -expireContactChatItems :: User -> VersionRangeChat -> Int64 -> ContactId -> CM () -expireContactChatItems user vr globalTTL ctId = +expireContactChatItems :: User -> StoreCxt -> Int64 -> ContactId -> CM () +expireContactChatItems user cxt globalTTL ctId = -- reading contacts and groups inside the loop, -- to allow ttl changing while processing and to reduce memory usage - tryAllErrors (withStore $ \db -> getContact db vr user ctId) >>= mapM_ process + tryAllErrors (withStore $ \db -> getContact db cxt user ctId) >>= mapM_ process where process ct@Contact {chatItemTTL} = withExpirationDate globalTTL chatItemTTL $ \expirationDate -> do @@ -5160,9 +5160,9 @@ expireContactChatItems user vr globalTTL ctId = deleteCIFiles user filesInfo withStore' $ \db -> deleteContactExpiredCIs db user ct expirationDate -expireGroupChatItems :: User -> VersionRangeChat -> Int64 -> UTCTime -> GroupId -> CM () -expireGroupChatItems user vr globalTTL createdAtCutoff groupId = - tryAllErrors (withStore $ \db -> getGroupInfo db vr user groupId) >>= mapM_ process +expireGroupChatItems :: User -> StoreCxt -> Int64 -> UTCTime -> GroupId -> CM () +expireGroupChatItems user cxt globalTTL createdAtCutoff groupId = + tryAllErrors (withStore $ \db -> getGroupInfo db cxt user groupId) >>= mapM_ process where process gInfo@GroupInfo {chatItemTTL} = withExpirationDate globalTTL chatItemTTL $ \expirationDate -> do @@ -5170,7 +5170,7 @@ expireGroupChatItems user vr globalTTL createdAtCutoff groupId = filesInfo <- withStore' $ \db -> getGroupExpiredFileInfo db user gInfo expirationDate createdAtCutoff deleteCIFiles user filesInfo withStore' $ \db -> deleteGroupExpiredCIs db user gInfo expirationDate createdAtCutoff - membersToDelete <- withStore' $ \db -> getGroupMembersForExpiration db vr user gInfo + membersToDelete <- withStore' $ \db -> getGroupMembersForExpiration db cxt user gInfo forM_ membersToDelete $ \m -> withStore' $ \db -> deleteGroupMember db user m withExpirationDate :: Int64 -> Maybe Int64 -> (UTCTime -> CM ()) -> CM () diff --git a/src/Simplex/Chat/Library/Internal.hs b/src/Simplex/Chat/Library/Internal.hs index a2f12aff83..8317ac81d1 100644 --- a/src/Simplex/Chat/Library/Internal.hs +++ b/src/Simplex/Chat/Library/Internal.hs @@ -482,12 +482,12 @@ deleteGroupCIs user gInfo chatScopeInfo items byGroupMember_ deletedTs = do deleteCIFiles user ciFilesInfo (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (deleteItem db) items) unless (null errs) $ toView $ CEvtChatErrors errs - vr <- chatVersionRange + cxt <- chatStoreCxt deletions' <- case chatScopeInfo of Nothing -> pure deletions Just scopeInfo@GCSIMemberSupport {groupMember_} -> do let decStats = countDeletedUnreadItems groupMember_ deletions - gInfo' <- withFastStore' $ \db -> updateGroupScopeUnreadStats db vr user gInfo scopeInfo decStats + gInfo' <- withFastStore' $ \db -> updateGroupScopeUnreadStats db cxt user gInfo scopeInfo decStats pure $ map (updateDeletionGroupInfo gInfo') deletions pure deletions' where @@ -696,7 +696,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI unless (fileStatus == RFSNew) $ case fileStatus of RFSCancelled _ -> throwChatError $ CEFileCancelled fName _ -> throwChatError $ CEFileAlreadyReceiving fName - vr <- chatVersionRange + cxt <- chatStoreCxt case (xftpRcvFile, fileConnReq) of -- XFTP (Just XFTPRcvFile {userApprovedRelays = approvedBeforeReady}, _) -> do @@ -705,7 +705,7 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI (ci, rfd) <- withStore $ \db -> do -- marking file as accepted and reading description in the same transaction -- to prevent race condition with appending description - ci <- xftpAcceptRcvFT db vr user fileId filePath userApproved + ci <- xftpAcceptRcvFT db cxt user fileId filePath userApproved rfd <- getRcvFileDescrByRcvFileId db fileId pure (ci, rfd) receiveViaCompleteFD user fileId rfd userApproved cryptoArgs @@ -716,10 +716,10 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI chatRef <- withStore $ \db -> getChatRefByFileId db user fileId case (chatRef, grpMemberId) of (ChatRef CTDirect contactId _, Nothing) -> do - ct <- withStore $ \db -> getContact db vr user contactId + ct <- withStore $ \db -> getContact db cxt user contactId acceptFile $ \msg -> void $ sendDirectContactMessage user ct msg (ChatRef CTGroup groupId _, Just memId) -> do - GroupMember {activeConn} <- withStore $ \db -> getGroupMember db vr user groupId memId + GroupMember {activeConn} <- withStore $ \db -> getGroupMember db cxt user groupId memId case activeConn of Just conn -> do acceptFile $ \msg -> void $ sendDirectMemberMessage conn msg groupId @@ -730,12 +730,12 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI acceptFile send = do filePath <- getRcvFilePath fileId filePath_ fName True inline <- receiveInline - vr <- chatVersionRange + cxt <- chatStoreCxt if | inline -> do -- accepting inline (ci, sharedMsgId) <- withStore $ \db -> - liftM2 (,) (acceptRcvInlineFT db vr user fileId filePath) (getSharedMsgIdByFileId db userId fileId) + liftM2 (,) (acceptRcvInlineFT db cxt user fileId filePath) (getSharedMsgIdByFileId db userId fileId) send $ XFileAcptInv sharedMsgId Nothing fName pure ci | fileInline == Just IFMSent -> throwChatError $ CEFileAlreadyReceiving fName @@ -811,13 +811,13 @@ getNetworkConfig = withAgent' $ liftIO . getFastNetworkConfig resetRcvCIFileStatus :: User -> FileTransferId -> CIFileStatus 'MDRcv -> CM (Maybe AChatItem) resetRcvCIFileStatus user fileId ciFileStatus = do - vr <- chatVersionRange + cxt <- chatStoreCxt withStore $ \db -> do liftIO $ do updateCIFileStatus db user fileId ciFileStatus updateRcvFileStatus db fileId FSNew updateRcvFileAgentId db fileId Nothing - lookupChatItemByFileId db vr user fileId + lookupChatItemByFileId db cxt user fileId receiveViaURI :: User -> FileDescriptionURI -> CryptoFile -> CM RcvFileTransfer receiveViaURI user@User {userId} FileDescriptionURI {description} cf@CryptoFile {cryptoArgs} = do @@ -835,11 +835,11 @@ receiveViaURI user@User {userId} FileDescriptionURI {description} cf@CryptoFile startReceivingFile :: User -> FileTransferId -> CM () startReceivingFile user fileId = do - vr <- chatVersionRange + cxt <- chatStoreCxt ci <- withStore $ \db -> do liftIO $ updateRcvFileStatus db fileId FSConnected liftIO $ updateCIFileStatus db user fileId $ CIFSRcvTransfer 0 1 - getChatItemByFileId db vr user fileId + getChatItemByFileId db cxt user fileId toView $ CEvtRcvFileStart user ci getRcvFilePath :: FileTransferId -> Maybe FilePath -> String -> Bool -> CM FilePath @@ -890,8 +890,8 @@ acceptContactRequest nm user@User {userId} UserContactRequest {agentInvitationId subMode <- chatReadVar subscriptionMode let pqSup = PQSupportOn pqSup' = pqSup `CR.pqSupportAnd` pqSupport - vr <- chatVersionRange - let chatV = vr `peerConnChatVersion` cReqChatVRange + cxt <- chatStoreCxt + let chatV = vr cxt `peerConnChatVersion` cReqChatVRange (ct, conn, incognitoProfile) <- case contactId_ of Nothing -> do incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing @@ -900,7 +900,7 @@ acceptContactRequest nm user@User {userId} UserContactRequest {agentInvitationId 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 + ct <- withFastStore $ \db -> getContact db cxt user contactId case contactConn ct of Nothing -> do incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing @@ -926,15 +926,15 @@ acceptContactRequestAsync incognitoProfile = do subMode <- chatReadVar subscriptionMode let profileToSend = userProfileDirect user (fromIncognitoProfile <$> incognitoProfile) (Just ct) True - vr <- chatVersionRange - let chatV = vr `peerConnChatVersion` cReqChatVRange + cxt <- chatStoreCxt + let chatV = vr cxt `peerConnChatVersion` cReqChatVRange (cmdId, acId) <- agentAcceptContactAsync user True cReqInvId (XInfo profileToSend) subMode cReqPQSup chatV currentTs <- liftIO getCurrentTime withStore $ \db -> do forM_ xContactId $ \xcId -> liftIO $ setContactAcceptedXContactId db ct xcId Connection {connId} <- liftIO $ createAcceptedContactConn db user (Just uclId) contactId acId chatV cReqChatVRange cReqPQSup incognitoProfile subMode currentTs liftIO $ setCommandConnId db user cmdId connId - getContact db vr user contactId + getContact db cxt user contactId acceptGroupJoinRequestAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> Maybe MemberId -> Maybe SharedMsgId -> GroupAcceptance -> GroupMemberRole -> Maybe IncognitoProfile -> Maybe MemberKey -> CM GroupMember acceptGroupJoinRequestAsync @@ -970,12 +970,12 @@ acceptGroupJoinRequestAsync groupSize = Just currentMemCount } subMode <- chatReadVar subscriptionMode - vr <- chatVersionRange - let chatV = vr `peerConnChatVersion` cReqChatVRange + cxt <- chatStoreCxt + let chatV = vr cxt `peerConnChatVersion` cReqChatVRange connIds <- agentAcceptContactAsync user True cReqInvId msg subMode PQSupportOff chatV withStore $ \db -> do liftIO $ createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode - getGroupMemberById db vr user groupMemberId + getGroupMemberById db cxt user groupMemberId acceptGroupJoinSendRejectAsync :: User -> Int64 -> GroupInfo -> InvitationId -> VersionRangeChat -> Profile -> Maybe XContactId -> GroupRejectionReason -> CM GroupMember acceptGroupJoinSendRejectAsync @@ -1000,12 +1000,12 @@ acceptGroupJoinSendRejectAsync rejectionReason } subMode <- chatReadVar subscriptionMode - vr <- chatVersionRange - let chatV = vr `peerConnChatVersion` cReqChatVRange + cxt <- chatStoreCxt + let chatV = vr cxt `peerConnChatVersion` cReqChatVRange connIds <- agentAcceptContactAsync user False cReqInvId msg subMode PQSupportOff chatV withStore $ \db -> do liftIO $ createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode - getGroupMemberById db vr user groupMemberId + getGroupMemberById db cxt user groupMemberId acceptBusinessJoinRequestAsync :: User -> Int64 -> GroupInfo -> GroupMember -> UserContactRequest -> CM (GroupInfo, GroupMember) acceptBusinessJoinRequestAsync @@ -1014,7 +1014,7 @@ acceptBusinessJoinRequestAsync gInfo@GroupInfo {membership = GroupMember {memberRole = userRole, memberId = userMemberId}} clientMember@GroupMember {groupMemberId, memberId} UserContactRequest {agentInvitationId = AgentInvId cReqInvId, cReqChatVRange, xContactId} = do - vr <- chatVersionRange + cxt <- chatStoreCxt let userProfile@Profile {displayName, preferences} = fromLocalProfile $ profile' user -- TODO [short links] take groupPreferences from group info groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs preferences @@ -1033,7 +1033,7 @@ acceptBusinessJoinRequestAsync groupSize = Just 1 } subMode <- chatReadVar subscriptionMode - let chatV = vr `peerConnChatVersion` cReqChatVRange + let chatV = vr cxt `peerConnChatVersion` cReqChatVRange connIds <- agentAcceptContactAsync user True cReqInvId msg subMode PQSupportOff chatV withStore' $ \db -> do forM_ xContactId $ \xcId -> setBusinessChatAcceptedXContactId db gInfo xcId @@ -1057,28 +1057,28 @@ acceptRelayJoinRequestAsync -- TODO [channel web] derive RelayCapabilities from relay config (RelayWebOptions) let msg = XGrpRelayAcpt relayLink defaultRelayCapabilities subMode <- chatReadVar subscriptionMode - vr <- chatVersionRange - let chatV = vr `peerConnChatVersion` cReqChatVRange + cxt <- chatStoreCxt + let chatV = vr cxt `peerConnChatVersion` cReqChatVRange connIds <- agentAcceptContactAsync user True cReqInvId msg subMode PQSupportOff chatV withStore $ \db -> do liftIO $ createJoiningMemberConnection db user uclId connIds chatV cReqChatVRange groupMemberId subMode gInfo' <- liftIO $ updateRelayOwnStatusFromTo db gInfo RSInvited RSAccepted - ownerMember' <- getGroupMemberById db vr user groupMemberId + ownerMember' <- getGroupMemberById db cxt user groupMemberId pure (gInfo', ownerMember') rejectRelayInvitationAsync :: User -> Int64 - -> VersionRangeChat + -> StoreCxt -> GroupRelayInvitation -> InvitationId -> VersionRangeChat -> Int64 -> RelayRejectionReason -> CM () -rejectRelayInvitationAsync user uclId vr groupRelayInv invId reqChatVRange initialDelay reason = do +rejectRelayInvitationAsync user uclId cxt groupRelayInv invId reqChatVRange initialDelay reason = do (_gInfo, ownerMember) <- withStore $ \db -> - createRelayRequestGroup db vr user groupRelayInv invId reqChatVRange initialDelay GSMemInvited RSRejected + createRelayRequestGroup db cxt user groupRelayInv invId reqChatVRange initialDelay GSMemInvited RSRejected let GroupMember {groupMemberId} = ownerMember msg = XGrpRelayReject reason subMode <- chatReadVar subscriptionMode @@ -1092,15 +1092,15 @@ businessGroupProfile :: Profile -> GroupPreferences -> GroupProfile businessGroupProfile Profile {displayName, fullName, shortDescr, image} groupPreferences = GroupProfile {displayName, fullName, description = Nothing, shortDescr, image, publicGroup = Nothing, simplexName = Nothing, groupPreferences = Just groupPreferences, memberAdmission = Nothing} -introduceToModerators :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM () -introduceToModerators vr user gInfo@GroupInfo {groupId} m@GroupMember {memberRole, memberId} = do +introduceToModerators :: StoreCxt -> User -> GroupInfo -> GroupMember -> CM () +introduceToModerators cxt user gInfo@GroupInfo {groupId} m@GroupMember {memberRole, memberId} = do forM_ (memberConn m) $ \mConn -> do let msg = if maxVersion (memberChatVRange m) >= groupKnockingVersion then XGrpLinkAcpt GAPendingReview memberRole memberId else XMsgNew $ mcSimple (MCText pendingReviewMessage) void $ sendDirectMemberMessage mConn msg groupId - modMs <- withStore' $ \db -> getGroupModerators db vr user gInfo + modMs <- withStore' $ \db -> getGroupModerators db cxt user gInfo let rcpModMs = filter shouldIntroduceToMod modMs introduceMember user gInfo m rcpModMs (Just $ MSMember $ memberId' m) where @@ -1110,15 +1110,15 @@ introduceToModerators vr user gInfo@GroupInfo {groupId} m@GroupMember {memberRol && groupMemberId' mem /= groupMemberId' m && maxVersion (memberChatVRange mem) >= groupKnockingVersion -introduceToAll :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM () -introduceToAll vr user gInfo m = do - (members, vector) <- withStore $ \db -> liftM2 (,) (liftIO $ getGroupMembers db vr user gInfo) (getMemberRelationsVector db m) +introduceToAll :: StoreCxt -> User -> GroupInfo -> GroupMember -> CM () +introduceToAll cxt user gInfo m = do + (members, vector) <- withStore $ \db -> liftM2 (,) (liftIO $ getGroupMembers db cxt user gInfo) (getMemberRelationsVector db m) let recipients = filter (shouldIntroduce m vector) members introduceMember user gInfo m recipients Nothing -introduceToRemaining :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM () -introduceToRemaining vr user gInfo m = do - (members, vector) <- withStore $ \db -> liftM2 (,) (liftIO $ getGroupMembers db vr user gInfo) (getMemberRelationsVector db m) +introduceToRemaining :: StoreCxt -> User -> GroupInfo -> GroupMember -> CM () +introduceToRemaining cxt user gInfo m = do + (members, vector) <- withStore $ \db -> liftM2 (,) (liftIO $ getGroupMembers db cxt user gInfo) (getMemberRelationsVector db m) let recipients = filter (shouldIntroduce m vector) members introduceMember user gInfo m recipients Nothing @@ -1172,10 +1172,10 @@ memberIntroEvt gInfo reMember = -- Used in groups with relays to introduce moderators and above to a new member, -- and to announce the new member to moderators and above. -- This doesn't create introduction records in db, compared to above methods. -introduceInChannel :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> CM () +introduceInChannel :: StoreCxt -> User -> GroupInfo -> GroupMember -> CM () introduceInChannel _ _ _ GroupMember {activeConn = Nothing} = throwChatError $ CEInternalError "member connection not active" -introduceInChannel vr user gInfo subscriber@GroupMember {activeConn = Just conn, indexInGroup = subscriberIdx} = do - modMs <- withStore' $ \db -> getGroupModerators db vr user gInfo +introduceInChannel cxt user gInfo subscriber@GroupMember {activeConn = Just conn, indexInGroup = subscriberIdx} = do + modMs <- withStore' $ \db -> getGroupModerators db cxt user gInfo void $ sendGroupMessage' user gInfo modMs $ XGrpMemNew (memberInfo gInfo subscriber) Nothing withStore' $ \db -> setMemberVectorNewRelations db subscriber [(indexInGroup m, (IDSubjectIntroduced, MRIntroduced)) | m <- modMs] @@ -1338,9 +1338,9 @@ setGroupLinkData' nm user gInfo = setGroupLinkData :: NetworkRequestMode -> User -> GroupInfo -> GroupLink -> CM GroupLink setGroupLinkData nm user gInfo gLink = do - vr <- chatVersionRange + cxt <- chatStoreCxt (conn, groupRelays) <- withFastStore $ \db -> - (,) <$> getGroupLinkConnection db vr user gInfo <*> liftIO (getConnectedGroupRelays db gInfo) + (,) <$> getGroupLinkConnection db cxt user gInfo <*> liftIO (getConnectedGroupRelays db gInfo) let (userLinkData, crClientData) = groupLinkData gInfo gLink groupRelays linkType = if useRelays' gInfo then CCTChannel else CCTGroup sLnk <- shortenShortLink' . setShortLinkType_ linkType =<< withAgent (\a -> setConnShortLink a nm (aConnId conn) SCMContact userLinkData (Just crClientData)) @@ -1348,17 +1348,17 @@ setGroupLinkData nm user gInfo gLink = do setGroupLinkDataAsync :: User -> GroupInfo -> GroupLink -> CM () setGroupLinkDataAsync user gInfo gLink = do - vr <- chatVersionRange + cxt <- chatStoreCxt (conn, groupRelays) <- withStore $ \db -> - (,) <$> getGroupLinkConnection db vr user gInfo <*> liftIO (getConnectedGroupRelays db gInfo) + (,) <$> getGroupLinkConnection db cxt user gInfo <*> liftIO (getConnectedGroupRelays db gInfo) let (userLinkData, crClientData) = groupLinkData gInfo gLink groupRelays setAgentConnShortLinkAsync user conn userLinkData (Just crClientData) connectToRelayAsync :: User -> GroupInfo -> ShortLinkContact -> CM () connectToRelayAsync user gInfo relayLink = do - vr <- chatVersionRange + cxt <- chatStoreCxt gVar <- asks random - relayMember@GroupMember {activeConn} <- withFastStore $ \db -> getCreateRelayForMember db vr gVar user gInfo relayLink + relayMember@GroupMember {activeConn} <- withFastStore $ \db -> getCreateRelayForMember db cxt gVar user gInfo relayLink case activeConn of Just _ -> pure () Nothing -> do @@ -1369,9 +1369,9 @@ connectToRelayAsync user gInfo relayLink = do updatePublicGroupData :: User -> GroupInfo -> CM GroupInfo updatePublicGroupData user gInfo | useRelays' gInfo && memberRole' (membership gInfo) == GROwner = do - vr <- chatVersionRange + cxt <- chatStoreCxt (gInfo', gLink) <- withStore $ \db -> do - gInfo' <- updatePublicMemberCount db vr user gInfo + gInfo' <- updatePublicMemberCount db cxt user gInfo gLink <- getGroupLink db user gInfo' pure (gInfo', gLink) setGroupLinkDataAsync user gInfo' gLink @@ -1381,12 +1381,12 @@ updatePublicGroupData user gInfo updateGroupFromLinkData :: User -> GroupInfo -> GroupShortLinkData -> CM (GroupInfo, Bool) updateGroupFromLinkData user gInfo@GroupInfo {groupProfile = p, groupSummary = GroupSummary {publicMemberCount = localCount}} GroupShortLinkData {groupProfile, publicGroupData} | profileChanged || countChanged = do - vr <- chatVersionRange + cxt <- chatStoreCxt withStore $ \db -> do g <- if profileChanged then updateGroupProfile db user gInfo groupProfile else pure gInfo g' <- case publicGroupData of Just PublicGroupData {publicMemberCount} | countChanged -> - setPublicMemberCount db vr user g publicMemberCount + setPublicMemberCount db cxt user g publicMemberCount _ -> pure g pure (g', profileChanged) | otherwise = pure (gInfo, False) @@ -1465,14 +1465,14 @@ shortenCreatedLink (CCLink cReq sLnk) = CCLink cReq <$> mapM shortenShortLink' s deleteGroupLink' :: User -> GroupInfo -> CM () deleteGroupLink' user gInfo = do - vr <- chatVersionRange - conn <- withStore $ \db -> getGroupLinkConnection db vr user gInfo + cxt <- chatStoreCxt + conn <- withStore $ \db -> getGroupLinkConnection db cxt user gInfo deleteGroupLink_ user gInfo conn deleteGroupLinkIfExists :: User -> GroupInfo -> CM () deleteGroupLinkIfExists user gInfo = do - vr <- chatVersionRange - conn_ <- eitherToMaybe <$> withStore' (\db -> runExceptT $ getGroupLinkConnection db vr user gInfo) + cxt <- chatStoreCxt + conn_ <- eitherToMaybe <$> withStore' (\db -> runExceptT $ getGroupLinkConnection db cxt user gInfo) mapM_ (deleteGroupLink_ user gInfo) conn_ deleteGroupLink_ :: User -> GroupInfo -> Connection -> CM () @@ -1507,16 +1507,16 @@ deleteTimedItem user (ChatRef cType chatId scope, itemId) deleteAt = do ts <- liftIO getCurrentTime liftIO $ threadDelay' $ diffToMicroseconds $ diffUTCTime deleteAt ts lift waitChatStartedAndActivated - vr <- chatVersionRange + cxt <- chatStoreCxt case cType of CTDirect -> do - (ct, ci) <- withStore $ \db -> (,) <$> getContact db vr user chatId <*> getDirectChatItem db user chatId itemId + (ct, ci) <- withStore $ \db -> (,) <$> getContact db cxt user chatId <*> getDirectChatItem db user chatId itemId deletions <- deleteDirectCIs user ct [ci] toView $ CEvtChatItemsDeleted user deletions True True CTGroup -> do - (gInfo, ci) <- withStore $ \db -> (,) <$> getGroupInfo db vr user chatId <*> getGroupChatItem db user chatId itemId + (gInfo, ci) <- withStore $ \db -> (,) <$> getGroupInfo db cxt user chatId <*> getGroupChatItem db user chatId itemId deletedTs <- liftIO getCurrentTime - chatScopeInfo <- mapM (getChatScopeInfo vr user) scope + chatScopeInfo <- mapM (getChatScopeInfo cxt user) scope deletions <- deleteGroupCIs user gInfo chatScopeInfo [ci] Nothing deletedTs toView $ CEvtChatItemsDeleted user deletions True True _ -> eToView $ ChatError $ CEInternalError "bad deleteTimedItem cType" @@ -1633,25 +1633,25 @@ parseChatMessage conn s = do errType = CEInvalidChatMessage conn Nothing (safeDecodeUtf8 s) {-# INLINE parseChatMessage #-} -getChatScopeInfo :: VersionRangeChat -> User -> GroupChatScope -> CM GroupChatScopeInfo -getChatScopeInfo vr user = \case +getChatScopeInfo :: StoreCxt -> User -> GroupChatScope -> CM GroupChatScopeInfo +getChatScopeInfo cxt user = \case GCSMemberSupport Nothing -> pure $ GCSIMemberSupport Nothing GCSMemberSupport (Just gmId) -> do - supportMem <- withFastStore $ \db -> getGroupMemberById db vr user gmId + supportMem <- withFastStore $ \db -> getGroupMemberById db cxt user gmId pure $ GCSIMemberSupport (Just supportMem) -getGroupRecipients :: VersionRangeChat -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> VersionChat -> CM [GroupMember] -getGroupRecipients vr user gInfo@GroupInfo {membership} scopeInfo modsCompatVersion +getGroupRecipients :: StoreCxt -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> VersionChat -> CM [GroupMember] +getGroupRecipients cxt user gInfo@GroupInfo {membership} scopeInfo modsCompatVersion | useRelays' gInfo && not (isRelay membership) = do unless (memberCurrent membership && memberActive membership) $ throwChatError $ CECommandError "not current member" - withFastStore' $ \db -> getGroupRelayMembers db vr user gInfo + withFastStore' $ \db -> getGroupRelayMembers db cxt user gInfo | otherwise = case scopeInfo of Nothing -> do unless (memberCurrent membership && memberActive membership) $ throwChatError $ CECommandError "not current member" - ms <- withFastStore' $ \db -> getGroupMembers db vr user gInfo + ms <- withFastStore' $ \db -> getGroupMembers db cxt user gInfo pure $ filter memberCurrent ms Just (GCSIMemberSupport Nothing) -> do - modMs <- withFastStore' $ \db -> getGroupModerators db vr user gInfo + modMs <- withFastStore' $ \db -> getGroupModerators db cxt user gInfo let rcpModMs' = filter (\m -> compatible m && memberCurrent m) modMs when (null rcpModMs') $ throwChatError $ CECommandError "no admins support this message" pure rcpModMs' @@ -1661,7 +1661,7 @@ getGroupRecipients vr user gInfo@GroupInfo {membership} scopeInfo modsCompatVers if memberStatus supportMem == GSMemPendingApproval then pure [supportMem] else do - modMs <- withFastStore' $ \db -> getGroupModerators db vr user gInfo + modMs <- withFastStore' $ \db -> getGroupModerators db cxt user gInfo let rcpModMs' = filter (\m -> compatible m && memberCurrent m) modMs pure $ [supportMem] <> rcpModMs' where @@ -1687,8 +1687,8 @@ mkGroupChatScope gInfo@GroupInfo {membership} m | otherwise = pure (gInfo, m, Nothing) -mkGetMessageChatScope :: VersionRangeChat -> User -> GroupInfo -> GroupMember -> MsgContent -> Maybe MsgScope -> CM (GroupInfo, GroupMember, Maybe GroupChatScopeInfo) -mkGetMessageChatScope vr user gInfo@GroupInfo {membership} m mc msgScope_ = +mkGetMessageChatScope :: StoreCxt -> User -> GroupInfo -> GroupMember -> MsgContent -> Maybe MsgScope -> CM (GroupInfo, GroupMember, Maybe GroupChatScopeInfo) +mkGetMessageChatScope cxt user gInfo@GroupInfo {membership} m mc msgScope_ = mkGroupChatScope gInfo m >>= \case groupScope@(_gInfo', _m', Just _scopeInfo) -> pure groupScope (_, _, Nothing) @@ -1703,7 +1703,7 @@ mkGetMessageChatScope vr user gInfo@GroupInfo {membership} m mc msgScope_ = (gInfo', scopeInfo) <- mkGroupSupportChatInfo gInfo pure (gInfo', m, Just scopeInfo) | otherwise -> do - referredMember <- withStore $ \db -> getGroupMemberByMemberId db vr user gInfo mId + referredMember <- withStore $ \db -> getGroupMemberByMemberId db cxt user gInfo mId -- TODO [knocking] return patched _referredMember'? (_referredMember', scopeInfo) <- mkMemberSupportChatInfo referredMember pure (gInfo, m, Just scopeInfo) @@ -1817,8 +1817,8 @@ cancelSndFileTransfer user@User {userId} ft@SndFileTransfer {fileId, connId, fil withStore' $ \db -> updateSndFileStatus db ft FSCancelled when sendCancel $ case fileInline of Just _ -> do - vr <- chatVersionRange - (sharedMsgId, conn) <- withStore $ \db -> (,) <$> getSharedMsgIdByFileId db userId fileId <*> getConnectionById db vr user connId + cxt <- chatStoreCxt + (sharedMsgId, conn) <- withStore $ \db -> (,) <$> getSharedMsgIdByFileId db userId fileId <*> getConnectionById db cxt user connId void $ sendDirectMessage_ conn (BFileChunk sharedMsgId FileChunkCancel) (ConnectionId connId) _ -> throwChatError $ CEException "cancelSndFileTransfer: cancelling file via a separate connection is deprecated" @@ -2017,13 +2017,13 @@ batchSndMessagesJSON mode = batchMessages mode maxEncodedMsgLength . L.toList encodeConnInfo :: MsgEncodingI e => ChatMsgEvent e -> CM ByteString encodeConnInfo chatMsgEvent = do - vr <- chatVersionRange - encodeConnInfoPQ PQSupportOff (maxVersion vr) chatMsgEvent + cxt <- chatStoreCxt + encodeConnInfoPQ PQSupportOff (maxVersion (vr cxt)) chatMsgEvent encodeConnInfoPQ :: MsgEncodingI e => PQSupport -> VersionChat -> ChatMsgEvent e -> CM ByteString encodeConnInfoPQ pqSup v chatMsgEvent = do - vr <- chatVersionRange - let info = ChatMessage {chatVRange = vr, msgId = Nothing, chatMsgEvent} + cxt <- chatStoreCxt + let info = ChatMessage {chatVRange = vr cxt, msgId = Nothing, chatMsgEvent} case encodeChatMessage maxEncodedInfoLength info of ECMEncoded connInfo -> case pqSup of PQSupportOn | v >= pqEncryptionCompressionVersion && B.length connInfo > maxCompressedInfoLength -> do @@ -2337,8 +2337,8 @@ saveGroupRcvMsg user groupId authorMember conn@Connection {connId} agentMsgMeta withStore (\db -> createNewMessageAndRcvMsgDelivery db (GroupId groupId) newMsg sharedMsgId_ rcvMsgDelivery $ Just amGroupMemId) `catchAllErrors` \e -> case e of ChatErrorStore (SEDuplicateGroupMessage _ _ _ (Just forwardedByGroupMemberId)) -> do - vr <- chatVersionRange - fm <- withStore $ \db -> getGroupMember db vr user groupId forwardedByGroupMemberId + cxt <- chatStoreCxt + fm <- withStore $ \db -> getGroupMember db cxt user groupId forwardedByGroupMemberId forM_ (memberConn fm) $ \fmConn -> void $ sendDirectMemberMessage fmConn (XGrpMemCon amMemId) groupId throwError e @@ -2358,8 +2358,8 @@ saveGroupFwdRcvMsg user gInfo@GroupInfo {groupId} forwardingMember refAuthorMemb | useRelays' gInfo -> pure Nothing -- with chat relays, duplicates are expected | otherwise -> case (authorGroupMemberId, forwardedByGroupMemberId) of (Just authorGMId, Nothing) -> do - vr <- chatVersionRange - am@GroupMember {memberId = amMemberId} <- withStore $ \db -> getGroupMember db vr user groupId authorGMId + cxt <- chatStoreCxt + am@GroupMember {memberId = amMemberId} <- withStore $ \db -> getGroupMember db cxt user groupId authorGMId if maybe False (\ref -> sameMemberId (memberId' ref) am) refAuthorMember_ then forM_ (memberConn forwardingMember) $ \fmConn -> void $ sendDirectMemberMessage fmConn (XGrpMemCon amMemberId) groupId @@ -2401,9 +2401,9 @@ saveSndChatItems :: CM [Either ChatError (ChatItem c 'MDSnd)] saveSndChatItems user cd showGroupAsSender itemsData itemTimed live = do createdAt <- liftIO getCurrentTime - vr <- chatVersionRange + cxt <- chatStoreCxt when (contactChatDeleted cd || any (\NewSndChatItemData {content} -> ciRequiresAttention content) (rights itemsData)) $ - void (withStore' $ \db -> updateChatTsStats db vr user cd createdAt Nothing) + void (withStore' $ \db -> updateChatTsStats db cxt user cd createdAt Nothing) lift $ withStoreBatch (\db -> map (bindRight $ createItem db createdAt) itemsData) where createItem :: DB.Connection -> UTCTime -> NewSndChatItemData c -> IO (Either ChatError (ChatItem c 'MDSnd)) @@ -2429,14 +2429,14 @@ ciContentNoParse content = (content, (ciContentToText content, Nothing)) saveRcvChatItem' :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c 'MDRcv -> RcvMessage -> Maybe SharedMsgId -> UTCTime -> (CIContent 'MDRcv, (Text, Maybe MarkdownList)) -> Maybe (CIFile 'MDRcv) -> Maybe CITimed -> Bool -> Map MemberName MsgMention -> CM (ChatItem c 'MDRcv, ChatInfo c) saveRcvChatItem' user cd msg@RcvMessage {chatMsgEvent, msgSigned, forwardedByMember} sharedMsgId_ brokerTs (content, (t, ft_)) ciFile itemTimed live mentions = do createdAt <- liftIO getCurrentTime - vr <- chatVersionRange + cxt <- chatStoreCxt withStore' $ \db -> do (mentions' :: Map MemberName CIMention, userMention) <- case toChatInfo cd of GroupChat g@GroupInfo {membership} _ -> groupMentions db g membership _ -> pure (M.empty, False) cInfo' <- if (ciRequiresAttention content || contactChatDeleted cd) - then updateChatTsStats db vr user cd createdAt (memberChatStats userMention) + then updateChatTsStats db cxt user cd createdAt (memberChatStats userMention) else pure $ toChatInfo cd let showAsGroup = case cd of CDChannelRcv {} -> True; _ -> False hasLink_ = ciContentHasLink content ft_ @@ -2729,13 +2729,13 @@ createChatItems :: createChatItems user itemTs_ dirsCIContents = do createdAt <- liftIO getCurrentTime let itemTs = fromMaybe createdAt itemTs_ - vr <- chatVersionRange' - void . withStoreBatch' $ \db -> map (updateChat db vr createdAt) dirsCIContents + cxt <- chatStoreCxt' + void . withStoreBatch' $ \db -> map (updateChat db cxt createdAt) dirsCIContents withStoreBatch' $ \db -> concatMap (createACIs db itemTs createdAt) dirsCIContents where - updateChat :: DB.Connection -> VersionRangeChat -> UTCTime -> (ChatDirection c d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId)]) -> IO () - updateChat db vr createdAt (cd, _, contents) - | any (ciRequiresAttention . fst) contents || contactChatDeleted cd = void $ updateChatTsStats db vr user cd createdAt memberChatStats + updateChat :: DB.Connection -> StoreCxt -> UTCTime -> (ChatDirection c d, ShowGroupAsSender, [(CIContent d, Maybe SharedMsgId)]) -> IO () + updateChat db cxt createdAt (cd, _, contents) + | any (ciRequiresAttention . fst) contents || contactChatDeleted cd = void $ updateChatTsStats db cxt user cd createdAt memberChatStats | otherwise = pure () where memberChatStats :: Maybe (Int, MemberAttention, Int) @@ -2774,8 +2774,8 @@ createLocalChatItems :: UTCTime -> CM [ChatItem 'CTLocal 'MDSnd] createLocalChatItems user cd itemsData createdAt = do - vr <- chatVersionRange - void $ withStore' $ \db -> updateChatTsStats db vr user cd createdAt Nothing + cxt <- chatStoreCxt + void $ withStore' $ \db -> updateChatTsStats db cxt user cd createdAt Nothing (errs, items) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (createItem db) $ L.toList itemsData) unless (null errs) $ toView $ CEvtChatErrors errs pure items @@ -2825,6 +2825,14 @@ waitChatStartedAndActivated = do activated <- readTVar chatActivated unless (isJust started && activated) retry +chatStoreCxt :: CM StoreCxt +chatStoreCxt = lift chatStoreCxt' +{-# INLINE chatStoreCxt #-} + +chatStoreCxt' :: CM' StoreCxt +chatStoreCxt' = mkStoreCxt <$> asks config +{-# INLINE chatStoreCxt' #-} + chatVersionRange :: CM VersionRangeChat chatVersionRange = lift chatVersionRange' {-# INLINE chatVersionRange #-} diff --git a/src/Simplex/Chat/Library/Subscriber.hs b/src/Simplex/Chat/Library/Subscriber.hs index 015830a850..975b5adf2b 100644 --- a/src/Simplex/Chat/Library/Subscriber.hs +++ b/src/Simplex/Chat/Library/Subscriber.hs @@ -117,10 +117,10 @@ processAgentMessage _ "" (ERR e) = processAgentMessage corrId connId msg = do lockEntity <- critical connId (withStore (`getChatLockEntity` AgentConnId connId)) withEntityLock "processAgentMessage" lockEntity $ do - vr <- chatVersionRange + cxt <- chatStoreCxt -- getUserByAConnId never throws logical errors, only SEDBBusyError can be thrown here critical connId (withStore' (`getUserByAConnId` AgentConnId connId)) >>= \case - Just user -> processAgentMessageConn vr user corrId connId msg `catchAllErrors` eToView + Just user -> processAgentMessageConn cxt user corrId connId msg `catchAllErrors` eToView _ -> throwChatError $ CENoConnectionUser (AgentConnId connId) -- CRITICAL error will be shown to the user as alert with restart button in Android/desktop apps. @@ -182,27 +182,27 @@ processAgentMsgSndFile _corrId aFileId msg = do process :: User -> FileTransferId -> CM () process user fileId = do (ft@FileTransferMeta {xftpRedirectFor, cancelled}, sfts) <- withStore $ \db -> getSndFileTransfer db user fileId - vr <- chatVersionRange + cxt <- chatStoreCxt unless cancelled $ case msg of SFPROG sndProgress sndTotal -> do let status = CIFSSndTransfer {sndProgress, sndTotal} ci <- withStore $ \db -> do liftIO $ updateCIFileStatus db user fileId status - lookupChatItemByFileId db vr user fileId + lookupChatItemByFileId db cxt user fileId toView $ CEvtSndFileProgressXFTP user ci ft sndProgress sndTotal SFDONE sndDescr rfds -> do withStore' $ \db -> setSndFTPrivateSndDescr db user fileId (fileDescrText sndDescr) - ci <- withStore $ \db -> lookupChatItemByFileId db vr user fileId + ci <- withStore $ \db -> lookupChatItemByFileId db cxt user fileId case ci of Nothing -> do lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText rfds) case rfds of - [] -> sendFileError (FileErrOther "no receiver descriptions") "no receiver descriptions" vr ft + [] -> sendFileError (FileErrOther "no receiver descriptions") "no receiver descriptions" cxt ft rfd : _ -> case [fd | fd@(FD.ValidFileDescription FD.FileDescription {chunks = [_]}) <- rfds] of [] -> case xftpRedirectFor of Nothing -> xftpSndFileRedirect user fileId rfd >>= toView . CEvtSndFileRedirectStartXFTP user ft - Just _ -> sendFileError (FileErrOther "chaining redirects") "Prohibit chaining redirects" vr ft + Just _ -> sendFileError (FileErrOther "chaining redirects") "Prohibit chaining redirects" cxt ft rfds' -> do -- we have 1 chunk - use it as URI whether it is redirect or not ft' <- maybe (pure ft) (\fId -> withStore $ \db -> getFileTransferMeta db user fId) xftpRedirectFor @@ -235,13 +235,13 @@ processAgentMsgSndFile _corrId aFileId msg = do sendFileDescriptions (GroupId groupId) rfdsMemberFTs' sharedMsgId ci' <- withStore $ \db -> do liftIO $ updateCIFileStatus db user fileId CIFSSndComplete - getChatItemByFileId db vr user fileId + getChatItemByFileId db cxt user fileId lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) toView $ CEvtSndFileCompleteXFTP user ci' ft where getRecipients - | useRelays' g = withStore' $ \db -> getGroupRelayMembers db vr user g - | otherwise = withStore' $ \db -> getGroupMembers db vr user g + | useRelays' g = withStore' $ \db -> getGroupRelayMembers db cxt user g + | otherwise = withStore' $ \db -> getGroupMembers db cxt user g memberFTs :: [GroupMember] -> [(Connection, SndFileTransfer)] memberFTs ms = M.elems $ M.intersectionWith (,) (M.fromList mConns') (M.fromList sfts') where @@ -254,10 +254,10 @@ processAgentMsgSndFile _corrId aFileId msg = do logWarn $ "Sent file warning: " <> err ci <- withStore $ \db -> do liftIO $ updateCIFileStatus db user fileId (CIFSSndWarning $ agentFileError e) - lookupChatItemByFileId db vr user fileId + lookupChatItemByFileId db cxt user fileId toView $ CEvtSndFileWarning user ci ft err SFERR e -> - sendFileError (agentFileError e) (tshow e) vr ft + sendFileError (agentFileError e) (tshow e) cxt ft where fileDescrText :: FilePartyI p => ValidFileDescription p -> T.Text fileDescrText = safeDecodeUtf8 . strEncode @@ -282,12 +282,12 @@ processAgentMsgSndFile _corrId aFileId msg = do toMsgReq :: (Connection, (ConnOrGroupId, Maybe MsgSigning, ChatMsgEvent 'Json)) -> SndMessage -> ChatMsgReq toMsgReq (conn, _) SndMessage {msgId, msgBody} = (conn, MsgFlags {notification = hasNotification XMsgFileDescr_}, (vrValue msgBody, [msgId])) - sendFileError :: FileError -> Text -> VersionRangeChat -> FileTransferMeta -> CM () - sendFileError ferr err vr ft = do + sendFileError :: FileError -> Text -> StoreCxt -> FileTransferMeta -> CM () + sendFileError ferr err cxt ft = do logError $ "Sent file error: " <> err ci <- withStore $ \db -> do liftIO $ updateFileCancelled db user fileId (CIFSSndError ferr) - lookupChatItemByFileId db vr user fileId + lookupChatItemByFileId db cxt user fileId lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) toView $ CEvtSndFileError user ci ft err @@ -322,13 +322,13 @@ processAgentMsgRcvFile _corrId aFileId msg = do process :: User -> FileTransferId -> CM () process user fileId = do ft <- withStore $ \db -> getRcvFileTransfer db user fileId - vr <- chatVersionRange + cxt <- chatStoreCxt unless (rcvFileCompleteOrCancelled ft) $ case msg of RFPROG rcvProgress rcvTotal -> do let status = CIFSRcvTransfer {rcvProgress, rcvTotal} ci <- withStore $ \db -> do liftIO $ updateCIFileStatus db user fileId status - lookupChatItemByFileId db vr user fileId + lookupChatItemByFileId db cxt user fileId toView $ CEvtRcvFileProgressXFTP user ci rcvProgress rcvTotal ft RFDONE xftpPath -> case liveRcvFileTransferPath ft of @@ -340,13 +340,13 @@ processAgentMsgRcvFile _corrId aFileId msg = do liftIO $ do updateRcvFileStatus db fileId FSComplete updateCIFileStatus db user fileId CIFSRcvComplete - lookupChatItemByFileId db vr user fileId + lookupChatItemByFileId db cxt user fileId agentXFTPDeleteRcvFile aFileId fileId toView $ maybe (CEvtRcvStandaloneFileComplete user fsTargetPath ft) (CEvtRcvFileComplete user) ci_ RFWARN e -> do ci <- withStore $ \db -> do liftIO $ updateCIFileStatus db user fileId (CIFSRcvWarning $ agentFileError e) - lookupChatItemByFileId db vr user fileId + lookupChatItemByFileId db cxt user fileId toView $ CEvtRcvFileWarning user ci e ft RFERR e | e == FILE NOT_APPROVED -> do @@ -357,21 +357,21 @@ processAgentMsgRcvFile _corrId aFileId msg = do | otherwise -> do aci_ <- withStore $ \db -> do liftIO $ updateFileCancelled db user fileId (CIFSRcvError $ agentFileError e) - lookupChatItemByFileId db vr user fileId + lookupChatItemByFileId db cxt user fileId forM_ aci_ cleanupACIFile agentXFTPDeleteRcvFile aFileId fileId toView $ CEvtRcvFileError user aci_ e ft type ShouldDeleteGroupConns = Bool -processAgentMessageConn :: VersionRangeChat -> User -> ACorrId -> ConnId -> AEvent 'AEConn -> CM () -processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = do +processAgentMessageConn :: StoreCxt -> User -> ACorrId -> ConnId -> AEvent 'AEConn -> CM () +processAgentMessageConn cxt user@User {userId} corrId agentConnId agentMessage = do -- Missing connection/entity errors here will be sent to the view but not shown as CRITICAL alert, -- as in this case no need to ACK message - we can't process messages for this connection anyway. -- SEDBBusyError will be re-thrown as CRITICAL (via `critical`) as it indicates a transient lock/IO -- condition that usually resolves after app restart. Other SEDBException flavours surface as -- non-CRITICAL store errors. - entity <- critical agentConnId $ withStore (\db -> getConnectionEntity db vr user $ AgentConnId agentConnId) >>= updateConnStatus + entity <- critical agentConnId $ withStore (\db -> getConnectionEntity db cxt user $ AgentConnId agentConnId) >>= updateConnStatus case agentMessage of END -> case entity of RcvDirectMsgConnection _ (Just ct) -> toView $ CEvtContactAnotherClient user ct @@ -574,7 +574,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- XGrpLinkInv here means we are connecting via business contact card, so we replace contact with group (gInfo, host) <- withStore $ \db -> do liftIO $ deleteContactCardKeepConn db connId ct - createGroupInvitedViaLink db vr user conn'' glInv + createGroupInvitedViaLink db cxt user conn'' glInv void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing (Just epochStart) -- [incognito] send saved profile incognitoProfile <- forM customUserProfileId $ \pId -> withStore (\db -> getProfileById db userId pId) @@ -626,7 +626,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = when (connChatVersion < batchSend2Version) $ forM_ (autoReply $ addressSettings ucl) $ \mc -> sendAutoReply ct' mc Nothing -- old versions only -- TODO REMOVE LEGACY vvv forM_ gli_ $ \GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do - groupInfo <- withStore $ \db -> getGroupInfo db vr user groupId + groupInfo <- withStore $ \db -> getGroupInfo db cxt user groupId subMode <- chatReadVar subscriptionMode groupConnIds <- createAgentConnectionAsync user CFCreateConnGrpInv True SCMInvitation subMode gVar <- asks random @@ -737,7 +737,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- [async agent commands] group link auto-accept continuation on receiving INV CFCreateConnGrpInv -> do (ct, groupLinkId) <- withStore $ \db -> do - ct <- getContactViaMember db vr user m + ct <- getContactViaMember db cxt user m liftIO $ setNewContactMemberConnRequest db user m cReq liftIO $ (ct,) <$> getGroupLinkId db user gInfo sendGrpInvitation ct m groupLinkId @@ -805,7 +805,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = pgId = fmap (\PublicGroupProfile {publicGroupId} -> publicGroupId), useRelays' gInfo == isJust rcvPG && pgId rcvPG == pgId curPG -> do -- XGrpLinkInv here means we are connecting via prepared group, and we have to update user and host member records - (gInfo', m') <- withStore $ \db -> updatePreparedUserAndHostMembersInvited db vr user gInfo m glInv + (gInfo', m') <- withStore $ \db -> updatePreparedUserAndHostMembersInvited db cxt user gInfo m glInv -- [incognito] send saved profile incognitoProfile <- forM customUserProfileId $ \pId -> withStore (\db -> getProfileById db userId pId) let profileToSend = userProfileInGroup user gInfo (fromLocalProfile <$> incognitoProfile) @@ -813,7 +813,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = toView $ CEvtGroupLinkConnecting user gInfo' m' | otherwise -> messageError "x.grp.link.inv: publicGroupId mismatch" XGrpLinkReject glRjct@GroupLinkRejection {rejectionReason} -> do - (gInfo', m') <- withStore $ \db -> updatePreparedUserAndHostMembersRejected db vr user gInfo m glRjct + (gInfo', m') <- withStore $ \db -> updatePreparedUserAndHostMembersRejected db cxt user gInfo m glRjct toView $ CEvtGroupLinkConnecting user gInfo' m' toViewTE $ TEGroupLinkRejected user gInfo' rejectionReason _ -> messageError "CONF from host member in prepared group must have x.grp.link.inv or x.grp.link.reject" @@ -887,7 +887,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = where firstConnectedHost | useRelays' gInfo = do - relayMems <- withStore' $ \db -> getGroupRelayMembers db vr user gInfo + relayMems <- withStore' $ \db -> getGroupRelayMembers db cxt user gInfo let numConnected = length $ filter (\GroupMember {memberStatus = ms} -> ms == GSMemConnected) relayMems pure $ numConnected == 1 | otherwise = pure True @@ -917,13 +917,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = when (connChatVersion < batchSend2Version) $ getAutoReplyMsg >>= mapM_ (\mc -> sendGroupAutoReply mc Nothing) if useRelays' gInfo'' then do - introduceInChannel vr user gInfo'' m' + introduceInChannel cxt user gInfo'' m' when (groupFeatureAllowed SGFHistory gInfo'') $ sendHistory user gInfo'' m' else case mStatus of GSMemPendingApproval -> pure () - GSMemPendingReview -> introduceToModerators vr user gInfo'' m' + GSMemPendingReview -> introduceToModerators cxt user gInfo'' m' _ -> do - introduceToAll vr user gInfo'' m' + introduceToAll cxt user gInfo'' m' let memberIsCustomer = case businessChat gInfo'' of Just BusinessChatInfo {chatType = BCCustomer, customerId} -> memberId' m' == customerId _ -> False @@ -946,12 +946,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = sendXGrpMemCon = \case GCPreMember -> forM_ (invitedByGroupMemberId membership) $ \hostId -> do - host <- withStore $ \db -> getGroupMember db vr user groupId hostId + host <- withStore $ \db -> getGroupMember db cxt user groupId hostId forM_ (memberConn host) $ \hostConn -> void $ sendDirectMemberMessage hostConn (XGrpMemCon memberId) groupId GCPostMember -> forM_ (invitedByGroupMemberId m) $ \invitingMemberId -> do - im <- withStore $ \db -> getGroupMember db vr user groupId invitingMemberId + im <- withStore $ \db -> getGroupMember db cxt user groupId invitingMemberId forM_ (memberConn im) $ \imConn -> void $ sendDirectMemberMessage imConn (XGrpMemCon memberId) groupId _ -> messageWarning "sendXGrpMemCon: member category GCPreMember or GCPostMember is expected" @@ -1211,7 +1211,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = (confId, m', relay) <- withStore $ \db -> do confId <- getRelayConfId db m liftIO $ updateGroupMemberStatus db userId m GSMemAccepted - (m', relay) <- setRelayLinkAccepted db vr user m (MemberKey relayKey) relayProfile + (m', relay) <- setRelayLinkAccepted db cxt user m (MemberKey relayKey) relayProfile pure (confId, m', relay) allowAgentConnectionAsync user conn confId XOk toView $ CEvtGroupRelayUpdated user gInfo m' relay @@ -1299,7 +1299,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = FileChunkCancel -> unless (rcvFileCompleteOrCancelled ft) $ do cancelRcvFileTransfer user ft - ci <- withStore $ \db -> getChatItemByFileId db vr user fileId + ci <- withStore $ \db -> getChatItemByFileId db cxt user fileId toView $ CEvtRcvFileSndCancelled user ci ft FileChunk {chunkNo, chunkBytes = chunk} -> do case integrity of @@ -1322,7 +1322,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = updateRcvFileStatus db fileId FSComplete updateCIFileStatus db user fileId CIFSRcvComplete deleteRcvFileChunks db ft - getChatItemByFileId db vr user fileId + getChatItemByFileId db cxt user fileId toView $ CEvtRcvFileComplete user ci mapM_ (deleteAgentConnectionAsync . aConnId) conn_ RcvChunkDuplicate -> withAckMessage' "file msg" agentConnId meta $ pure () @@ -1347,7 +1347,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = case (ucGroupId_, auData) of (Just groupId, UserContactLinkData UserContactData {relays = relayLinks}) -> do (gInfo, gLink, relays, relaysChanged, newlyActiveLinks) <- withStore $ \db -> do - gInfo <- getGroupInfo db vr user groupId + gInfo <- getGroupInfo db cxt user groupId gLink <- getGroupLink db user gInfo relays <- liftIO $ getGroupRelays db gInfo (relays', changed, newlyActive) <- liftIO $ foldrM (updateRelay db) ([], False, []) relays @@ -1360,7 +1360,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- dedicated subscriber count). when (fromMaybe 0 publicMemberCount > 1) $ forM_ (L.nonEmpty newlyActiveLinks) $ \newlyActive -> do - allRelayMembers <- withFastStore' $ \db -> getGroupRelayMembers db vr user gInfo + allRelayMembers <- withFastStore' $ \db -> getGroupRelayMembers db cxt user gInfo let recipients = filter (\GroupMember {memberStatus, relayLink} -> @@ -1410,7 +1410,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = AddressSettings {autoAccept} = addressSettings isSimplexTeam = sameConnReqContact connReq adminContactReq gVar <- asks random - withStore (\db -> createOrUpdateContactRequest db gVar vr user uclId ucl isSimplexTeam invId chatVRange p xContactId_ welcomeMsgId_ requestMsg_ reqPQSup) >>= \case + withStore (\db -> createOrUpdateContactRequest db gVar cxt user uclId ucl isSimplexTeam invId chatVRange p xContactId_ welcomeMsgId_ requestMsg_ reqPQSup) >>= \case RSAcceptedRequest _ucr re -> case re of REContact ct -> -- TODO [short links] update request msg @@ -1542,7 +1542,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- ##### Group link join requests (don't create contact requests) ##### Just gli@GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do -- TODO [short links] deduplicate request by xContactId? - gInfo <- withStore $ \db -> getGroupInfo db vr user groupId + gInfo <- withStore $ \db -> getGroupInfo db cxt user groupId if useRelays' gInfo then messageWarning $ "processContactConnMessage (group " <> groupName' gInfo <> "): ignored direct join request from " <> displayName <> " (group uses relays)" else do @@ -1568,10 +1568,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = rejected <- withStore' $ \db -> isRelayGroupRejected db user groupLink initialDelay <- asks $ initialInterval . relayRequestRetryInterval . config if rejected - then rejectRelayInvitationAsync user uclId vr groupRelayInv invId chatVRange initialDelay RRRRejoinRejected + then rejectRelayInvitationAsync user uclId cxt groupRelayInv invId chatVRange initialDelay RRRRejoinRejected else do (_gInfo, _ownerMember) <- withStore $ \db -> - createRelayRequestGroup db vr user groupRelayInv invId chatVRange initialDelay GSMemAccepted RSInvited + createRelayRequestGroup db cxt user groupRelayInv invId chatVRange initialDelay GSMemAccepted RSInvited lift $ void $ getRelayRequestWorker True xGrpRelayTest :: InvitationId -> VersionRangeChat -> ByteString -> CM () xGrpRelayTest invId chatVRange challenge = do @@ -1586,7 +1586,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let chatV = chatVR `peerConnChatVersion` chatVRange (cmdId, acId) <- agentAcceptContactAsync user True invId msg subMode PQSupportOff chatV withStore $ \db -> do - Connection {connId = testCId} <- createRelayTestConnection db vr user acId ConnAccepted chatV subMode + Connection {connId = testCId} <- createRelayTestConnection db cxt user acId ConnAccepted chatV subMode liftIO $ setCommandConnId db user cmdId testCId -- TODO [relays] owner, relays: TBC how to communicate member rejection rules from owner to relays -- TODO [relays] relay: TBC communicate rejection when memberId already exists (currently checked in createJoiningMember) @@ -1595,7 +1595,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = (_ucl, gLinkInfo_) <- withStore $ \db -> getUserContactLinkById db userId uclId case gLinkInfo_ of Just GroupLinkInfo {groupId, memberRole = gLinkMemRole} -> do - gInfo <- withStore $ \db -> getGroupInfo db vr user groupId + gInfo <- withStore $ \db -> getGroupInfo db cxt user groupId mem <- acceptGroupJoinRequestAsync user uclId gInfo invId chatVRange p Nothing (Just joiningMemberId) Nothing GAAccepted gLinkMemRole Nothing (Just joiningMemberKey) (gInfo', mem', scopeInfo) <- mkGroupChatScope gInfo mem createInternalChatItem user (CDGroupRcv gInfo' scopeInfo mem') (CIRcvGroupEvent RGEInvitedViaGroupLink) Nothing @@ -1765,7 +1765,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- sendProbe -> sendProbeHashes (currently) -- sendProbeHashes -> sendProbe (reversed - change order in code, may add delay) sendProbe probe - ms <- map COMGroupMember <$> withStore' (\db -> getMatchingMembers db vr user ct) + ms <- map COMGroupMember <$> withStore' (\db -> getMatchingMembers db cxt user ct) sendProbeHashes ms probe probeId else sendProbe . Probe =<< liftIO (encodedRandomBytes gVar 32) where @@ -1781,7 +1781,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = then do (probe, probeId) <- withStore $ \db -> createSentProbe db gVar userId $ COMGroupMember m sendProbe probe - cs <- map COMContact <$> withStore' (\db -> getMatchingMemberContacts db vr user m) + cs <- map COMContact <$> withStore' (\db -> getMatchingMemberContacts db cxt user m) sendProbeHashes cs probe probeId else sendProbe . Probe =<< liftIO (encodedRandomBytes gVar 32) where @@ -1854,7 +1854,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = messageFileDescription Contact {contactId} sharedMsgId fileDescr = do (fileId, aci) <- withStore $ \db -> do fileId <- getFileIdBySharedMsgId db userId contactId sharedMsgId - aci <- getChatItemByFileId db vr user fileId + aci <- getChatItemByFileId db cxt user fileId pure (fileId, aci) processFDMessage fileId aci fileDescr @@ -1862,7 +1862,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = groupMessageFileDescription g@GroupInfo {groupId} m_ sharedMsgId fileDescr = do (fileId, aci) <- withStore $ \db -> do fileId <- getGroupFileIdBySharedMsgId db userId groupId sharedMsgId - aci <- getChatItemByFileId db vr user fileId + aci <- getChatItemByFileId db cxt user fileId pure (fileId, aci) case aci of AChatItem SCTGroup SMDRcv (GroupChat _g scopeInfo) ChatItem {chatDir} @@ -2023,7 +2023,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = cci <- case itemMemberId of Just itemMemberId' -> getGroupMemberCIBySharedMsgId db user g itemMemberId' sharedMsgId Nothing -> getGroupChatItemBySharedMsgId db user g Nothing sharedMsgId - scopeInfo <- getGroupChatScopeInfoForItem db vr user g (cChatItemId cci) + scopeInfo <- getGroupChatScopeInfoForItem db cxt user g (cChatItemId cci) pure (cci, scopeInfo) if ciReactionAllowed ci then do @@ -2061,13 +2061,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- no delivery task - message already forwarded by relay pure Nothing Just m@GroupMember {memberId} -> do - (gInfo', m', scopeInfo) <- mkGetMessageChatScope vr user gInfo m content msgScope_ + (gInfo', m', scopeInfo) <- mkGetMessageChatScope cxt user gInfo m content msgScope_ if blockedByAdmin m' then createBlockedByAdmin gInfo' (Just m') scopeInfo $> Nothing else case prohibitedGroupContent gInfo' m' scopeInfo content ft_ fInv_ False of Just f -> rejected gInfo' (Just m') scopeInfo f $> Nothing Nothing -> - withStore' (\db -> getCIModeration db vr user gInfo' memberId sharedMsgId_) >>= \case + withStore' (\db -> getCIModeration db cxt user gInfo' memberId sharedMsgId_) >>= \case Just ciModeration -> do applyModeration gInfo' m' scopeInfo ciModeration withStore' $ \db -> deleteCIModeration db gInfo' memberId sharedMsgId_ @@ -2157,7 +2157,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = else case m_ of Just m -> do let mentions' = if memberBlocked m then [] else mentions - (gInfo', m', scopeInfo) <- mkGetMessageChatScope vr user gInfo m mc msgScope_ + (gInfo', m', scopeInfo) <- mkGetMessageChatScope cxt user gInfo m mc msgScope_ pure (gInfo', CDGroupRcv gInfo' scopeInfo m', mentions', scopeInfo) Nothing -> pure (gInfo, CDChannelRcv gInfo Nothing, mentions, Nothing) case m_ >>= \m -> prohibitedGroupContent gInfo' m scopeInfo mc ft_ (Nothing :: Maybe String) False of @@ -2188,7 +2188,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = else case m_ of Just m -> getGroupMemberCIBySharedMsgId db user gInfo (memberId' m) sharedMsgId Nothing -> getGroupChatItemBySharedMsgId db user gInfo Nothing sharedMsgId - (cci,) <$> getGroupChatScopeInfoForItem db vr user gInfo (cChatItemId cci) + (cci,) <$> getGroupChatScopeInfoForItem db cxt user gInfo (cChatItemId cci) case cci of CChatItem SMDRcv ci@ChatItem {chatDir = CIGroupRcv m', meta = CIMeta {itemLive}, content = CIRcvMsgContent oldMC} | isSender m' -> updateCI False ci scopeInfo oldMC itemLive (Just $ memberId' m') @@ -2300,7 +2300,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | otherwise = a delete :: CChatItem 'CTGroup -> Bool -> Maybe GroupMember -> CM (Maybe DeliveryTaskContext) delete cci asGroup byGroupMember = do - scopeInfo <- withStore $ \db -> getGroupChatScopeInfoForItem db vr user gInfo (cChatItemId cci) + scopeInfo <- withStore $ \db -> getGroupChatScopeInfoForItem db cxt user gInfo (cChatItemId cci) let fullDelete | asGroup = groupFeatureAllowed SGFFullDelete gInfo | otherwise = maybe False (\m -> groupFeatureMemberAllowed SGFFullDelete m gInfo) m_ @@ -2368,14 +2368,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = (fileId,) <$> getRcvFileTransfer db user fileId unless (rcvFileCompleteOrCancelled ft) $ do cancelRcvFileTransfer user ft - ci <- withStore $ \db -> getChatItemByFileId db vr user fileId + ci <- withStore $ \db -> getChatItemByFileId db cxt user fileId toView $ CEvtRcvFileSndCancelled user ci ft xFileAcptInv :: Contact -> SharedMsgId -> Maybe ConnReqInvitation -> String -> CM () xFileAcptInv ct sharedMsgId fileConnReq_ fName = do (fileId, AChatItem _ _ _ ci) <- withStore $ \db -> do fileId <- getDirectFileIdBySharedMsgId db user ct sharedMsgId - (fileId,) <$> getChatItemByFileId db vr user fileId + (fileId,) <$> getChatItemByFileId db cxt user fileId assertSMPAcceptNotProhibited ci ft@FileTransferMeta {fileName, fileSize, fileInline, cancelled} <- withStore (\db -> getFileTransferMeta db user fileId) -- [async agent commands] no continuation needed, but command should be asynchronous for stability @@ -2384,7 +2384,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- receiving inline Nothing -> do event <- withStore $ \db -> do - ci' <- updateDirectCIFileStatus db vr user fileId $ CIFSSndTransfer 0 1 + ci' <- updateDirectCIFileStatus db cxt user fileId $ CIFSSndTransfer 0 1 sft <- createSndDirectInlineFT db ct ft pure $ CEvtSndFileStart user ci' sft toView event @@ -2412,7 +2412,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = forM_ sft_ $ \sft@SndFileTransfer {fileId} -> do ci@(AChatItem _ _ _ ChatItem {file}) <- withStore $ \db -> do liftIO $ updateSndFileStatus db sft FSComplete - updateDirectCIFileStatus db vr user fileId CIFSSndComplete + updateDirectCIFileStatus db cxt user fileId CIFSSndComplete case file of Just CIFile {fileProtocol = FPXFTP} -> do ft <- withStore $ \db -> getFileTransferMeta db user fileId @@ -2450,7 +2450,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = xFileCancelGroup g@GroupInfo {groupId} m_ sharedMsgId = do (fileId, aci) <- withStore $ \db -> do fileId <- getGroupFileIdBySharedMsgId db userId groupId sharedMsgId - (fileId,) <$> getChatItemByFileId db vr user fileId + (fileId,) <$> getChatItemByFileId db cxt user fileId case aci of AChatItem SCTGroup SMDRcv (GroupChat _g scopeInfo) ChatItem {chatDir} | validSender m_ chatDir -> do @@ -2466,7 +2466,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = xFileAcptInvGroup GroupInfo {groupId} m@GroupMember {activeConn} sharedMsgId fileConnReq_ fName = do (fileId, AChatItem _ _ _ ci) <- withStore $ \db -> do fileId <- getGroupFileIdBySharedMsgId db userId groupId sharedMsgId - (fileId,) <$> getChatItemByFileId db vr user fileId + (fileId,) <$> getChatItemByFileId db cxt user fileId assertSMPAcceptNotProhibited ci -- TODO check that it's not already accepted ft@FileTransferMeta {fileName, fileSize, fileInline, cancelled} <- withStore (\db -> getFileTransferMeta db user fileId) @@ -2475,7 +2475,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = (Nothing, Just conn) -> do -- receiving inline event <- withStore $ \db -> do - ci' <- updateDirectCIFileStatus db vr user fileId $ CIFSSndTransfer 0 1 + ci' <- updateDirectCIFileStatus db cxt user fileId $ CIFSSndTransfer 0 1 sft <- liftIO $ createSndGroupInlineFT db m conn ft pure $ CEvtSndFileStart user ci' sft toView event @@ -2501,7 +2501,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = when (fromRole < GRAdmin || fromRole < memRole) $ throwChatError (CEGroupContactRole c) when (fromMemId == memId) $ throwChatError CEGroupDuplicateMemberId -- [incognito] if direct connection with host is incognito, create membership using the same incognito profile - (gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership}, hostId) <- withStore $ \db -> createGroupInvitation db vr user ct inv customUserProfileId + (gInfo@GroupInfo {groupId, localDisplayName, groupProfile, membership}, hostId) <- withStore $ \db -> createGroupInvitation db cxt user ct inv customUserProfileId void $ createChatItem user (CDGroupSnd gInfo Nothing) False CIChatBanner Nothing (Just epochStart) let GroupMember {groupMemberId, memberId = membershipMemId} = membership if sameGroupLinkId groupLinkId groupLinkId' @@ -2542,7 +2542,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = then do (ct', contactConns) <- withStore' $ \db -> do ct' <- updateContactStatus db user c CSDeleted - (ct',) <$> getContactConnections db vr userId ct' + (ct',) <$> getContactConnections db cxt userId ct' deleteAgentConnectionsAsync $ map aConnId contactConns forM_ contactConns $ \conn -> withStore' $ \db -> updateConnectionStatus db conn ConnDeleted activeConn' <- forM (contactConn ct') $ \conn -> pure conn {connStatus = ConnDeleted} @@ -2551,7 +2551,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = toView $ CEvtNewChatItems user [AChatItem SCTDirect SMDRcv cInfo ci] toView $ CEvtContactDeletedByContact user ct'' else do - contactConns <- withStore' $ \db -> getContactConnections db vr userId c + contactConns <- withStore' $ \db -> getContactConnections db cxt userId c deleteAgentConnectionsAsync $ map aConnId contactConns withStore $ \db -> deleteContact db user c where @@ -2629,7 +2629,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = messageError "x.grp.link.acpt with insufficient member permissions" | sameMemberId memberId membership = processUserAccepted | otherwise = - withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memberId) >>= \case + withStore' (\db -> runExceptT $ getGroupMemberByMemberId db cxt user gInfo memberId) >>= \case Left _ -> messageError "x.grp.link.acpt error: referenced member does not exist" Right referencedMember -> do (referencedMember', gInfo') <- withStore' $ \db -> do @@ -2673,7 +2673,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = GAPendingApproval -> messageWarning "x.grp.link.acpt: unexpected group acceptance - pending approval" introduceToRemainingMembers acceptedMember = do - introduceToRemaining vr user gInfo acceptedMember + introduceToRemaining cxt user gInfo acceptedMember when (groupFeatureAllowed SGFHistory gInfo) $ sendHistory user gInfo acceptedMember maybeCreateGroupDescrLocal :: GroupInfo -> GroupMember -> CM () @@ -2697,7 +2697,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = toView $ CEvtGroupMemberUpdated user gInfo m m' pure m' Just mContactId -> do - mCt <- withStore $ \db -> getContact db vr user mContactId + mCt <- withStore $ \db -> getContact db cxt user mContactId if canUpdateProfile mCt then do (m', ct', displaced_) <- withStore $ \db -> updateContactMemberProfileWithConflict db user m mCt p' @@ -2748,7 +2748,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = contactMerge <- readTVarIO =<< asks contactMergeEnabled -- [incognito] unless connected incognito when (contactMerge && not (contactOrMemberIncognito cgm2)) $ do - cgm1s <- withStore' $ \db -> matchReceivedProbe db vr user cgm2 probe + cgm1s <- withStore' $ \db -> matchReceivedProbe db cxt user cgm2 probe let cgm1s' = filter (not . contactOrMemberIncognito) cgm1s probeMatches cgm1s' cgm2 where @@ -2764,7 +2764,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = contactMerge <- readTVarIO =<< asks contactMergeEnabled -- [incognito] unless connected incognito when (contactMerge && not (contactOrMemberIncognito cgm1)) $ do - cgm2Probe_ <- withStore' $ \db -> matchReceivedProbeHash db vr user cgm1 probeHash + cgm2Probe_ <- withStore' $ \db -> matchReceivedProbeHash db cxt user cgm1 probeHash forM_ cgm2Probe_ $ \(cgm2, probe) -> unless (contactOrMemberIncognito cgm2) . void $ probeMatch cgm1 cgm2 probe @@ -2794,7 +2794,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = xInfoProbeOk :: ContactOrMember -> Probe -> CM () xInfoProbeOk cgm1 probe = do - cgm2 <- withStore' $ \db -> matchSentProbe db vr user cgm1 probe + cgm2 <- withStore' $ \db -> matchSentProbe db cxt user cgm1 probe case cgm1 of COMContact c1 -> case cgm2 of @@ -2943,14 +2943,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = associateMemberWithContact c1 m2@GroupMember {groupId} = do g <- withStore $ \db -> do liftIO $ associateMemberWithContactRecord db user c1 m2 - getGroupInfo db vr user groupId + getGroupInfo db cxt user groupId toView $ CEvtContactAndMemberAssociated user c1 g m2 c1 pure c1 associateContactWithMember :: GroupMember -> Contact -> CM Contact associateContactWithMember m1@GroupMember {groupId} c2 = do (c2', g) <- withStore $ \db -> - liftM2 (,) (associateContactWithMemberRecord db vr user m1 c2) (getGroupInfo db vr user groupId) + liftM2 (,) (associateContactWithMemberRecord db cxt user m1 c2) (getGroupInfo db cxt user groupId) toView $ CEvtContactAndMemberAssociated user c2 g m1 c2' pure c2' @@ -2966,17 +2966,17 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- the source of truth. let Connection {simplexName} = conn' Profile {simplexName = pSimplexName} = p - (ct, displaced_) <- withStore $ \db -> createDirectContact db vr user conn' p simplexName + (ct, displaced_) <- withStore $ \db -> createDirectContact db cxt user conn' p simplexName let Contact {localDisplayName = newLDN} = ct surfaceSimplexNameConflict user pSimplexName displaced_ SNCEContact newLDN toView $ CEvtContactConnecting user ct pure (conn', Nothing) XGrpLinkInv glInv -> do - (gInfo, host) <- withStore $ \db -> createGroupInvitedViaLink db vr user conn' glInv + (gInfo, host) <- withStore $ \db -> createGroupInvitedViaLink db cxt user conn' glInv toView $ CEvtGroupLinkConnecting user gInfo host pure (conn', Just gInfo) XGrpLinkReject glRjct@GroupLinkRejection {rejectionReason} -> do - (gInfo, host) <- withStore $ \db -> createGroupRejectedViaLink db vr user conn' glRjct + (gInfo, host) <- withStore $ \db -> createGroupRejectedViaLink db cxt user conn' glRjct toView $ CEvtGroupLinkConnecting user gInfo host toViewTE $ TEGroupLinkRejected user gInfo rejectionReason pure (conn', Just gInfo) @@ -2991,10 +2991,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = if sameMemberId memId (membership gInfo) then pure Nothing else do - withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case + withStore' (\db -> runExceptT $ getGroupMemberByMemberId db cxt user gInfo memId) >>= \case Right unknownMember@GroupMember {memberStatus = GSMemUnknown} -> do (updatedMember, gInfo') <- withStore $ \db -> do - updatedMember <- updateUnknownMemberAnnounced db vr user m unknownMember memInfo initialStatus + updatedMember <- updateUnknownMemberAnnounced db cxt user m unknownMember memInfo initialStatus gInfo' <- if memberPending updatedMember then liftIO $ increaseGroupMembersRequireAttention db user gInfo @@ -3047,10 +3047,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = xGrpMemIntro gInfo@GroupInfo {chatSettings} m@GroupMember {memberRole, localDisplayName = c} memInfo@(MemberInfo memId _ memChatVRange _ _) memRestrictions = do case memberCategory m of GCHostMember -> - withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case + withStore' (\db -> runExceptT $ getGroupMemberByMemberId db cxt user gInfo memId) >>= \case Right existingMember | useRelays' gInfo -> do - updatedMember <- withStore $ \db -> updatePreparedChannelMember db vr user existingMember memInfo + updatedMember <- withStore $ \db -> updatePreparedChannelMember db cxt user existingMember memInfo toView $ CEvtGroupMemberUpdated user gInfo existingMember updatedMember | otherwise -> messageError "x.grp.mem.intro ignored: member already exists" @@ -3071,7 +3071,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = subMode <- chatReadVar subscriptionMode -- [async agent commands] commands should be asynchronous, continuation is to send XGrpMemInv - have to remember one has completed and process on second groupConnIds <- createConn subMode - let chatV = maybe (minVersion vr) (\peerVR -> vr `peerConnChatVersion` fromChatVRange peerVR) memChatVRange + let chatV = maybe (minVersion (vr cxt)) (\peerVR -> vr cxt `peerConnChatVersion` fromChatVRange peerVR) memChatVRange void $ withStore $ \db -> do reMember <- createIntroReMember db user gInfo memInfo memRestrictions createIntroReMemberConn db user m reMember chatV memInfo groupConnIds subMode @@ -3082,7 +3082,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = sendXGrpMemInv :: Int64 -> Maybe ConnReqInvitation -> XGrpMemIntroCont -> CM () sendXGrpMemInv hostConnId directConnReq XGrpMemIntroCont {groupId, groupMemberId, memberId, groupConnReq} = do - hostConn <- withStore $ \db -> getConnectionById db vr user hostConnId + hostConn <- withStore $ \db -> getConnectionById db cxt user hostConnId let msg = XGrpMemInv memberId IntroInvitation {groupConnReq, directConnReq} void $ sendDirectMemberMessage hostConn msg groupId withStore' $ \db -> updateGroupMemberStatusById db userId groupMemberId GSMemIntroInvited @@ -3091,7 +3091,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = xGrpMemInv gInfo m memId introInv = do case memberCategory m of GCInviteeMember -> - withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case + withStore' (\db -> runExceptT $ getGroupMemberByMemberId db cxt user gInfo memId) >>= \case Left _ -> messageError "x.grp.mem.inv error: referenced member does not exist" Right reMember -> sendGroupMemberMessage gInfo reMember $ XGrpMemFwd (memberInfo gInfo m) introInv _ -> messageError "x.grp.mem.inv can be only sent by invitee member" @@ -3102,7 +3102,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = checkHostRole m memRole toMember <- withStore $ \db -> do toMember <- - getGroupMemberByMemberId db vr user gInfo memId + getGroupMemberByMemberId db cxt user gInfo memId -- TODO if the missed messages are correctly sent as soon as there is connection before anything else is sent -- the situation when member does not exist is an error -- member receiving x.grp.mem.fwd should have also received x.grp.mem.new prior to that. @@ -3126,7 +3126,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = directConnIds <- forM directConnReq $ \dcr -> joinAgentConnectionAsync user Nothing True dcr dm subMode let customUserProfileId = localProfileId <$> incognitoMembershipProfile gInfo mcvr = maybe chatInitialVRange fromChatVRange memChatVRange - chatV = vr `peerConnChatVersion` mcvr + chatV = vr cxt `peerConnChatVersion` mcvr withStore' $ \db -> createIntroToMemberContact db user m toMember chatV mcvr groupConnIds directConnIds customUserProfileId subMode xGrpMemRole :: GroupInfo -> GroupMember -> MemberId -> GroupMemberRole -> RcvMessage -> UTCTime -> CM (Maybe DeliveryJobScope) @@ -3135,7 +3135,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = let gInfo' = gInfo {membership = membership {memberRole = memRole}} in changeMemberRole gInfo' membership $ RGEUserRole memRole | otherwise = - withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case + withStore' (\db -> runExceptT $ getGroupMemberByMemberId db cxt user gInfo memId) >>= \case Right member -> changeMemberRole gInfo member $ RGEMemberRole (groupMemberId' member) (fromLocalProfile $ memberProfile member) memRole Left _ -> messageError "x.grp.mem.role with unknown member ID" $> Nothing where @@ -3166,7 +3166,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | membershipMemId == memId = pure Nothing -- ignore - XGrpMemRestrict can be sent to restricted member for efficiency | otherwise = do unknownRole <- unknownMemberRole gInfo - withStore (\db -> getCreateUnknownGMByMemberId db vr user gInfo memId "" unknownRole True) >>= \case + withStore (\db -> getCreateUnknownGMByMemberId db cxt user gInfo memId "" unknownRole True) >>= \case Nothing -> messageError "x.grp.mem.restrict: no member" $> Nothing -- shouldn't happen Just (bm, unknown) -> do let GroupMember {groupMemberId = bmId, memberRole, blockedByAdmin, memberProfile = bmp} = bm @@ -3190,7 +3190,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = xGrpMemCon :: GroupInfo -> GroupMember -> MemberId -> CM () xGrpMemCon gInfo sendingMem memId = do - refMem <- withStore $ \db -> getGroupMemberByMemberId db vr user gInfo memId + refMem <- withStore $ \db -> getGroupMemberByMemberId db cxt user gInfo memId -- Updating vectors in separate transactions to avoid deadlocks. withStore $ \db -> setMemberVectorRelationConnected db sendingMem refMem MRSubjectConnected withStore $ \db -> setMemberVectorRelationConnected db refMem sendingMem MRReferencedConnected @@ -3212,7 +3212,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = toView $ CEvtDeletedMemberUser user gInfo {membership = membership'} m withMessages msgSigned pure $ Just DJSGroup {jobSpec = DJRelayRemoved} else - withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case + withStore' (\db -> runExceptT $ getGroupMemberByMemberId db cxt user gInfo memId) >>= \case Left _ -> do messageError "x.grp.mem.del with unknown member ID" pure $ Just DJSGroup {jobSpec = DJDeliveryJob {includePending = True}} @@ -3365,7 +3365,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = case memberContactId of Nothing -> createNewContact subMode Just mContactId -> do - mCt <- withStore $ \db -> getContact db vr user mContactId + mCt <- withStore $ \db -> getContact db cxt user mContactId let Contact {activeConn, contactGrpInvSent} = mCt forM_ activeConn $ \Connection {connId} -> if contactGrpInvSent @@ -3392,7 +3392,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = mCt' <- withStore $ \db -> do updateMemberContactInvited db user mCt groupDirectInv void $ liftIO $ createMemberContactConn db user acId (Just cmdId) g mConn ConnJoined mContactId subMode - getContact db vr user mContactId + getContact db cxt user mContactId securityCodeChanged mCt' createItems mCt' m | otherwise = do @@ -3400,7 +3400,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = mCt' <- withStore $ \db -> do updateMemberContactInvited db user mCt groupDirectInv void $ liftIO $ createMemberContactConn db user acId Nothing g mConn ConnPrepared mContactId subMode - getContact db vr user mContactId + getContact db cxt user mContactId securityCodeChanged mCt' createInternalChatItem user (CDDirectRcv mCt') (CIRcvDirectEvent $ RDEGroupInvLinkReceived gp) Nothing createItems mCt' m @@ -3411,7 +3411,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = (mCt, m') <- withStore $ \db -> do (mContactId, m') <- liftIO $ createMemberContactInvited db user g m groupDirectInv void $ liftIO $ createMemberContactConn db user acId (Just cmdId) g mConn ConnJoined mContactId subMode - mCt <- getContact db vr user mContactId + mCt <- getContact db cxt user mContactId pure (mCt, m') createInternalChatItem user (CDDirectSnd mCt) CIChatBanner (Just epochStart) createItems mCt m' @@ -3420,7 +3420,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = (mCt, m') <- withStore $ \db -> do (mContactId, m') <- liftIO $ createMemberContactInvited db user g m groupDirectInv void $ liftIO $ createMemberContactConn db user acId Nothing g mConn ConnPrepared mContactId subMode - mCt <- getContact db vr user mContactId + mCt <- getContact db cxt user mContactId pure (mCt, m') createInternalChatItem user (CDDirectSnd mCt) CIChatBanner (Just epochStart) createInternalChatItem user (CDDirectRcv mCt) (CIRcvDirectEvent $ RDEGroupInvLinkReceived gp) Nothing @@ -3451,7 +3451,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = FwdMember memberId memberName -> do unknownRole <- unknownMemberRole gInfo let allowCreate = toCMEventTag chatMsgEvent /= XGrpLeave_ - withStore (\db -> getCreateUnknownGMByMemberId db vr user gInfo memberId memberName unknownRole allowCreate) >>= \case + withStore (\db -> getCreateUnknownGMByMemberId db cxt user gInfo memberId memberName unknownRole allowCreate) >>= \case Just (author, unknown) | memberRemoved author -> logInfo $ "x.grp.msg.forward: ignoring content from removed member, group " <> tshow (groupId' gInfo) <> ", member " <> safeDecodeUtf8 (strEncode memberId) <> ", event " <> tshow (toCMEventTag chatMsgEvent) @@ -3507,8 +3507,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = Just sm@SignedMsg {chatBinding, signatures, signedBody} | GroupMember {memberPubKey = Just pubKey, memberId} <- member -> case chatBinding of - CBGroup | Just GroupKeys {publicGroupId} <- groupKeys gInfo -> - let prefix = smpEncode chatBinding <> smpEncode (publicGroupId, memberId) + CBGroup -> + let prefix = smpEncode chatBinding <> bindingData + bindingData = case groupKeys gInfo of + Just GroupKeys {publicGroupId} -> smpEncode (publicGroupId, memberId) + Nothing -> smpEncode (memberId, pubKey) -- forward compatibility for verifying signed messages in p2p groups in signed MSSVerified <$ guard (all (\(MsgSignature KRMember sig) -> C.verify (C.APublicVerifyKey C.SEd25519 pubKey) sig (prefix <> signedBody)) signatures) _ -> signed MSSSignedNoKey <$ guard signatureOptional | otherwise -> signed MSSSignedNoKey <$ guard (signatureOptional || unverifiedAllowed membership member tag) @@ -3581,7 +3584,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- SENT and RCVD events are received for messages that may be batched in single scope, -- so we can look up scope of first item scopeInfo <- case cis of - (ci : _) -> getGroupChatScopeInfoForItem db vr user gInfo (chatItemId' ci) + (ci : _) -> getGroupChatScopeInfoForItem db cxt user gInfo (chatItemId' ci) _ -> pure Nothing pure $ map (gItem scopeInfo) cis unless (null acis) $ toView $ CEvtChatItemsStatusesUpdated user acis @@ -3605,14 +3608,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = deleteGroupConnections :: User -> GroupInfo -> Bool -> CM () deleteGroupConnections user gInfo@GroupInfo {membership} waitDelivery = do - vr <- chatVersionRange + cxt <- chatStoreCxt -- member records are not deleted to keep history - members <- getMembers vr + members <- getMembers cxt deleteMembersConnections' user members waitDelivery where - getMembers vr - | useRelays' gInfo, not (isRelay membership) = withStore' $ \db -> getGroupRelayMembers db vr user gInfo - | otherwise = withStore' $ \db -> getGroupMembers db vr user gInfo + getMembers cxt + | useRelays' gInfo, not (isRelay membership) = withStore' $ \db -> getGroupRelayMembers db cxt user gInfo + | otherwise = withStore' $ \db -> getGroupMembers db cxt user gInfo startDeliveryTaskWorkers :: CM () startDeliveryTaskWorkers = do @@ -3632,20 +3635,20 @@ getDeliveryTaskWorker hasWork deliveryKey = do runDeliveryTaskWorker :: AgentClient -> DeliveryWorkerKey -> Worker -> CM () runDeliveryTaskWorker a deliveryKey Worker {doWork} = do delay <- asks $ deliveryWorkerDelay . config - vr <- chatVersionRange + cxt <- chatStoreCxt -- TODO [relays] in future may be required to read groupInfo and user on each iteration for up to date state -- TODO - same for delivery jobs (runDeliveryJobWorker) gInfo <- withStore $ \db -> do user <- getUserByGroupId db groupId - getGroupInfo db vr user groupId + getGroupInfo db cxt user groupId forever $ do unless (delay == 0) $ liftIO $ threadDelay' delay lift $ waitForWork doWork - runDeliveryTaskOperation vr gInfo + runDeliveryTaskOperation cxt gInfo where (groupId, workerScope) = deliveryKey - runDeliveryTaskOperation :: VersionRangeChat -> GroupInfo -> CM () - runDeliveryTaskOperation vr gInfo = do + runDeliveryTaskOperation :: StoreCxt -> GroupInfo -> CM () + runDeliveryTaskOperation cxt gInfo = do withWork_ a doWork (withStore' $ \db -> getNextDeliveryTask db deliveryKey) $ \task -> processDeliveryTask task `catchAllErrors` \e -> do @@ -3661,7 +3664,7 @@ runDeliveryTaskWorker a deliveryKey Worker {doWork} = do withStore' $ \db -> setDeliveryTaskErrStatus db (deliveryTaskId task) "relay inactive" | otherwise -> withWorkItems a doWork (withStore' $ \db -> getNextDeliveryTasks db gInfo task) $ \nextTasks -> do - let (body, acceptedTasks, largeTasks) = batchDeliveryTasks1 vr maxEncodedMsgLength nextTasks + let (body, acceptedTasks, largeTasks) = batchDeliveryTasks1 (vr cxt) maxEncodedMsgLength nextTasks senderGMIds = S.toList . S.fromList $ map (\MessageDeliveryTask {senderGMId} -> senderGMId) acceptedTasks withStore' $ \db -> do createMsgDeliveryJob db gInfo jobScope senderGMIds body @@ -3720,19 +3723,19 @@ encodeMemberNew vr gInfo member = case encodeChatMessage maxBatchElementSize cha runDeliveryJobWorker :: AgentClient -> DeliveryWorkerKey -> Worker -> CM () runDeliveryJobWorker a deliveryKey Worker {doWork} = do delay <- asks $ deliveryWorkerDelay . config - vr <- chatVersionRange + cxt <- chatStoreCxt (user, gInfo) <- withStore $ \db -> do user <- getUserByGroupId db groupId - gInfo <- getGroupInfo db vr user groupId + gInfo <- getGroupInfo db cxt user groupId pure (user, gInfo) forever $ do unless (delay == 0) $ liftIO $ threadDelay' delay lift $ waitForWork doWork - runDeliveryJobOperation vr user gInfo + runDeliveryJobOperation cxt user gInfo where (groupId, workerScope) = deliveryKey - runDeliveryJobOperation :: VersionRangeChat -> User -> GroupInfo -> CM () - runDeliveryJobOperation vr user gInfo = do + runDeliveryJobOperation :: StoreCxt -> User -> GroupInfo -> CM () + runDeliveryJobOperation cxt user gInfo = do withWork_ a doWork (withStore' $ \db -> getNextDeliveryJob db deliveryKey) $ \job -> processDeliveryJob job `catchAllErrors` \e -> do @@ -3772,7 +3775,7 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do senders <- withStore' $ \db -> fmap catMaybes . forM senderGMIds $ \sId -> fmap eitherToMaybe . runExceptT $ do - sender <- getNonRemovedMemberById db vr user sId + sender <- getNonRemovedMemberById db cxt user sId vec <- getMemberRelationsVector db sender pure (sender, vec) let missingSenders = length senderGMIds - length senders @@ -3788,7 +3791,7 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do -- TODO [relays] public groups: revisit if mods/admins are introduced via this sidecar. let (encoderErrs, validLabeled) = partitionEithers - [ (\bs -> (s, bs)) <$> encodeMemberNew vr gInfo s + [ (\bs -> (s, bs)) <$> encodeMemberNew (vr cxt) gInfo s | (s, _) <- senders, memberRole' s <= GRMember ] (extBody', inBody, overflowLabeled, large1) = batchProfilesWithBody maxEncodedMsgLength body validLabeled @@ -3808,7 +3811,7 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do where sendLoop :: Int -> Maybe GroupMemberId -> Map GroupMemberId ByteString -> [(Int, (ByteString, [GroupMember]))] -> [GroupMember] -> ByteString -> [GroupMember] -> CM () sendLoop bucketSize cursorGMId_ senderVec overflowWithIds inBodySenders extBody activeSenders = do - mems <- withStore' $ \db -> getGroupMembersByCursor db vr user gInfo cursorGMId_ singleSenderGMId_ bucketSize + mems <- withStore' $ \db -> getGroupMembersByCursor db cxt user gInfo cursorGMId_ singleSenderGMId_ bucketSize unless (null mems) $ do let msgReqs = buildMsgReqs mems unless (null msgReqs) $ void $ withAgent (`sendMessages` msgReqs) @@ -3853,7 +3856,7 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do Nothing -> True DJSMemberSupport scopeGMId -> do -- for member support scope we just load all recipients in one go, without cursor - modMs <- withStore' $ \db -> getGroupModerators db vr user gInfo + modMs <- withStore' $ \db -> getGroupModerators db cxt user gInfo let moderatorFilter m = memberCurrent m && maxVersion (memberChatVRange m) >= groupKnockingVersion @@ -3863,14 +3866,14 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do if Just scopeGMId == singleSenderGMId_ then pure modMs' else do - scopeMem <- withStore $ \db -> getGroupMemberById db vr user scopeGMId + scopeMem <- withStore $ \db -> getGroupMemberById db cxt user scopeGMId pure $ scopeMem : modMs' unless (null mems) $ deliver body mems -- fully connected group | otherwise = case singleSenderGMId_ of Nothing -> throwChatError $ CEInternalError "delivery job worker: singleSenderGMId is required when not using relays" Just sId -> do - sender <- withStore $ \db -> getGroupMemberById db vr user sId + sender <- withStore $ \db -> getGroupMemberById db cxt user sId ms <- buildMemberList sender unless (null ms) $ deliver body ms where @@ -3880,14 +3883,14 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do let introducedMemsIdxs = getRelationsIndexes MRIntroduced vec case jobScope of DJSGroup {jobSpec} -> do - ms <- withStore' $ \db -> getGroupMembersByIndexes db vr user gInfo introducedMemsIdxs + ms <- withStore' $ \db -> getGroupMembersByIndexes db cxt user gInfo introducedMemsIdxs pure $ filter shouldForwardTo ms where shouldForwardTo m | jobSpecImpliedPending jobSpec = memberCurrentOrPending m | otherwise = memberCurrent m DJSMemberSupport scopeGMId -> do - ms <- withStore' $ \db -> getSupportScopeMembersByIndexes db vr user gInfo scopeGMId introducedMemsIdxs + ms <- withStore' $ \db -> getSupportScopeMembersByIndexes db cxt user gInfo scopeGMId introducedMemsIdxs pure $ filter shouldForwardTo ms where shouldForwardTo m = groupMemberId' m == scopeGMId || currentModerator m @@ -3938,7 +3941,7 @@ getRelayRequestWorker hasWork = do runRelayRequestWorker :: AgentClient -> Worker -> CM () runRelayRequestWorker a Worker {doWork} = do - vr <- chatVersionRange + cxt <- chatStoreCxt (user, uclId) <- withStore $ \db -> do user <- getRelayUser db UserContactLink {userContactLinkId} <- getUserAddress db user @@ -3946,10 +3949,10 @@ runRelayRequestWorker a Worker {doWork} = do delayThreads <- liftIO TM.emptyIO forever $ do lift $ waitForWork doWork - runRelayRequestOperation delayThreads vr user uclId + runRelayRequestOperation delayThreads cxt user uclId where - runRelayRequestOperation :: TM.TMap GroupId (TMVar (Weak ThreadId)) -> VersionRangeChat -> User -> Int64 -> CM () - runRelayRequestOperation delayThreads vr user uclId = + runRelayRequestOperation :: TM.TMap GroupId (TMVar (Weak ThreadId)) -> StoreCxt -> User -> Int64 -> CM () + runRelayRequestOperation delayThreads cxt user uclId = withWork_ a doWork getReadyRelayRequest $ \(groupId, rrd) -> do ChatConfig {relayRequestExpiry} <- asks config @@ -3998,7 +4001,7 @@ runRelayRequestWorker a Worker {doWork} = do processRelayRequest :: GroupId -> RelayRequestData -> CM () processRelayRequest groupId rrd = do (gInfo, groupLink_) <- withStore $ \db -> do - gInfo <- getGroupInfo db vr user groupId + gInfo <- getGroupInfo db cxt user groupId groupLink_ <- liftIO $ runExceptT $ getGroupLink db user gInfo pure (gInfo, groupLink_) -- Check if relay link already exists (recovery case) @@ -4026,7 +4029,7 @@ runRelayRequestWorker a Worker {doWork} = do gInfo' <- withStore $ \db -> do void $ updateGroupProfile db user gInfo gp updateRelayGroupKeys db user gInfo pg rootKey memberPrivKey owners - getGroupInfo db vr user groupId + getGroupInfo db cxt user groupId pure (gInfo', sLnk) where validateGroupProfile :: GroupProfile -> CM () @@ -4058,5 +4061,5 @@ runRelayRequestWorker a Worker {doWork} = do pure (sigKeys, sLnk) acceptOwnerConnection :: RelayRequestData -> GroupInfo -> ShortLinkContact -> CM () acceptOwnerConnection RelayRequestData {relayInvId, reqChatVRange} gi relayLink = do - ownerMember <- withStore $ \db -> getHostMember db vr user groupId + ownerMember <- withStore $ \db -> getHostMember db cxt user groupId void $ acceptRelayJoinRequestAsync user uclId gi ownerMember relayInvId reqChatVRange relayLink diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index fdb44b3707..dbe06ea283 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -74,8 +74,8 @@ getChatLockEntity db agentConnId = do -- TODO consider whether ConnFailed connections should be excluded: -- - from receiving: getConnectionEntity, getContactConnEntityByConnReqHash -- - from subscribing: getContactConnsToSub, getUCLConnsToSub, getMemberConnsToSub, getPendingConnsToSub -getConnectionEntity :: DB.Connection -> VersionRangeChat -> User -> AgentConnId -> ExceptT StoreError IO ConnectionEntity -getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do +getConnectionEntity :: DB.Connection -> StoreCxt -> User -> AgentConnId -> ExceptT StoreError IO ConnectionEntity +getConnectionEntity db cxt user@User {userId, userContactId} agentConnId = do c@Connection {connType, entityId} <- getConnection_ case entityId of Nothing -> @@ -90,7 +90,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do where getConnection_ :: ExceptT StoreError IO Connection getConnection_ = ExceptT $ do - firstRow (toConnection vr) (SEConnectionNotFound agentConnId) $ + firstRow (toConnection cxt) (SEConnectionNotFound agentConnId) $ DB.query db [sql| @@ -175,7 +175,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do liftIO $ bitraverse (addGroupChatTags db) pure gm toGroupAndMember :: Connection -> GroupInfoRow :. GroupMemberRow -> (GroupInfo, GroupMember) toGroupAndMember c (groupInfoRow :. memberRow) = - let groupInfo = toGroupInfo vr userContactId [] groupInfoRow + let groupInfo = toGroupInfo cxt userContactId [] groupInfoRow member = toGroupMember userContactId memberRow in (groupInfo, (member :: GroupMember) {activeConn = Just c}) getUserContact_ :: Int64 -> ExceptT StoreError IO UserContact @@ -194,17 +194,17 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do userContact_ [(cReq, groupId)] = Right UserContact {userContactLinkId, connReqContact = cReq, groupId} userContact_ _ = Left SEUserContactLinkNotFound -getConnectionEntityByConnReq :: DB.Connection -> VersionRangeChat -> User -> (ConnReqInvitation, ConnReqInvitation) -> IO (Maybe ConnectionEntity) -getConnectionEntityByConnReq db vr user@User {userId} (cReqSchema1, cReqSchema2) = do +getConnectionEntityByConnReq :: DB.Connection -> StoreCxt -> User -> (ConnReqInvitation, ConnReqInvitation) -> IO (Maybe ConnectionEntity) +getConnectionEntityByConnReq db cxt user@User {userId} (cReqSchema1, cReqSchema2) = do connId_ <- maybeFirstRow fromOnly $ DB.query db "SELECT agent_conn_id FROM connections WHERE user_id = ? AND conn_req_inv IN (?,?) LIMIT 1" (userId, cReqSchema1, cReqSchema2) - maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db vr user) connId_ + maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db cxt user) connId_ -getConnectionEntityViaShortLink :: DB.Connection -> VersionRangeChat -> User -> ShortLinkInvitation -> IO (Maybe (ConnReqInvitation, ConnectionEntity)) -getConnectionEntityViaShortLink db vr user@User {userId} shortLink = fmap eitherToMaybe $ runExceptT $ do +getConnectionEntityViaShortLink :: DB.Connection -> StoreCxt -> User -> ShortLinkInvitation -> IO (Maybe (ConnReqInvitation, ConnectionEntity)) +getConnectionEntityViaShortLink db cxt user@User {userId} shortLink = fmap eitherToMaybe $ runExceptT $ do (cReq, connId) <- ExceptT getConnReqConnId - (cReq,) <$> getConnectionEntity db vr user connId + (cReq,) <$> getConnectionEntity db cxt user connId where getConnReqConnId = firstRow' toConnReqConnId (SEInternalError "connection not found") $ @@ -225,8 +225,8 @@ getConnectionEntityViaShortLink db vr user@User {userId} shortLink = fmap either -- multiple connections can have same via_contact_uri_hash if request was repeated; -- this function searches for latest connection with contact so that "known contact" plan would be chosen; -- deleted connections are filtered out to allow re-connecting via same contact address -getContactConnEntityByConnReqHash :: DB.Connection -> VersionRangeChat -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe ConnectionEntity) -getContactConnEntityByConnReqHash db vr user@User {userId} (cReqHash1, cReqHash2) = do +getContactConnEntityByConnReqHash :: DB.Connection -> StoreCxt -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe ConnectionEntity) +getContactConnEntityByConnReqHash db cxt user@User {userId} (cReqHash1, cReqHash2) = do connId_ <- maybeFirstRow fromOnly $ DB.query @@ -243,7 +243,7 @@ getContactConnEntityByConnReqHash db vr user@User {userId} (cReqHash1, cReqHash2 ) c |] (userId, cReqHash1, cReqHash2, ConnDeleted) - maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db vr user) connId_ + maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getConnectionEntity db cxt user) connId_ getContactConnsToSub :: DB.Connection -> User -> Bool -> IO [ConnId] getContactConnsToSub db User {userId} filterToSubscribe = diff --git a/src/Simplex/Chat/Store/ContactRequest.hs b/src/Simplex/Chat/Store/ContactRequest.hs index bd2179cbd6..b4faef8491 100644 --- a/src/Simplex/Chat/Store/ContactRequest.hs +++ b/src/Simplex/Chat/Store/ContactRequest.hs @@ -49,7 +49,7 @@ import Database.SQLite.Simple.QQ (sql) createOrUpdateContactRequest :: DB.Connection -> TVar ChaChaDRG -> - VersionRangeChat -> + StoreCxt -> User -> Int64 -> UserContactLink -> @@ -65,7 +65,7 @@ createOrUpdateContactRequest :: createOrUpdateContactRequest db gVar - vr + cxt user@User {userId, userContactId} uclId UserContactLink {addressSettings = AddressSettings {businessAddress}} @@ -89,7 +89,7 @@ createOrUpdateContactRequest Nothing -> liftIO (getAcceptedBusinessChat xContactId) >>= \case Just gInfo@GroupInfo {businessChat = Just BusinessChatInfo {customerId}} -> do - clientMember <- getGroupMemberByMemberId db vr user gInfo customerId + clientMember <- getGroupMemberByMemberId db cxt user gInfo customerId cr <- liftIO $ getContactRequestByXContactId xContactId pure $ RSAcceptedRequest cr (REBusinessChat gInfo clientMember) Just GroupInfo {businessChat = Nothing} -> throwError SEInvalidBusinessChatContactRequest @@ -104,7 +104,7 @@ createOrUpdateContactRequest getAcceptedContact :: XContactId -> IO (Maybe Contact) getAcceptedContact xContactId = do ct_ <- - maybeFirstRow (toContact vr user []) $ + maybeFirstRow (toContact cxt user []) $ DB.query db [sql| @@ -128,7 +128,7 @@ createOrUpdateContactRequest getAcceptedBusinessChat :: XContactId -> IO (Maybe GroupInfo) getAcceptedBusinessChat xContactId = do g_ <- - maybeFirstRow (toGroupInfo vr userContactId []) $ + maybeFirstRow (toGroupInfo cxt userContactId []) $ DB.query db (groupInfoQuery <> " WHERE g.business_xcontact_id = ? AND g.user_id = ? AND mu.contact_id = ?") @@ -200,12 +200,12 @@ createOrUpdateContactRequest "UPDATE contact_requests SET contact_id = ? WHERE contact_request_id = ?" (contactId, contactRequestId) ucr <- getContactRequest db user contactRequestId - ct <- getContact db vr user contactId + ct <- getContact db cxt user contactId pure $ RSCurrentRequest Nothing ucr (Just $ REContact ct) createBusinessChat = do let groupPreferences = maybe defaultBusinessGroupPrefs businessGroupPrefs $ preferences' user (gInfo@GroupInfo {groupId}, clientMember) <- - createBusinessRequestGroup db vr gVar user cReqChatVRange profile profileId ldn groupPreferences + createBusinessRequestGroup db cxt gVar user cReqChatVRange profile profileId ldn groupPreferences liftIO $ DB.execute db @@ -278,13 +278,13 @@ createOrUpdateContactRequest getRequestEntity UserContactRequest {contactRequestId, contactId_, businessGroupId_} = case (contactId_, businessGroupId_) of (Just contactId, Nothing) -> do - ct <- getContact db vr user contactId + ct <- getContact db cxt user contactId pure $ Just (REContact ct) (Nothing, Just businessGroupId) -> do - gInfo <- getGroupInfo db vr user businessGroupId + gInfo <- getGroupInfo db cxt user businessGroupId case gInfo of GroupInfo {businessChat = Just BusinessChatInfo {customerId}} -> do - clientMember <- getGroupMemberByMemberId db vr user gInfo customerId + clientMember <- getGroupMemberByMemberId db cxt user gInfo customerId pure $ Just (REBusinessChat gInfo clientMember) _ -> throwError SEInvalidBusinessChatContactRequest (Nothing, Nothing) -> pure Nothing diff --git a/src/Simplex/Chat/Store/Delivery.hs b/src/Simplex/Chat/Store/Delivery.hs index 75345e5e86..5e2e45f278 100644 --- a/src/Simplex/Chat/Store/Delivery.hs +++ b/src/Simplex/Chat/Store/Delivery.hs @@ -348,8 +348,8 @@ updateDeliveryJobStatus_ db jobId status errReason_ = do (status, errReason_, currentTs, jobId) -- TODO [relays] possible improvement is to prioritize owners and "active" members -getGroupMembersByCursor :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> Maybe GroupMemberId -> Maybe GroupMemberId -> Int -> IO [GroupMember] -getGroupMembersByCursor db vr user@User {userContactId} GroupInfo {groupId} cursorGMId_ singleSenderGMId_ count = do +getGroupMembersByCursor :: DB.Connection -> StoreCxt -> User -> GroupInfo -> Maybe GroupMemberId -> Maybe GroupMemberId -> Int -> IO [GroupMember] +getGroupMembersByCursor db cxt user@User {userContactId} GroupInfo {groupId} cursorGMId_ singleSenderGMId_ count = do gmIds :: [Int64] <- map fromOnly <$> case cursorGMId_ of Nothing -> @@ -367,13 +367,13 @@ getGroupMembersByCursor db vr user@User {userContactId} GroupInfo {groupId} curs :. (cursorGMId, count) ) #if defined(dbPostgres) - map (toContactMember vr user) <$> + map (toContactMember cxt user) <$> DB.query db - (groupMemberQuery <> " WHERE m.group_member_id IN ?") + (groupMemberQuery <> " WHERE m.group_member_id IN ? ORDER BY m.group_member_id ASC") (Only (In gmIds)) #else - rights <$> mapM (runExceptT . getGroupMemberById db vr user) gmIds + rights <$> mapM (runExceptT . getGroupMemberById db cxt user) gmIds #endif where query = diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index 71eb124bb9..886797f94a 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -249,8 +249,8 @@ createRelayMemberConnectionAsync db user@User {userId} gInfo GroupMember {groupM where customUserProfileId_ = localProfileId <$> incognitoMembershipProfile gInfo -createRelayTestConnection :: DB.Connection -> VersionRangeChat -> User -> ConnId -> ConnStatus -> VersionChat -> SubscriptionMode -> ExceptT StoreError IO Connection -createRelayTestConnection db vr user@User {userId} agentConnId connStatus chatV subMode = do +createRelayTestConnection :: DB.Connection -> StoreCxt -> User -> ConnId -> ConnStatus -> VersionChat -> SubscriptionMode -> ExceptT StoreError IO Connection +createRelayTestConnection db cxt user@User {userId} agentConnId connStatus chatV subMode = do currentTs <- liftIO getCurrentTime liftIO $ DB.execute @@ -267,7 +267,7 @@ createRelayTestConnection db vr user@User {userId} agentConnId connStatus chatV :. (BI True, currentTs, currentTs) ) connId <- liftIO $ insertedRowId db - getConnectionById db vr user connId + getConnectionById db cxt user connId updateConnLinkData :: DB.Connection -> User -> Connection -> ConnReqContact -> ConnReqUriHash -> Maybe GroupLinkId -> VersionChat -> PQSupport -> IO () updateConnLinkData db User {userId} Connection {connId} cReq cReqHash groupLinkId_ chatV pqSup = do @@ -291,13 +291,13 @@ setPreparedGroupStartedConnection db groupId = do "UPDATE groups SET conn_link_started_connection = ?, updated_at = ? WHERE group_id = ?" (BI True, currentTs, groupId) -getConnReqContactXContactId :: DB.Connection -> VersionRangeChat -> User -> ConnReqUriHash -> ConnReqUriHash -> IO (Either (Maybe Connection) Contact) -getConnReqContactXContactId db vr user@User {userId} cReqHash1 cReqHash2 = - getContactByConnReqHash db vr user cReqHash1 cReqHash2 >>= maybe (Left <$> getConnection) (pure . Right) +getConnReqContactXContactId :: DB.Connection -> StoreCxt -> User -> ConnReqUriHash -> ConnReqUriHash -> IO (Either (Maybe Connection) Contact) +getConnReqContactXContactId db cxt user@User {userId} cReqHash1 cReqHash2 = + getContactByConnReqHash db cxt user cReqHash1 cReqHash2 >>= maybe (Left <$> getConnection) (pure . Right) where getConnection :: IO (Maybe Connection) getConnection = - maybeFirstRow (toConnection vr) $ + maybeFirstRow (toConnection cxt) $ DB.query db [sql| @@ -311,10 +311,10 @@ getConnReqContactXContactId db vr user@User {userId} cReqHash1 cReqHash2 = |] (userId, cReqHash1, userId, cReqHash2) -getContactByConnReqHash :: DB.Connection -> VersionRangeChat -> User -> ConnReqUriHash -> ConnReqUriHash -> IO (Maybe Contact) -getContactByConnReqHash db vr user@User {userId} cReqHash1 cReqHash2 = do +getContactByConnReqHash :: DB.Connection -> StoreCxt -> User -> ConnReqUriHash -> ConnReqUriHash -> IO (Maybe Contact) +getContactByConnReqHash db cxt user@User {userId} cReqHash1 cReqHash2 = do ct <- - maybeFirstRow (toContact vr user []) $ + maybeFirstRow (toContact cxt user []) $ DB.query db [sql| @@ -405,19 +405,19 @@ createIncognitoProfile db User {userId} p = do -- contact_profiles row whose peer-claimed simplex_name was cleared to make -- room for the new contact's claim, so the caller can emit -- CEvtSimplexNameConflict. -createPreparedContact :: DB.Connection -> VersionRangeChat -> User -> Profile -> ACreatedConnLink -> Maybe SharedMsgId -> Maybe SimplexNameInfo -> ExceptT StoreError IO (Contact, Maybe ContactName) -createPreparedContact db vr user p connLinkToConnect welcomeSharedMsgId simplexName = do +createPreparedContact :: DB.Connection -> StoreCxt -> User -> Profile -> ACreatedConnLink -> Maybe SharedMsgId -> Maybe SimplexNameInfo -> ExceptT StoreError IO (Contact, Maybe ContactName) +createPreparedContact db cxt user p connLinkToConnect welcomeSharedMsgId simplexName = do currentTs <- liftIO getCurrentTime let prepared = Just (connLinkToConnect, welcomeSharedMsgId) ctUserPreferences = newContactUserPrefs user p (contactId, displaced) <- createContact_ db user p ctUserPreferences prepared "" currentTs simplexName - ct <- getContact db vr user contactId + ct <- getContact db cxt user contactId pure (ct, displaced) -updatePreparedContactUser :: DB.Connection -> VersionRangeChat -> User -> Contact -> User -> ExceptT StoreError IO Contact +updatePreparedContactUser :: DB.Connection -> StoreCxt -> User -> Contact -> User -> ExceptT StoreError IO Contact updatePreparedContactUser db - vr + cxt user Contact {contactId, localDisplayName = oldLDN, profile = profile@LocalProfile {profileId, displayName}} newUser@User {userId = newUserId} = do @@ -450,16 +450,16 @@ updatePreparedContactUser |] (newUserId, currentTs, contactId) safeDeleteLDN db user oldLDN - getContact db vr newUser contactId + getContact db cxt newUser contactId -- | Returns (contact, displaced) — see createPreparedContact for displaced. -createDirectContact :: DB.Connection -> VersionRangeChat -> User -> Connection -> Profile -> Maybe SimplexNameInfo -> ExceptT StoreError IO (Contact, Maybe ContactName) -createDirectContact db vr user Connection {connId, localAlias} p simplexName = do +createDirectContact :: DB.Connection -> StoreCxt -> User -> Connection -> Profile -> Maybe SimplexNameInfo -> ExceptT StoreError IO (Contact, Maybe ContactName) +createDirectContact db cxt user Connection {connId, localAlias} p simplexName = do currentTs <- liftIO getCurrentTime let ctUserPreferences = newContactUserPrefs user p (contactId, displaced) <- createContact_ db user p ctUserPreferences Nothing localAlias currentTs simplexName liftIO $ DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, currentTs, connId) - ct <- getContact db vr user contactId + ct <- getContact db cxt user contactId pure (ct, displaced) deleteContactConnections :: DB.Connection -> User -> Contact -> IO () @@ -514,13 +514,13 @@ deleteContactWithoutGroups db user@User {userId} ct@Contact {contactId, localDis deleteUnusedIncognitoProfileById_ db user profileId -- TODO remove in future versions: only used for legacy contact cleanup -getDeletedContacts :: DB.Connection -> VersionRangeChat -> User -> IO [Contact] -getDeletedContacts db vr user@User {userId} = do +getDeletedContacts :: DB.Connection -> StoreCxt -> User -> IO [Contact] +getDeletedContacts db cxt user@User {userId} = do contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 1" (Only userId) - rights <$> mapM (runExceptT . getDeletedContact db vr user) contactIds + rights <$> mapM (runExceptT . getDeletedContact db cxt user) contactIds -getDeletedContact :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO Contact -getDeletedContact db vr user contactId = getContact_ db vr user contactId True +getDeletedContact :: DB.Connection -> StoreCxt -> User -> Int64 -> ExceptT StoreError IO Contact +getDeletedContact db cxt user contactId = getContact_ db cxt user contactId True deleteContactProfile_ :: DB.Connection -> UserId -> ContactId -> IO () deleteContactProfile_ db userId contactId = @@ -801,16 +801,16 @@ updateContactLDN_ db user@User {userId} contactId displayName newName updatedAt (newName, updatedAt, userId, contactId) safeDeleteLDN db user displayName -getContactByName :: DB.Connection -> VersionRangeChat -> User -> ContactName -> ExceptT StoreError IO Contact -getContactByName db vr user localDisplayName = do +getContactByName :: DB.Connection -> StoreCxt -> User -> ContactName -> ExceptT StoreError IO Contact +getContactByName db cxt user localDisplayName = do cId <- getContactIdByName db user localDisplayName - getContact db vr user cId + getContact db cxt user cId -getContactBySimplexName :: DB.Connection -> VersionRangeChat -> User -> SimplexNameInfo -> ExceptT StoreError IO (Maybe Contact) -getContactBySimplexName db vr user ni = +getContactBySimplexName :: DB.Connection -> StoreCxt -> User -> SimplexNameInfo -> ExceptT StoreError IO (Maybe Contact) +getContactBySimplexName db cxt user ni = liftIO (getContactIdBySimplexName db user ni) >>= \case Nothing -> pure Nothing - Just cId -> Just <$> getContact db vr user cId + Just cId -> Just <$> getContact db cxt user cId getContactIdBySimplexName :: DB.Connection -> User -> SimplexNameInfo -> IO (Maybe Int64) getContactIdBySimplexName db User {userId} ni = @@ -823,10 +823,10 @@ getContactIdBySimplexName db User {userId} ni = |] (userId, ni) -getUserContacts :: DB.Connection -> VersionRangeChat -> User -> IO [Contact] -getUserContacts db vr user@User {userId} = do +getUserContacts :: DB.Connection -> StoreCxt -> User -> IO [Contact] +getUserContacts db cxt user@User {userId} = do contactIds <- map fromOnly <$> DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND deleted = 0" (Only userId) - contacts <- rights <$> mapM (runExceptT . getContact db vr user) contactIds + contacts <- rights <$> mapM (runExceptT . getContact db cxt user) contactIds pure $ filter (\Contact {activeConn} -> isJust activeConn) contacts getUserContactLinkIdByCReq :: DB.Connection -> Int64 -> ExceptT StoreError IO (Maybe Int64) @@ -954,22 +954,22 @@ getContactIdByName db User {userId} cName = ExceptT . firstRow fromOnly (SEContactNotFoundByName cName) $ DB.query db "SELECT contact_id FROM contacts WHERE user_id = ? AND local_display_name = ? AND deleted = 0" (userId, cName) -getContactViaShortLinkToConnect :: forall c. ConnectionModeI c => DB.Connection -> VersionRangeChat -> User -> ConnShortLink c -> ExceptT StoreError IO (Maybe (ConnectionRequestUri c, Contact)) -getContactViaShortLinkToConnect db vr user@User {userId} shortLink = do +getContactViaShortLinkToConnect :: forall c. ConnectionModeI c => DB.Connection -> StoreCxt -> User -> ConnShortLink c -> ExceptT StoreError IO (Maybe (ConnectionRequestUri c, Contact)) +getContactViaShortLinkToConnect db cxt user@User {userId} shortLink = do liftIO (maybeFirstRow id $ DB.query db "SELECT contact_id, conn_full_link_to_connect FROM contacts WHERE user_id = ? AND conn_short_link_to_connect = ?" (userId, shortLink)) >>= \case Just (ctId :: Int64, Just (ACR cMode cReq)) -> case testEquality cMode (sConnectionMode @c) of - Just Refl -> Just . (cReq,) <$> getContact db vr user ctId + Just Refl -> Just . (cReq,) <$> getContact db cxt user ctId Nothing -> pure Nothing _ -> pure Nothing -getContact :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO Contact -getContact db vr user contactId = getContact_ db vr user contactId False +getContact :: DB.Connection -> StoreCxt -> User -> Int64 -> ExceptT StoreError IO Contact +getContact db cxt user contactId = getContact_ db cxt user contactId False -getContact_ :: DB.Connection -> VersionRangeChat -> User -> Int64 -> Bool -> ExceptT StoreError IO Contact -getContact_ db vr user@User {userId} contactId deleted = do +getContact_ :: DB.Connection -> StoreCxt -> User -> Int64 -> Bool -> ExceptT StoreError IO Contact +getContact_ db cxt user@User {userId} contactId deleted = do chatTags <- liftIO $ getDirectChatTags db contactId - ExceptT . firstRow (toContact vr user chatTags) (SEContactNotFound contactId) $ + ExceptT . firstRow (toContact cxt user chatTags) (SEContactNotFound contactId) $ DB.query db [sql| @@ -996,8 +996,8 @@ getUserByContactRequestId db contactRequestId = ExceptT . firstRow toUser (SEUserNotFoundByContactRequestId contactRequestId) $ DB.query db (userQuery <> " JOIN contact_requests cr ON cr.user_id = u.user_id WHERE cr.contact_request_id = ?") (Only contactRequestId) -getContactConnections :: DB.Connection -> VersionRangeChat -> UserId -> Contact -> IO [Connection] -getContactConnections db vr userId Contact {contactId} = +getContactConnections :: DB.Connection -> StoreCxt -> UserId -> Contact -> IO [Connection] +getContactConnections db cxt userId Contact {contactId} = connections =<< liftIO getConnections_ where getConnections_ = @@ -1014,11 +1014,11 @@ getContactConnections db vr userId Contact {contactId} = |] (userId, userId, contactId) connections [] = pure [] - connections rows = pure $ map (toConnection vr) rows + connections rows = pure $ map (toConnection cxt) rows -getConnectionById :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO Connection -getConnectionById db vr User {userId} connId = ExceptT $ do - firstRow (toConnection vr) (SEConnectionNotFoundById connId) $ +getConnectionById :: DB.Connection -> StoreCxt -> User -> Int64 -> ExceptT StoreError IO Connection +getConnectionById db cxt User {userId} connId = ExceptT $ do + firstRow (toConnection cxt) (SEConnectionNotFoundById connId) $ DB.query db [sql| diff --git a/src/Simplex/Chat/Store/Files.hs b/src/Simplex/Chat/Store/Files.hs index 951fce8958..5289a3b304 100644 --- a/src/Simplex/Chat/Store/Files.hs +++ b/src/Simplex/Chat/Store/Files.hs @@ -570,19 +570,19 @@ getRcvFileTransfer_ db userId fileId = do Just fp -> pure fp cancelled = maybe False unBI cancelled_ -acceptRcvInlineFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem -acceptRcvInlineFT db vr user fileId filePath = do +acceptRcvInlineFT :: DB.Connection -> StoreCxt -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem +acceptRcvInlineFT db cxt user fileId filePath = do liftIO $ acceptRcvFT_ db user fileId filePath False (Just IFMOffer) =<< getCurrentTime - getChatItemByFileId db vr user fileId + getChatItemByFileId db cxt user fileId startRcvInlineFT :: DB.Connection -> User -> RcvFileTransfer -> FilePath -> Maybe InlineFileMode -> IO () startRcvInlineFT db user RcvFileTransfer {fileId} filePath rcvFileInline = acceptRcvFT_ db user fileId filePath False rcvFileInline =<< getCurrentTime -xftpAcceptRcvFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> Bool -> ExceptT StoreError IO AChatItem -xftpAcceptRcvFT db vr user fileId filePath userApprovedRelays = do +xftpAcceptRcvFT :: DB.Connection -> StoreCxt -> User -> FileTransferId -> FilePath -> Bool -> ExceptT StoreError IO AChatItem +xftpAcceptRcvFT db cxt user fileId filePath userApprovedRelays = do liftIO $ acceptRcvFT_ db user fileId filePath userApprovedRelays Nothing =<< getCurrentTime - getChatItemByFileId db vr user fileId + getChatItemByFileId db cxt user fileId acceptRcvFT_ :: DB.Connection -> User -> FileTransferId -> FilePath -> Bool -> Maybe InlineFileMode -> UTCTime -> IO () acceptRcvFT_ db User {userId} fileId filePath userApprovedRelays rcvFileInline currentTs = do @@ -860,9 +860,9 @@ getLocalCryptoFile db userId fileId sent = pure $ CryptoFile filePath fileCryptoArgs _ -> throwError $ SEFileNotFound fileId -updateDirectCIFileStatus :: forall d. MsgDirectionI d => DB.Connection -> VersionRangeChat -> User -> Int64 -> CIFileStatus d -> ExceptT StoreError IO AChatItem -updateDirectCIFileStatus db vr user fileId fileStatus = do - aci@(AChatItem cType d cInfo ci) <- getChatItemByFileId db vr user fileId +updateDirectCIFileStatus :: forall d. MsgDirectionI d => DB.Connection -> StoreCxt -> User -> Int64 -> CIFileStatus d -> ExceptT StoreError IO AChatItem +updateDirectCIFileStatus db cxt user fileId fileStatus = do + aci@(AChatItem cType d cInfo ci) <- getChatItemByFileId db cxt user fileId case (cType, testEquality d $ msgDirection @d) of (SCTDirect, Just Refl) -> do liftIO $ updateCIFileStatus db user fileId fileStatus diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index 7004d61066..ce5a36e93c 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -257,9 +257,9 @@ createGroupLink db gVar user@User {userId} groupInfo@GroupInfo {groupId, localDi void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId ConnNew initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode PQSupportOff Nothing getGroupLink db user groupInfo -getGroupLinkConnection :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> ExceptT StoreError IO Connection -getGroupLinkConnection db vr User {userId} groupInfo@GroupInfo {groupId} = - ExceptT . firstRow (toConnection vr) (SEGroupLinkNotFound groupInfo) $ +getGroupLinkConnection :: DB.Connection -> StoreCxt -> User -> GroupInfo -> ExceptT StoreError IO Connection +getGroupLinkConnection db cxt User {userId} groupInfo@GroupInfo {groupId} = + ExceptT . firstRow (toConnection cxt) (SEGroupLinkNotFound groupInfo) $ DB.query db [sql| @@ -354,8 +354,8 @@ setGroupLinkShortLink db gLnk@GroupLink {userContactLinkId, connLinkContact = CC pure gLnk {connLinkContact = CCLink connFullLink (Just shortLink), shortLinkDataSet = True, shortLinkLargeDataSet = BoolDef True} -- | creates completely new group with a single member - the current user -createNewGroup :: DB.Connection -> VersionRangeChat -> User -> GroupProfile -> Maybe Profile -> Bool -> MemberId -> Maybe GroupKeys -> Maybe Int64 -> ExceptT StoreError IO GroupInfo -createNewGroup db vr user@User {userId} groupProfile incognitoProfile useRelays memberId groupKeys publicMemberCount_ = ExceptT $ do +createNewGroup :: DB.Connection -> StoreCxt -> User -> GroupProfile -> Maybe Profile -> Bool -> MemberId -> Maybe GroupKeys -> Maybe Int64 -> ExceptT StoreError IO GroupInfo +createNewGroup db cxt user@User {userId} groupProfile incognitoProfile useRelays memberId groupKeys publicMemberCount_ = ExceptT $ do let GroupProfile {displayName, fullName, shortDescr, description, image, publicGroup, groupPreferences, memberAdmission} = groupProfile (groupType_, groupLink_, publicGroupId_) = case publicGroup of Just PublicGroupProfile {groupType, groupLink, publicGroupId} -> (Just groupType, Just groupLink, Just publicGroupId) @@ -399,7 +399,7 @@ createNewGroup db vr user@User {userId} groupProfile incognitoProfile useRelays ) insertedRowId db let memberPubKey = C.publicKey . memberPrivKey <$> groupKeys - membership <- createContactMemberInv_ db user groupId Nothing user (MemberIdRole memberId GROwner) GCUserMember GSMemCreator IBUser customUserProfileId memberPubKey currentTs vr + membership <- createContactMemberInv_ db user groupId Nothing user (MemberIdRole memberId GROwner) GCUserMember GSMemCreator IBUser customUserProfileId memberPubKey currentTs (vr cxt) let chatSettings = ChatSettings {enableNtfs = MFAll, sendRcpts = Nothing, favorite = False} pure GroupInfo @@ -431,13 +431,13 @@ createNewGroup db vr user@User {userId} groupProfile incognitoProfile useRelays } -- | creates a new group record for the group the current user was invited to, or returns an existing one -createGroupInvitation :: DB.Connection -> VersionRangeChat -> User -> Contact -> GroupInvitation -> Maybe ProfileId -> ExceptT StoreError IO (GroupInfo, GroupMemberId) +createGroupInvitation :: DB.Connection -> StoreCxt -> User -> Contact -> GroupInvitation -> Maybe ProfileId -> ExceptT StoreError IO (GroupInfo, GroupMemberId) createGroupInvitation _ _ _ Contact {localDisplayName, activeConn = Nothing} _ _ = throwError $ SEContactNotReady localDisplayName -createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activeConn = Just Connection {peerChatVRange}} GroupInvitation {fromMember, invitedMember, connRequest, groupProfile, business} incognitoProfileId = do +createGroupInvitation db cxt user@User {userId} contact@Contact {contactId, activeConn = Just Connection {peerChatVRange}} GroupInvitation {fromMember, invitedMember, connRequest, groupProfile, business} incognitoProfileId = do liftIO getInvitationGroupId_ >>= \case Nothing -> createGroupInvitation_ Just gId -> do - gInfo@GroupInfo {membership, groupProfile = p'} <- getGroupInfo db vr user gId + gInfo@GroupInfo {membership, groupProfile = p'} <- getGroupInfo db cxt user gId hostId <- getHostMemberId_ db user gId let GroupMember {groupMemberId, memberId, memberRole} = membership MemberIdRole {memberId = invMemberId, memberRole = invMemberRole} = invitedMember @@ -476,9 +476,9 @@ createGroupInvitation db vr user@User {userId} contact@Contact {contactId, activ |] ((profileId, localDisplayName, connRequest, userId, BI True, currentTs, currentTs, currentTs, currentTs) :. businessChatInfoRow business) insertedRowId db - let hostVRange = adjustedMemberVRange vr peerChatVRange + let hostVRange = adjustedMemberVRange (vr cxt) peerChatVRange GroupMember {groupMemberId} <- createContactMemberInv_ db user groupId Nothing contact fromMember GCHostMember GSMemInvited IBUnknown Nothing Nothing currentTs hostVRange - membership <- createContactMemberInv_ db user groupId (Just groupMemberId) user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfileId Nothing currentTs vr + membership <- createContactMemberInv_ db user groupId (Just groupMemberId) user invitedMember GCUserMember GSMemInvited (IBContact contactId) incognitoProfileId Nothing currentTs (vr cxt) let chatSettings = ChatSettings {enableNtfs = MFAll, sendRcpts = Nothing, favorite = False} pure ( GroupInfo @@ -622,8 +622,8 @@ deleteContactCardKeepConn db connId Contact {contactId, profile = LocalProfile { DB.execute db "DELETE FROM contacts WHERE contact_id = ?" (Only contactId) DB.execute db "DELETE FROM contact_profiles WHERE contact_profile_id = ?" (Only profileId) -createPreparedGroup :: DB.Connection -> TVar ChaChaDRG -> VersionRangeChat -> User -> GroupProfile -> Bool -> CreatedLinkContact -> Maybe SharedMsgId -> Bool -> GroupMemberRole -> Maybe Int64 -> Maybe SimplexNameInfo -> ExceptT StoreError IO (GroupInfo, Maybe GroupMember) -createPreparedGroup db gVar vr user@User {userId, userContactId} groupProfile business connLinkToConnect welcomeSharedMsgId useRelays userMemberRole publicMemberCount_ simplexName = do +createPreparedGroup :: DB.Connection -> TVar ChaChaDRG -> StoreCxt -> User -> GroupProfile -> Bool -> CreatedLinkContact -> Maybe SharedMsgId -> Bool -> GroupMemberRole -> Maybe Int64 -> Maybe SimplexNameInfo -> ExceptT StoreError IO (GroupInfo, Maybe GroupMember) +createPreparedGroup db gVar cxt user@User {userId, userContactId} groupProfile business connLinkToConnect welcomeSharedMsgId useRelays userMemberRole publicMemberCount_ simplexName = do currentTs <- liftIO getCurrentTime let prepared = Just (connLinkToConnect, welcomeSharedMsgId) (groupId, groupLDN) <- createGroup_ db userId groupProfile prepared Nothing useRelays Nothing publicMemberCount_ currentTs simplexName @@ -637,11 +637,11 @@ createPreparedGroup db gVar vr user@User {userId, userContactId} groupProfile bu else pure $ MemberId $ encodeUtf8 groupLDN <> "_user_unknown_id" let userMember = MemberIdRole userMemberId userMemberRole -- TODO [member keys] user key must be included here. Should key be added when group is prepared? - membership <- createContactMemberInv_ db user groupId hostMemberId_ user userMember GCUserMember GSMemUnknown IBUnknown Nothing Nothing currentTs vr - hostMember_ <- forM hostMemberId_ $ getGroupMember db vr user groupId + membership <- createContactMemberInv_ db user groupId hostMemberId_ user userMember GCUserMember GSMemUnknown IBUnknown Nothing Nothing currentTs (vr cxt) + hostMember_ <- forM hostMemberId_ $ getGroupMember db cxt user groupId forM_ hostMember_ $ \hostMember -> when business $ liftIO $ setGroupBusinessChatInfo groupId membership hostMember - g <- getGroupInfo db vr user groupId + g <- getGroupInfo db cxt user groupId pure (g, hostMember_) where insertHost_ currentTs groupId groupLDN = do @@ -681,13 +681,13 @@ updateBusinessChatInfo db groupId businessChatInfo = |] (businessChatInfoRow businessChatInfo :. (Only groupId)) -updatePreparedGroupUser :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> Maybe GroupMember -> User -> ExceptT StoreError IO GroupInfo -updatePreparedGroupUser db vr user gInfo@GroupInfo {groupId, membership} hostMember_ newUser@User {userId = newUserId} = do +updatePreparedGroupUser :: DB.Connection -> StoreCxt -> User -> GroupInfo -> Maybe GroupMember -> User -> ExceptT StoreError IO GroupInfo +updatePreparedGroupUser db cxt user gInfo@GroupInfo {groupId, membership} hostMember_ newUser@User {userId = newUserId} = do currentTs <- liftIO getCurrentTime updateGroup gInfo currentTs liftIO $ updateMembership membership currentTs forM_ hostMember_ $ \hostMember -> updateHostMember hostMember currentTs - getGroupInfo db vr newUser groupId + getGroupInfo db cxt newUser groupId where updateGroup GroupInfo {localDisplayName = oldGroupLDN, groupProfile = GroupProfile {displayName = groupDisplayName}} currentTs = ExceptT . withLocalDisplayName db newUserId groupDisplayName $ \newGroupLDN -> runExceptT $ do @@ -753,21 +753,21 @@ updatePreparedGroupUser db vr user gInfo@GroupInfo {groupId, membership} hostMem (newUserId, currentTs, hostProfileId) safeDeleteLDN db user oldHostLDN -updatePreparedUserAndHostMembersInvited :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupMember -> GroupLinkInvitation -> ExceptT StoreError IO (GroupInfo, GroupMember) -updatePreparedUserAndHostMembersInvited db vr user gInfo hostMember GroupLinkInvitation {fromMember, fromMemberName, invitedMember, groupProfile, accepted, business} = do +updatePreparedUserAndHostMembersInvited :: DB.Connection -> StoreCxt -> User -> GroupInfo -> GroupMember -> GroupLinkInvitation -> ExceptT StoreError IO (GroupInfo, GroupMember) +updatePreparedUserAndHostMembersInvited db cxt user gInfo hostMember GroupLinkInvitation {fromMember, fromMemberName, invitedMember, groupProfile, accepted, business} = do let fromMemberProfile = profileFromName fromMemberName initialStatus = maybe GSMemAccepted (acceptanceToStatus $ memberAdmission groupProfile) accepted - updatePreparedUserAndHostMembers' db vr user gInfo hostMember fromMember fromMemberProfile invitedMember groupProfile business initialStatus + updatePreparedUserAndHostMembers' db cxt user gInfo hostMember fromMember fromMemberProfile invitedMember groupProfile business initialStatus -updatePreparedUserAndHostMembersRejected :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupMember -> GroupLinkRejection -> ExceptT StoreError IO (GroupInfo, GroupMember) -updatePreparedUserAndHostMembersRejected db vr user gInfo hostMember GroupLinkRejection {fromMember = fromMember@MemberIdRole {memberId}, invitedMember, groupProfile} = do +updatePreparedUserAndHostMembersRejected :: DB.Connection -> StoreCxt -> User -> GroupInfo -> GroupMember -> GroupLinkRejection -> ExceptT StoreError IO (GroupInfo, GroupMember) +updatePreparedUserAndHostMembersRejected db cxt user gInfo hostMember GroupLinkRejection {fromMember = fromMember@MemberIdRole {memberId}, invitedMember, groupProfile} = do let fromMemberProfile = profileFromName $ nameFromMemberId memberId - updatePreparedUserAndHostMembers' db vr user gInfo hostMember fromMember fromMemberProfile invitedMember groupProfile Nothing GSMemRejected + updatePreparedUserAndHostMembers' db cxt user gInfo hostMember fromMember fromMemberProfile invitedMember groupProfile Nothing GSMemRejected -updatePreparedUserAndHostMembers' :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupMember -> MemberIdRole -> Profile -> MemberIdRole -> GroupProfile -> Maybe BusinessChatInfo -> GroupMemberStatus -> ExceptT StoreError IO (GroupInfo, GroupMember) +updatePreparedUserAndHostMembers' :: DB.Connection -> StoreCxt -> User -> GroupInfo -> GroupMember -> MemberIdRole -> Profile -> MemberIdRole -> GroupProfile -> Maybe BusinessChatInfo -> GroupMemberStatus -> ExceptT StoreError IO (GroupInfo, GroupMember) updatePreparedUserAndHostMembers' db - vr + cxt user gInfo@GroupInfo {groupId, membership, groupProfile = gp, businessChat} hostMember @@ -786,7 +786,7 @@ updatePreparedUserAndHostMembers' void $ updateGroupProfile db user gInfo groupProfile when (isJust businessChat && isJust business) $ liftIO $ updateBusinessChatInfo db groupId business - gInfo' <- getGroupInfo db vr user groupId + gInfo' <- getGroupInfo db cxt user groupId pure (gInfo', hostMember') where updateUserMember currentTs = do @@ -817,23 +817,23 @@ updatePreparedUserAndHostMembers' WHERE group_member_id = ? |] (memberId, memberRole, currentTs, gmId) - getGroupMemberById db vr user gmId + getGroupMemberById db cxt user gmId -createGroupInvitedViaLink :: DB.Connection -> VersionRangeChat -> User -> Connection -> GroupLinkInvitation -> ExceptT StoreError IO (GroupInfo, GroupMember) -createGroupInvitedViaLink db vr user conn GroupLinkInvitation {fromMember, fromMemberName, invitedMember, groupProfile, accepted, business} = do +createGroupInvitedViaLink :: DB.Connection -> StoreCxt -> User -> Connection -> GroupLinkInvitation -> ExceptT StoreError IO (GroupInfo, GroupMember) +createGroupInvitedViaLink db cxt user conn GroupLinkInvitation {fromMember, fromMemberName, invitedMember, groupProfile, accepted, business} = do let fromMemberProfile = profileFromName fromMemberName initialStatus = maybe GSMemAccepted (acceptanceToStatus $ memberAdmission groupProfile) accepted - createGroupViaLink' db vr user conn fromMember fromMemberProfile invitedMember groupProfile business initialStatus + createGroupViaLink' db cxt user conn fromMember fromMemberProfile invitedMember groupProfile business initialStatus -createGroupRejectedViaLink :: DB.Connection -> VersionRangeChat -> User -> Connection -> GroupLinkRejection -> ExceptT StoreError IO (GroupInfo, GroupMember) -createGroupRejectedViaLink db vr user conn GroupLinkRejection {fromMember = fromMember@MemberIdRole {memberId}, invitedMember, groupProfile} = do +createGroupRejectedViaLink :: DB.Connection -> StoreCxt -> User -> Connection -> GroupLinkRejection -> ExceptT StoreError IO (GroupInfo, GroupMember) +createGroupRejectedViaLink db cxt user conn GroupLinkRejection {fromMember = fromMember@MemberIdRole {memberId}, invitedMember, groupProfile} = do let fromMemberProfile = profileFromName $ nameFromMemberId memberId - createGroupViaLink' db vr user conn fromMember fromMemberProfile invitedMember groupProfile Nothing GSMemRejected + createGroupViaLink' db cxt user conn fromMember fromMemberProfile invitedMember groupProfile Nothing GSMemRejected -createGroupViaLink' :: DB.Connection -> VersionRangeChat -> User -> Connection -> MemberIdRole -> Profile -> MemberIdRole -> GroupProfile -> Maybe BusinessChatInfo -> GroupMemberStatus -> ExceptT StoreError IO (GroupInfo, GroupMember) +createGroupViaLink' :: DB.Connection -> StoreCxt -> User -> Connection -> MemberIdRole -> Profile -> MemberIdRole -> GroupProfile -> Maybe BusinessChatInfo -> GroupMemberStatus -> ExceptT StoreError IO (GroupInfo, GroupMember) createGroupViaLink' db - vr + cxt user@User {userId, userContactId} Connection {connId, customUserProfileId} fromMember @@ -848,9 +848,9 @@ createGroupViaLink' liftIO $ DB.execute db "UPDATE connections SET conn_type = ?, group_member_id = ?, updated_at = ? WHERE connection_id = ?" (ConnMember, hostMemberId, currentTs, connId) -- using IBUnknown since host is created without contact -- TODO [member keys] this is currently not used with public groups. If it needs to be used, member keys need to be added - void $ createContactMemberInv_ db user groupId (Just hostMemberId) user invitedMember GCUserMember membershipStatus IBUnknown customUserProfileId Nothing currentTs vr + void $ createContactMemberInv_ db user groupId (Just hostMemberId) user invitedMember GCUserMember membershipStatus IBUnknown customUserProfileId Nothing currentTs (vr cxt) liftIO $ setViaGroupLinkUri db groupId connId - (,) <$> getGroupInfo db vr user groupId <*> getGroupMemberById db vr user hostMemberId + (,) <$> getGroupInfo db cxt user groupId <*> getGroupMemberById db cxt user hostMemberId where insertHost_ currentTs groupId = do (localDisplayName, profileId) <- createNewMemberProfile_ db user fromMemberProfile currentTs @@ -911,10 +911,10 @@ setGroupInvitationChatItemId db User {userId} groupId chatItemId = do -- TODO return the last connection that is ready, not any last connection -- requires updating connection status -getGroup :: DB.Connection -> VersionRangeChat -> User -> GroupId -> ExceptT StoreError IO Group -getGroup db vr user groupId = do - gInfo <- getGroupInfo db vr user groupId - members <- liftIO $ getGroupMembers db vr user gInfo +getGroup :: DB.Connection -> StoreCxt -> User -> GroupId -> ExceptT StoreError IO Group +getGroup db cxt user groupId = do + gInfo <- getGroupInfo db cxt user groupId + members <- liftIO $ getGroupMembers db cxt user gInfo pure $ Group gInfo members deleteGroupChatItems :: DB.Connection -> User -> GroupInfo -> IO () @@ -1008,18 +1008,18 @@ deleteGroupProfile_ db userId groupId = |] (userId, groupId) -getInProgressGroups :: DB.Connection -> VersionRangeChat -> User -> UTCTime -> IO [GroupInfo] -getInProgressGroups db vr user@User {userId} createdAtCutoff = do +getInProgressGroups :: DB.Connection -> StoreCxt -> User -> UTCTime -> IO [GroupInfo] +getInProgressGroups db cxt user@User {userId} createdAtCutoff = do groupIds <- map fromOnly <$> DB.query db "SELECT group_id FROM groups WHERE user_id = ? AND creating_in_progress = 1 AND created_at <= ?" (userId, createdAtCutoff) - rights <$> mapM (runExceptT . getGroupInfo db vr user) groupIds + rights <$> mapM (runExceptT . getGroupInfo db cxt user) groupIds -getBaseGroupDetails :: DB.Connection -> VersionRangeChat -> User -> Maybe ContactId -> Maybe Text -> IO [GroupInfo] -getBaseGroupDetails db vr User {userId, userContactId} _contactId_ search_ = do - map (toGroupInfo vr userContactId []) +getBaseGroupDetails :: DB.Connection -> StoreCxt -> User -> Maybe ContactId -> Maybe Text -> IO [GroupInfo] +getBaseGroupDetails db cxt User {userId, userContactId} _contactId_ search_ = do + map (toGroupInfo cxt userContactId []) <$> DB.query db (groupInfoQuery <> " " <> condition) (userId, userContactId, search, search, search, search) where condition = @@ -1047,16 +1047,16 @@ getContactGroupPreferences db User {userId} Contact {contactId} = do |] (userId, contactId) -getGroupInfoByName :: DB.Connection -> VersionRangeChat -> User -> GroupName -> ExceptT StoreError IO GroupInfo -getGroupInfoByName db vr user gName = do +getGroupInfoByName :: DB.Connection -> StoreCxt -> User -> GroupName -> ExceptT StoreError IO GroupInfo +getGroupInfoByName db cxt user gName = do gId <- getGroupIdByName db user gName - getGroupInfo db vr user gId + getGroupInfo db cxt user gId -getGroupInfoBySimplexName :: DB.Connection -> VersionRangeChat -> User -> SimplexNameInfo -> ExceptT StoreError IO (Maybe GroupInfo) -getGroupInfoBySimplexName db vr user ni = +getGroupInfoBySimplexName :: DB.Connection -> StoreCxt -> User -> SimplexNameInfo -> ExceptT StoreError IO (Maybe GroupInfo) +getGroupInfoBySimplexName db cxt user ni = liftIO (getGroupIdBySimplexName db user ni) >>= \case Nothing -> pure Nothing - Just gId -> Just <$> getGroupInfo db vr user gId + Just gId -> Just <$> getGroupInfo db cxt user gId -- | Unlike the parallel 'getContactBySimplexName' lookup (which filters -- @ct.deleted = 0@ to match the @idx_contacts_simplex_name@ partial index), @@ -1078,17 +1078,17 @@ getGroupIdBySimplexName db User {userId} ni = maybeFirstRow fromOnly $ DB.query db "SELECT group_id FROM groups WHERE user_id = ? AND simplex_name = ?" (userId, ni) -getGroupMember :: DB.Connection -> VersionRangeChat -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO GroupMember -getGroupMember db vr user@User {userId} groupId groupMemberId = - ExceptT . firstRow (toContactMember vr user) (SEGroupMemberNotFound groupMemberId) $ +getGroupMember :: DB.Connection -> StoreCxt -> User -> GroupId -> GroupMemberId -> ExceptT StoreError IO GroupMember +getGroupMember db cxt user@User {userId} groupId groupMemberId = + ExceptT . firstRow (toContactMember cxt user) (SEGroupMemberNotFound groupMemberId) $ DB.query db (groupMemberQuery <> " WHERE m.group_id = ? AND m.group_member_id = ? AND m.user_id = ?") (groupId, groupMemberId, userId) -getHostMember :: DB.Connection -> VersionRangeChat -> User -> GroupId -> ExceptT StoreError IO GroupMember -getHostMember db vr user groupId = - ExceptT . firstRow (toContactMember vr user) (SEGroupHostMemberNotFound groupId) $ +getHostMember :: DB.Connection -> StoreCxt -> User -> GroupId -> ExceptT StoreError IO GroupMember +getHostMember db cxt user groupId = + ExceptT . firstRow (toContactMember cxt user) (SEGroupHostMemberNotFound groupId) $ DB.query db (groupMemberQuery <> " WHERE m.group_id = ? AND m.member_category = ?") @@ -1127,54 +1127,54 @@ toMentionedMember (groupMemberId, memberId, memberRole, displayName, localAlias) let memberRef = Just CIMentionMember {groupMemberId, displayName, localAlias, memberRole} in CIMention {memberId, memberRef} -getGroupMemberById :: DB.Connection -> VersionRangeChat -> User -> GroupMemberId -> ExceptT StoreError IO GroupMember -getGroupMemberById db vr user@User {userId} groupMemberId = - ExceptT . firstRow (toContactMember vr user) (SEGroupMemberNotFound groupMemberId) $ +getGroupMemberById :: DB.Connection -> StoreCxt -> User -> GroupMemberId -> ExceptT StoreError IO GroupMember +getGroupMemberById db cxt user@User {userId} groupMemberId = + ExceptT . firstRow (toContactMember cxt user) (SEGroupMemberNotFound groupMemberId) $ DB.query db (groupMemberQuery <> " WHERE m.group_member_id = ? AND m.user_id = ?") (groupMemberId, userId) -getNonRemovedMemberById :: DB.Connection -> VersionRangeChat -> User -> GroupMemberId -> ExceptT StoreError IO GroupMember -getNonRemovedMemberById db vr user@User {userId} groupMemberId = - ExceptT . firstRow (toContactMember vr user) (SEGroupMemberNotFound groupMemberId) $ +getNonRemovedMemberById :: DB.Connection -> StoreCxt -> User -> GroupMemberId -> ExceptT StoreError IO GroupMember +getNonRemovedMemberById db cxt user@User {userId} groupMemberId = + ExceptT . firstRow (toContactMember cxt user) (SEGroupMemberNotFound groupMemberId) $ DB.query db (groupMemberQuery <> " WHERE m.group_member_id = ? AND m.user_id = ? AND m.member_status NOT IN (?,?,?,?)") (groupMemberId, userId, GSMemRejected, GSMemRemoved, GSMemLeft, GSMemGroupDeleted) -getGroupMemberByIndex :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> Int64 -> ExceptT StoreError IO GroupMember -getGroupMemberByIndex db vr user GroupInfo {groupId} indexInGroup = - ExceptT . firstRow (toContactMember vr user) (SEGroupMemberNotFoundByIndex indexInGroup) $ +getGroupMemberByIndex :: DB.Connection -> StoreCxt -> User -> GroupInfo -> Int64 -> ExceptT StoreError IO GroupMember +getGroupMemberByIndex db cxt user GroupInfo {groupId} indexInGroup = + ExceptT . firstRow (toContactMember cxt user) (SEGroupMemberNotFoundByIndex indexInGroup) $ DB.query db (groupMemberQuery <> " WHERE m.group_id = ? AND m.index_in_group = ?") (groupId, indexInGroup) -getSupportScopeMemberByIndex :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupMemberId -> Int64 -> ExceptT StoreError IO GroupMember -getSupportScopeMemberByIndex db vr user GroupInfo {groupId} scopeGMId indexInGroup = - ExceptT . firstRow (toContactMember vr user) (SEGroupMemberNotFoundByIndex indexInGroup) $ +getSupportScopeMemberByIndex :: DB.Connection -> StoreCxt -> User -> GroupInfo -> GroupMemberId -> Int64 -> ExceptT StoreError IO GroupMember +getSupportScopeMemberByIndex db cxt user GroupInfo {groupId} scopeGMId indexInGroup = + ExceptT . firstRow (toContactMember cxt user) (SEGroupMemberNotFoundByIndex indexInGroup) $ DB.query db (groupMemberQuery <> " WHERE m.group_id = ? AND m.index_in_group = ? AND (m.member_role IN (?,?,?) OR m.group_member_id = ?)") (groupId, indexInGroup, GRModerator, GRAdmin, GROwner, scopeGMId) -getGroupMemberByMemberId :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> MemberId -> ExceptT StoreError IO GroupMember -getGroupMemberByMemberId db vr user GroupInfo {groupId} memberId = - ExceptT . firstRow (toContactMember vr user) (SEGroupMemberNotFoundByMemberId memberId) $ +getGroupMemberByMemberId :: DB.Connection -> StoreCxt -> User -> GroupInfo -> MemberId -> ExceptT StoreError IO GroupMember +getGroupMemberByMemberId db cxt user GroupInfo {groupId} memberId = + ExceptT . firstRow (toContactMember cxt user) (SEGroupMemberNotFoundByMemberId memberId) $ DB.query db (groupMemberQuery <> " WHERE m.group_id = ? AND m.member_id = ?") (groupId, memberId) -getCreateUnknownGMByMemberId :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> MemberId -> ContactName -> GroupMemberRole -> Bool -> ExceptT StoreError IO (Maybe (GroupMember, Bool)) -getCreateUnknownGMByMemberId db vr user gInfo memberId memberName unknownMemberRole allowCreate = do - liftIO (runExceptT $ getGroupMemberByMemberId db vr user gInfo memberId) >>= \case +getCreateUnknownGMByMemberId :: DB.Connection -> StoreCxt -> User -> GroupInfo -> MemberId -> ContactName -> GroupMemberRole -> Bool -> ExceptT StoreError IO (Maybe (GroupMember, Bool)) +getCreateUnknownGMByMemberId db cxt user gInfo memberId memberName unknownMemberRole allowCreate = do + liftIO (runExceptT $ getGroupMemberByMemberId db cxt user gInfo memberId) >>= \case Right m -> pure $ Just (m, False) Left (SEGroupMemberNotFoundByMemberId _) | allowCreate -> do let name = if T.null memberName then nameFromMemberId memberId else memberName - m <- createNewUnknownGroupMember db vr user gInfo memberId name unknownMemberRole + m <- createNewUnknownGroupMember db cxt user gInfo memberId name unknownMemberRole pure $ Just (m, True) | otherwise -> pure Nothing Left e -> throwError e @@ -1193,59 +1193,59 @@ getGroupMemberIdViaMemberId db User {userId} GroupInfo {groupId} memberId = "SELECT group_member_id FROM group_members WHERE user_id = ? AND group_id = ? AND member_id = ?" (userId, groupId, memberId) -getGroupMembers :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> IO [GroupMember] -getGroupMembers db vr user@User {userId, userContactId} GroupInfo {groupId} = - map (toContactMember vr user) +getGroupMembers :: DB.Connection -> StoreCxt -> User -> GroupInfo -> IO [GroupMember] +getGroupMembers db cxt user@User {userId, userContactId} GroupInfo {groupId} = + map (toContactMember cxt user) <$> DB.query db (groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?)") (userId, groupId, userContactId) -getGroupMembersByIndexes :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> [Int64] -> IO [GroupMember] -getGroupMembersByIndexes db vr user gInfo indexesInGroup = do +getGroupMembersByIndexes :: DB.Connection -> StoreCxt -> User -> GroupInfo -> [Int64] -> IO [GroupMember] +getGroupMembersByIndexes db cxt user gInfo indexesInGroup = do #if defined(dbPostgres) let GroupInfo {groupId} = gInfo - map (toContactMember vr user) <$> + map (toContactMember cxt user) <$> DB.query db (groupMemberQuery <> " WHERE m.group_id = ? AND m.index_in_group IN ?") (groupId, In indexesInGroup) #else - rights <$> mapM (runExceptT . getGroupMemberByIndex db vr user gInfo) indexesInGroup + rights <$> mapM (runExceptT . getGroupMemberByIndex db cxt user gInfo) indexesInGroup #endif -getSupportScopeMembersByIndexes :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupMemberId -> [Int64] -> IO [GroupMember] -getSupportScopeMembersByIndexes db vr user gInfo scopeGMId indexesInGroup = do +getSupportScopeMembersByIndexes :: DB.Connection -> StoreCxt -> User -> GroupInfo -> GroupMemberId -> [Int64] -> IO [GroupMember] +getSupportScopeMembersByIndexes db cxt user gInfo scopeGMId indexesInGroup = do #if defined(dbPostgres) let GroupInfo {groupId} = gInfo - map (toContactMember vr user) <$> + map (toContactMember cxt user) <$> DB.query db (groupMemberQuery <> " WHERE m.group_id = ? AND m.index_in_group IN ? AND (m.member_role IN (?,?,?) OR m.group_member_id = ?)") (groupId, In indexesInGroup, GRModerator, GRAdmin, GROwner, scopeGMId) #else - rights <$> mapM (runExceptT . getSupportScopeMemberByIndex db vr user gInfo scopeGMId) indexesInGroup + rights <$> mapM (runExceptT . getSupportScopeMemberByIndex db cxt user gInfo scopeGMId) indexesInGroup #endif -getGroupModerators :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> IO [GroupMember] -getGroupModerators db vr user@User {userId, userContactId} GroupInfo {groupId} = do - map (toContactMember vr user) +getGroupModerators :: DB.Connection -> StoreCxt -> User -> GroupInfo -> IO [GroupMember] +getGroupModerators db cxt user@User {userId, userContactId} GroupInfo {groupId} = do + map (toContactMember cxt user) <$> DB.query db (groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND (m.contact_id IS NULL OR m.contact_id != ?) AND m.member_role IN (?,?,?)") (userId, groupId, userContactId, GRModerator, GRAdmin, GROwner) -getGroupRelayMembers :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> IO [GroupMember] -getGroupRelayMembers db vr user@User {userId, userContactId} GroupInfo {groupId} = do - map (toContactMember vr user) +getGroupRelayMembers :: DB.Connection -> StoreCxt -> User -> GroupInfo -> IO [GroupMember] +getGroupRelayMembers db cxt user@User {userId, userContactId} GroupInfo {groupId} = do + map (toContactMember cxt user) <$> DB.query db (groupMemberQuery <> " WHERE m.user_id = ? AND m.group_id = ? AND m.contact_id IS DISTINCT FROM ? AND m.member_role = ?") (userId, groupId, userContactId, GRRelay) -getGroupMembersForExpiration :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> IO [GroupMember] -getGroupMembersForExpiration db vr user@User {userId, userContactId} GroupInfo {groupId} = do - map (toContactMember vr user) +getGroupMembersForExpiration :: DB.Connection -> StoreCxt -> User -> GroupInfo -> IO [GroupMember] +getGroupMembersForExpiration db cxt user@User {userId, userContactId} GroupInfo {groupId} = do + map (toContactMember cxt user) <$> DB.query db ( groupMemberQuery @@ -1260,22 +1260,22 @@ getGroupMembersForExpiration db vr user@User {userId, userContactId} GroupInfo { ) (groupId, userId, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted, GSMemUnknown) -getRemovedMembersToCleanup :: DB.Connection -> VersionRangeChat -> User -> UTCTime -> IO [GroupMember] -getRemovedMembersToCleanup db vr user@User {userId} cutoffTs = - map (toContactMember vr user) +getRemovedMembersToCleanup :: DB.Connection -> StoreCxt -> User -> UTCTime -> IO [GroupMember] +getRemovedMembersToCleanup db cxt user@User {userId} cutoffTs = + map (toContactMember cxt user) <$> DB.query db (groupMemberQuery <> " WHERE m.user_id = ? AND m.removed_at < ?") (userId, cutoffTs) -getGroupInvitation :: DB.Connection -> VersionRangeChat -> User -> GroupId -> ExceptT StoreError IO ReceivedGroupInvitation -getGroupInvitation db vr user groupId = +getGroupInvitation :: DB.Connection -> StoreCxt -> User -> GroupId -> ExceptT StoreError IO ReceivedGroupInvitation +getGroupInvitation db cxt user groupId = getConnRec_ user >>= \case Just connRequest -> do - groupInfo@GroupInfo {membership} <- getGroupInfo db vr user groupId + groupInfo@GroupInfo {membership} <- getGroupInfo db cxt user groupId when (memberStatus membership /= GSMemInvited) $ throwError SEGroupAlreadyJoined hostId <- getHostMemberId_ db user groupId - fromMember <- getGroupMember db vr user groupId hostId + fromMember <- getGroupMember db cxt user groupId hostId pure ReceivedGroupInvitation {fromMember, connRequest, groupInfo} _ -> throwError SEGroupInvitationNotFound where @@ -1413,8 +1413,8 @@ toGroupRelay ((groupRelayId, groupMemberId, chatRelayId, address, displayName, f relayCap = RelayCapabilities {webDomain} in GroupRelay {groupRelayId, groupMemberId, userChatRelay, relayStatus, relayLink, relayCap} -createRelayForOwner :: DB.Connection -> VersionRangeChat -> TVar ChaChaDRG -> User -> GroupInfo -> UserChatRelay -> ExceptT StoreError IO GroupMember -createRelayForOwner db vr gVar user@User {userId, userContactId} GroupInfo {groupId, membership} UserChatRelay {relayProfile = RelayProfile {displayName}} = do +createRelayForOwner :: DB.Connection -> StoreCxt -> TVar ChaChaDRG -> User -> GroupInfo -> UserChatRelay -> ExceptT StoreError IO GroupMember +createRelayForOwner db cxt gVar user@User {userId, userContactId} GroupInfo {groupId, membership} UserChatRelay {relayProfile = RelayProfile {displayName}} = do currentTs <- liftIO getCurrentTime let relayProfile = profileFromName displayName (localDisplayName, memProfileId) <- createNewMemberProfile_ db user relayProfile currentTs @@ -1433,14 +1433,14 @@ createRelayForOwner db vr gVar user@User {userId, userContactId} GroupInfo {grou :. (userId, localDisplayName, memProfileId, currentTs, currentTs) ) liftIO $ insertedRowId db - getGroupMemberById db vr user groupMemberId + getGroupMemberById db cxt user groupMemberId -getCreateRelayForMember :: DB.Connection -> VersionRangeChat -> TVar ChaChaDRG -> User -> GroupInfo -> ShortLinkContact -> ExceptT StoreError IO GroupMember -getCreateRelayForMember db vr gVar user@User {userId, userContactId} GroupInfo {groupId, localDisplayName = groupLDN} relayLink = +getCreateRelayForMember :: DB.Connection -> StoreCxt -> TVar ChaChaDRG -> User -> GroupInfo -> ShortLinkContact -> ExceptT StoreError IO GroupMember +getCreateRelayForMember db cxt gVar user@User {userId, userContactId} GroupInfo {groupId, localDisplayName = groupLDN} relayLink = liftIO getGroupMemberByRelayLink >>= maybe createRelayMember pure where getGroupMemberByRelayLink = - maybeFirstRow (toContactMember vr user) $ + maybeFirstRow (toContactMember cxt user) $ DB.query db #if defined(dbPostgres) @@ -1471,10 +1471,10 @@ getCreateRelayForMember db vr gVar user@User {userId, userContactId} GroupInfo { :. (userId, localDisplayName, profileId, currentTs, currentTs, relayLink) ) insertedRowId db - getGroupMember db vr user groupId groupMemberId + getGroupMember db cxt user groupId groupMemberId -createRelayConnection :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ConnId -> ConnStatus -> VersionChat -> SubscriptionMode -> ExceptT StoreError IO Connection -createRelayConnection db vr user@User {userId} groupMemberId agentConnId connStatus chatV subMode = do +createRelayConnection :: DB.Connection -> StoreCxt -> User -> Int64 -> ConnId -> ConnStatus -> VersionChat -> SubscriptionMode -> ExceptT StoreError IO Connection +createRelayConnection db cxt user@User {userId} groupMemberId agentConnId connStatus chatV subMode = do currentTs <- liftIO getCurrentTime liftIO $ DB.execute @@ -1491,7 +1491,7 @@ createRelayConnection db vr user@User {userId} groupMemberId agentConnId connSta :. (currentTs, currentTs) ) connId <- liftIO $ insertedRowId db - getConnectionById db vr user connId + getConnectionById db cxt user connId updateRelayStatus :: DB.Connection -> GroupRelay -> RelayStatus -> IO GroupRelay updateRelayStatus db relay@GroupRelay {groupRelayId} relayStatus = @@ -1508,8 +1508,8 @@ updateRelayStatus_ db relayId relayStatus = do currentTs <- getCurrentTime DB.execute db "UPDATE group_relays SET relay_status = ?, updated_at = ? WHERE group_relay_id = ?" (relayStatus, currentTs, relayId) -setRelayLinkAccepted :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> MemberKey -> Profile -> ExceptT StoreError IO (GroupMember, GroupRelay) -setRelayLinkAccepted db vr user m (MemberKey relayKey) profile = do +setRelayLinkAccepted :: DB.Connection -> StoreCxt -> User -> GroupMember -> MemberKey -> Profile -> ExceptT StoreError IO (GroupMember, GroupRelay) +setRelayLinkAccepted db cxt user m (MemberKey relayKey) profile = do let gmId = groupMemberId' m currentTs <- liftIO getCurrentTime liftIO $ DB.execute @@ -1529,7 +1529,7 @@ setRelayLinkAccepted db vr user m (MemberKey relayKey) profile = do |] (relayKey, currentTs, gmId) void $ updateMemberProfile db user m profile - (,) <$> getGroupMemberById db vr user gmId <*> getGroupRelayByGMId db gmId + (,) <$> getGroupMemberById db cxt user gmId <*> getGroupRelayByGMId db gmId setRelayLinkConfId :: DB.Connection -> GroupMember -> ConfirmationId -> ShortLinkContact -> IO () setRelayLinkConfId db m confId relayLink = do @@ -1597,8 +1597,8 @@ setGroupInProgressDone db GroupInfo {groupId} = do "UPDATE groups SET creating_in_progress = 0, updated_at = ? WHERE group_id = ?" (currentTs, groupId) -createRelayRequestGroup :: DB.Connection -> VersionRangeChat -> User -> GroupRelayInvitation -> InvitationId -> VersionRangeChat -> Int64 -> GroupMemberStatus -> RelayStatus -> ExceptT StoreError IO (GroupInfo, GroupMember) -createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMember, fromMemberProfile, relayMemberId, groupLink} invId reqChatVRange initialDelay memberStatus relayStatus = do +createRelayRequestGroup :: DB.Connection -> StoreCxt -> User -> GroupRelayInvitation -> InvitationId -> VersionRangeChat -> Int64 -> GroupMemberStatus -> RelayStatus -> ExceptT StoreError IO (GroupInfo, GroupMember) +createRelayRequestGroup db cxt user@User {userId} GroupRelayInvitation {fromMember, fromMemberProfile, relayMemberId, groupLink} invId reqChatVRange initialDelay memberStatus relayStatus = do currentTs <- liftIO getCurrentTime -- Create group with placeholder profile let Profile {displayName = fromMemberLDN} = fromMemberProfile @@ -1619,9 +1619,9 @@ createRelayRequestGroup db vr user@User {userId} GroupRelayInvitation {fromMembe ownerMemberId <- insertOwner_ currentTs groupId let relayMember = MemberIdRole relayMemberId GRRelay -- TODO [member keys] should relays use member keys? - _membership <- createContactMemberInv_ db user groupId (Just ownerMemberId) user relayMember GCUserMember memberStatus IBUnknown Nothing Nothing currentTs vr - ownerMember <- getGroupMember db vr user groupId ownerMemberId - g <- getGroupInfo db vr user groupId + _membership <- createContactMemberInv_ db user groupId (Just ownerMemberId) user relayMember GCUserMember memberStatus IBUnknown Nothing Nothing currentTs (vr cxt) + ownerMember <- getGroupMember db cxt user groupId ownerMemberId + g <- getGroupInfo db cxt user groupId pure (g, ownerMember) where setRelayRequestData_ groupId currentTs = @@ -1673,8 +1673,8 @@ updateRelayOwnStatus_ db GroupInfo {groupId} relayStatus = do -- Flip every RSRejected row sharing the targeted group's relay_request_group_link -- to RSInactive in one statement; returns the refreshed GroupInfo for the targeted groupId. -allowRelayGroup :: DB.Connection -> VersionRangeChat -> User -> GroupId -> ExceptT StoreError IO GroupInfo -allowRelayGroup db vr user@User {userId} groupId = do +allowRelayGroup :: DB.Connection -> StoreCxt -> User -> GroupId -> ExceptT StoreError IO GroupInfo +allowRelayGroup db cxt user@User {userId} groupId = do currentTs <- liftIO getCurrentTime liftIO $ DB.execute @@ -1687,7 +1687,7 @@ allowRelayGroup db vr user@User {userId} groupId = do AND relay_own_status = ? |] (RSInactive, currentTs, currentTs, userId, groupId, RSRejected) - getGroupInfo db vr user groupId + getGroupInfo db cxt user groupId isRelayGroupRejected :: DB.Connection -> User -> ShortLinkContact -> IO Bool isRelayGroupRejected db User {userId} groupLink = @@ -1706,9 +1706,9 @@ isRelayGroupRejected db User {userId} groupLink = (userId, groupLink, RSRejected) ) -getRelayServedGroups :: DB.Connection -> VersionRangeChat -> User -> IO [GroupInfo] -getRelayServedGroups db vr User {userId, userContactId} = do - map (toGroupInfo vr userContactId []) +getRelayServedGroups :: DB.Connection -> StoreCxt -> User -> IO [GroupInfo] +getRelayServedGroups db cxt User {userId, userContactId} = do + map (toGroupInfo cxt userContactId []) <$> DB.query db ( groupInfoQuery @@ -1716,10 +1716,10 @@ getRelayServedGroups db vr User {userId, userContactId} = do ) (userId, userContactId, RSAccepted, RSActive) -getRelayInactiveGroups :: DB.Connection -> VersionRangeChat -> User -> NominalDiffTime -> IO [GroupInfo] -getRelayInactiveGroups db vr User {userId, userContactId} ttl = do +getRelayInactiveGroups :: DB.Connection -> StoreCxt -> User -> NominalDiffTime -> IO [GroupInfo] +getRelayInactiveGroups db cxt User {userId, userContactId} ttl = do cutoffTs <- addUTCTime (- ttl) <$> getCurrentTime - map (toGroupInfo vr userContactId []) + map (toGroupInfo cxt userContactId []) <$> DB.query db ( groupInfoQuery @@ -1831,10 +1831,10 @@ createJoiningMemberConnection Connection {connId} <- createConnection_ db userId ConnMember (Just groupMemberId) agentConnId ConnNew chatV cReqChatVRange Nothing (Just uclId) Nothing 0 createdAt subMode PQSupportOff Nothing setCommandConnId db user cmdId connId -createBusinessRequestGroup :: DB.Connection -> VersionRangeChat -> TVar ChaChaDRG -> User -> VersionRangeChat -> Profile -> Int64 -> Text -> GroupPreferences -> ExceptT StoreError IO (GroupInfo, GroupMember) +createBusinessRequestGroup :: DB.Connection -> StoreCxt -> TVar ChaChaDRG -> User -> VersionRangeChat -> Profile -> Int64 -> Text -> GroupPreferences -> ExceptT StoreError IO (GroupInfo, GroupMember) createBusinessRequestGroup db - vr + cxt gVar user@User {userId, userContactId} cReqChatVRange @@ -1846,8 +1846,8 @@ createBusinessRequestGroup (groupId, membership@GroupMember {memberId = userMemberId}) <- insertGroup_ currentTs (groupMemberId, memberId) <- insertClientMember_ currentTs groupId membership liftIO $ DB.execute db "UPDATE groups SET business_member_id = ?, customer_member_id = ? WHERE group_id = ?" (userMemberId, memberId, groupId) - groupInfo <- getGroupInfo db vr user groupId - clientMember <- getGroupMemberById db vr user groupMemberId + groupInfo <- getGroupInfo db cxt user groupId + clientMember <- getGroupMemberById db cxt user groupMemberId pure (groupInfo, clientMember) where insertGroup_ currentTs = do @@ -1870,7 +1870,7 @@ createBusinessRequestGroup groupId <- liftIO $ insertedRowId db memberId <- liftIO $ encodedRandomBytes gVar 12 -- TODO [member keys] we could support member keys in business groups to allow binding agreements (though identity keys would be better for it. - membership <- createContactMemberInv_ db user groupId Nothing user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser Nothing Nothing currentTs vr + membership <- createContactMemberInv_ db user groupId Nothing user (MemberIdRole (MemberId memberId) GROwner) GCUserMember GSMemCreator IBUser Nothing Nothing currentTs (vr cxt) pure (groupId, membership) VersionRange minV maxV = cReqChatVRange insertClientMember_ currentTs groupId membership = @@ -1894,8 +1894,8 @@ createBusinessRequestGroup groupMemberId <- liftIO $ insertedRowId db pure (groupMemberId, MemberId memId) -getContactViaMember :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> ExceptT StoreError IO Contact -getContactViaMember db vr user@User {userId} GroupMember {groupMemberId} = do +getContactViaMember :: DB.Connection -> StoreCxt -> User -> GroupMember -> ExceptT StoreError IO Contact +getContactViaMember db cxt user@User {userId} GroupMember {groupMemberId} = do contactId <- ExceptT $ firstRow fromOnly (SEContactNotFoundByMemberId groupMemberId) $ @@ -1909,7 +1909,7 @@ getContactViaMember db vr user@User {userId} GroupMember {groupMemberId} = do LIMIT 1 |] (userId, groupMemberId) - getContact db vr user contactId + getContact db cxt user contactId setNewContactMemberConnRequest :: DB.Connection -> User -> GroupMember -> ConnReqInvitation -> IO () setNewContactMemberConnRequest db User {userId} GroupMember {groupMemberId} connRequest = do @@ -1936,18 +1936,18 @@ createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentCon -- This is called once before connecting to relays, unlike createConnReqConnection -> setPreparedGroupLinkInfo_, -- which is used in single-connection flows. updatePreparedRelayedGroup :: - DB.Connection -> VersionRangeChat -> User -> GroupInfo -> ConnReqContact -> ConnReqUriHash -> Maybe Profile -> + DB.Connection -> StoreCxt -> User -> GroupInfo -> ConnReqContact -> ConnReqUriHash -> Maybe Profile -> C.PublicKeyEd25519 -> C.PrivateKeyEd25519 -> Maybe Int64 -> ExceptT StoreError IO GroupInfo -updatePreparedRelayedGroup db vr user@User {userId} gInfo cReq cReqHash incognitoProfile rootPubKey memberPrivKey publicMemberCount_ = do +updatePreparedRelayedGroup db cxt user@User {userId} gInfo cReq cReqHash incognitoProfile rootPubKey memberPrivKey publicMemberCount_ = do currentTs <- liftIO getCurrentTime customUserProfileId <- liftIO $ mapM (createIncognitoProfile_ db userId currentTs) incognitoProfile liftIO $ setPreparedGroupLinkInfo_ db gInfo cReq cReqHash customUserProfileId publicMemberCount_ currentTs liftIO $ updateGroupMemberKeys db (groupId' gInfo) rootPubKey memberPrivKey (groupMemberId' $ membership gInfo) - getGroupInfo db vr user (groupId' gInfo) + getGroupInfo db cxt user (groupId' gInfo) -updatePublicMemberCount :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> ExceptT StoreError IO GroupInfo -updatePublicMemberCount db vr user GroupInfo {groupId} = do +updatePublicMemberCount :: DB.Connection -> StoreCxt -> User -> GroupInfo -> ExceptT StoreError IO GroupInfo +updatePublicMemberCount db cxt user GroupInfo {groupId} = do liftIO $ do totalCount <- fromMaybe 0 <$> maybeFirstRow fromOnly (DB.query db "SELECT summary_current_members_count FROM groups WHERE group_id = ?" (Only groupId)) @@ -1963,13 +1963,13 @@ updatePublicMemberCount db vr user GroupInfo {groupId} = do let publicCount = max 0 (totalCount - relayCount) :: Int64 currentTs <- getCurrentTime DB.execute db "UPDATE groups SET public_member_count = ?, updated_at = ? WHERE group_id = ?" (publicCount, currentTs, groupId) - getGroupInfo db vr user groupId + getGroupInfo db cxt user groupId -setPublicMemberCount :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> Int64 -> ExceptT StoreError IO GroupInfo -setPublicMemberCount db vr user GroupInfo {groupId} publicCount = do +setPublicMemberCount :: DB.Connection -> StoreCxt -> User -> GroupInfo -> Int64 -> ExceptT StoreError IO GroupInfo +setPublicMemberCount db cxt user GroupInfo {groupId} publicCount = do currentTs <- liftIO getCurrentTime liftIO $ DB.execute db "UPDATE groups SET public_member_count = ?, updated_at = ? WHERE group_id = ?" (publicCount, currentTs, groupId) - getGroupInfo db vr user groupId + getGroupInfo db cxt user groupId updateGroupMemberKeys :: DB.Connection -> GroupId -> C.PublicKeyEd25519 -> C.PrivateKeyEd25519 -> GroupMemberId -> IO () updateGroupMemberKeys db groupId rootPubKey memberPrivKey membershipGMId = do @@ -2539,8 +2539,8 @@ updateGroupProfileFromMember db user g@GroupInfo {groupId} Profile {displayName let publicGroupAccess = toPublicGroupAccess accessRow in GroupProfile {displayName, fullName, shortDescr, description, image, publicGroup = toPublicGroupProfile groupType_ groupLink_ publicGroupId_ publicGroupAccess, simplexName = decodeSimplexName simplexNameRaw, groupPreferences, memberAdmission} -getGroupInfoByUserContactLinkConnReq :: DB.Connection -> VersionRangeChat -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo) -getGroupInfoByUserContactLinkConnReq db vr user@User {userId} (cReqSchema1, cReqSchema2) = do +getGroupInfoByUserContactLinkConnReq :: DB.Connection -> StoreCxt -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe GroupInfo) +getGroupInfoByUserContactLinkConnReq db cxt user@User {userId} (cReqSchema1, cReqSchema2) = do -- fmap join is to support group_id = NULL if non-group contact request is sent to this function (e.g., if client data is appended). groupId_ <- fmap join . maybeFirstRow fromOnly $ @@ -2552,12 +2552,12 @@ getGroupInfoByUserContactLinkConnReq db vr user@User {userId} (cReqSchema1, cReq WHERE user_id = ? AND conn_req_contact IN (?,?) |] (userId, cReqSchema1, cReqSchema2) - maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db vr user) groupId_ + maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db cxt user) groupId_ -getGroupInfoViaUserShortLink :: DB.Connection -> VersionRangeChat -> User -> ShortLinkContact -> IO (Maybe (ConnReqContact, GroupInfo)) -getGroupInfoViaUserShortLink db vr user@User {userId} shortLink = fmap eitherToMaybe $ runExceptT $ do +getGroupInfoViaUserShortLink :: DB.Connection -> StoreCxt -> User -> ShortLinkContact -> IO (Maybe (ConnReqContact, GroupInfo)) +getGroupInfoViaUserShortLink db cxt user@User {userId} shortLink = fmap eitherToMaybe $ runExceptT $ do (cReq, groupId) <- ExceptT getConnReqGroup - (cReq,) <$> getGroupInfo db vr user groupId + (cReq,) <$> getGroupInfo db cxt user groupId where getConnReqGroup = firstRow' toConnReqGroupId (SEInternalError "group link not found") $ @@ -2574,14 +2574,14 @@ getGroupInfoViaUserShortLink db vr user@User {userId} shortLink = fmap eitherToM (cReq, Just groupId) -> Right (cReq, groupId) _ -> Left $ SEInternalError "no conn req or group ID" -getGroupViaShortLinkToConnect :: DB.Connection -> VersionRangeChat -> User -> ShortLinkContact -> ExceptT StoreError IO (Maybe (ConnReqContact, GroupInfo)) -getGroupViaShortLinkToConnect db vr user@User {userId} shortLink = +getGroupViaShortLinkToConnect :: DB.Connection -> StoreCxt -> User -> ShortLinkContact -> ExceptT StoreError IO (Maybe (ConnReqContact, GroupInfo)) +getGroupViaShortLinkToConnect db cxt user@User {userId} shortLink = liftIO (maybeFirstRow id $ DB.query db "SELECT group_id, conn_full_link_to_connect FROM groups WHERE user_id = ? AND conn_short_link_to_connect = ?" (userId, shortLink)) >>= \case - Just (gId :: Int64, Just cReq) -> Just . (cReq,) <$> getGroupInfo db vr user gId + Just (gId :: Int64, Just cReq) -> Just . (cReq,) <$> getGroupInfo db cxt user gId _ -> pure Nothing -getGroupInfoByGroupLinkHash :: DB.Connection -> VersionRangeChat -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe GroupInfo) -getGroupInfoByGroupLinkHash db vr user@User {userId, userContactId} (groupLinkHash1, groupLinkHash2) = do +getGroupInfoByGroupLinkHash :: DB.Connection -> StoreCxt -> User -> (ConnReqUriHash, ConnReqUriHash) -> IO (Maybe GroupInfo) +getGroupInfoByGroupLinkHash db cxt user@User {userId, userContactId} (groupLinkHash1, groupLinkHash2) = do groupId_ <- maybeFirstRow fromOnly $ DB.query @@ -2595,7 +2595,7 @@ getGroupInfoByGroupLinkHash db vr user@User {userId, userContactId} (groupLinkHa LIMIT 1 |] (userId, groupLinkHash1, groupLinkHash2, userContactId, GSMemRemoved, GSMemLeft, GSMemGroupDeleted, GSMemUnknown) - maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db vr user) groupId_ + maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getGroupInfo db cxt user) groupId_ getGroupIdByName :: DB.Connection -> User -> GroupName -> ExceptT StoreError IO GroupId getGroupIdByName db User {userId} gName = @@ -2607,8 +2607,8 @@ getGroupMemberIdByName db User {userId} groupId groupMemberName = ExceptT . firstRow fromOnly (SEGroupMemberNameNotFound groupId groupMemberName) $ DB.query db "SELECT group_member_id FROM group_members WHERE user_id = ? AND group_id = ? AND local_display_name = ?" (userId, groupId, groupMemberName) -getActiveMembersByName :: DB.Connection -> VersionRangeChat -> User -> ContactName -> ExceptT StoreError IO [(GroupInfo, GroupMember)] -getActiveMembersByName db vr user@User {userId} groupMemberName = do +getActiveMembersByName :: DB.Connection -> StoreCxt -> User -> ContactName -> ExceptT StoreError IO [(GroupInfo, GroupMember)] +getActiveMembersByName db cxt user@User {userId} groupMemberName = do groupMemberIds :: [(GroupId, GroupMemberId)] <- liftIO $ DB.query @@ -2621,17 +2621,17 @@ getActiveMembersByName db vr user@User {userId} groupMemberName = do |] (userId, groupMemberName, GSMemConnected, GSMemComplete, GCUserMember) possibleMembers <- forM groupMemberIds $ \(groupId, groupMemberId) -> do - groupInfo <- getGroupInfo db vr user groupId - groupMember <- getGroupMember db vr user groupId groupMemberId + groupInfo <- getGroupInfo db cxt user groupId + groupMember <- getGroupMember db cxt user groupId groupMemberId pure (groupInfo, groupMember) pure $ sortOn (Down . ts . fst) possibleMembers where ts GroupInfo {chatTs, updatedAt} = fromMaybe updatedAt chatTs -getMatchingContacts :: DB.Connection -> VersionRangeChat -> User -> Contact -> IO [Contact] -getMatchingContacts db vr user@User {userId} Contact {contactId, profile = LocalProfile {displayName, fullName, shortDescr, image}} = do +getMatchingContacts :: DB.Connection -> StoreCxt -> User -> Contact -> IO [Contact] +getMatchingContacts db cxt user@User {userId} Contact {contactId, profile = LocalProfile {displayName, fullName, shortDescr, image}} = do contactIds <- map fromOnly <$> DB.query db q (userId, contactId, CSActive, displayName, fullName, shortDescr, image) - rights <$> mapM (runExceptT . getContact db vr user) contactIds + rights <$> mapM (runExceptT . getContact db cxt user) contactIds where -- this query is different from one in getMatchingMemberContacts -- it checks that it's not the same contact @@ -2646,10 +2646,10 @@ getMatchingContacts db vr user@User {userId} Contact {contactId, profile = Local AND p.short_descr IS NOT DISTINCT FROM ? AND p.image IS NOT DISTINCT FROM ? |] -getMatchingMembers :: DB.Connection -> VersionRangeChat -> User -> Contact -> IO [GroupMember] -getMatchingMembers db vr user@User {userId} Contact {profile = LocalProfile {displayName, fullName, shortDescr, image}} = do +getMatchingMembers :: DB.Connection -> StoreCxt -> User -> Contact -> IO [GroupMember] +getMatchingMembers db cxt user@User {userId} Contact {profile = LocalProfile {displayName, fullName, shortDescr, image}} = do memberIds <- map fromOnly <$> DB.query db q (userId, GCUserMember, displayName, fullName, shortDescr, image) - filter memberCurrent . rights <$> mapM (runExceptT . getGroupMemberById db vr user) memberIds + filter memberCurrent . rights <$> mapM (runExceptT . getGroupMemberById db cxt user) memberIds where -- only match with members without associated contact q = @@ -2663,11 +2663,11 @@ getMatchingMembers db vr user@User {userId} Contact {profile = LocalProfile {dis AND p.short_descr IS NOT DISTINCT FROM ? AND p.image IS NOT DISTINCT FROM ? |] -getMatchingMemberContacts :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> IO [Contact] +getMatchingMemberContacts :: DB.Connection -> StoreCxt -> User -> GroupMember -> IO [Contact] getMatchingMemberContacts _ _ _ GroupMember {memberContactId = Just _} = pure [] -getMatchingMemberContacts db vr user@User {userId} GroupMember {memberProfile = LocalProfile {displayName, fullName, shortDescr, image}} = do +getMatchingMemberContacts db cxt user@User {userId} GroupMember {memberProfile = LocalProfile {displayName, fullName, shortDescr, image}} = do contactIds <- map fromOnly <$> DB.query db q (userId, CSActive, displayName, fullName, shortDescr, image) - rights <$> mapM (runExceptT . getContact db vr user) contactIds + rights <$> mapM (runExceptT . getContact db cxt user) contactIds where q = [sql| @@ -2700,8 +2700,8 @@ createSentProbeHash db userId probeId to = do "INSERT INTO sent_probe_hashes (sent_probe_id, contact_id, group_member_id, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" (probeId, ctId, gmId, userId, currentTs, currentTs) -matchReceivedProbe :: DB.Connection -> VersionRangeChat -> User -> ContactOrMember -> Probe -> IO [ContactOrMember] -matchReceivedProbe db vr user@User {userId} from (Probe probe) = do +matchReceivedProbe :: DB.Connection -> StoreCxt -> User -> ContactOrMember -> Probe -> IO [ContactOrMember] +matchReceivedProbe db cxt user@User {userId} from (Probe probe) = do let probeHash = C.sha256Hash probe cgmIds <- DB.query @@ -2722,7 +2722,7 @@ matchReceivedProbe db vr user@User {userId} from (Probe probe) = do "INSERT INTO received_probes (contact_id, group_member_id, probe, probe_hash, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?)" (ctId, gmId, Binary probe, Binary probeHash, userId, currentTs, currentTs) let cgmIds' = filterFirstContactId cgmIds - catMaybes <$> mapM (getContactOrMember_ db vr user) cgmIds' + catMaybes <$> mapM (getContactOrMember_ db cxt user) cgmIds' where filterFirstContactId :: [(Maybe ContactId, Maybe GroupId, Maybe GroupMemberId)] -> [(Maybe ContactId, Maybe GroupId, Maybe GroupMemberId)] filterFirstContactId cgmIds = do @@ -2732,8 +2732,8 @@ matchReceivedProbe db vr user@User {userId} from (Probe probe) = do (x : _) -> [x] ctIds' <> memIds -matchReceivedProbeHash :: DB.Connection -> VersionRangeChat -> User -> ContactOrMember -> ProbeHash -> IO (Maybe (ContactOrMember, Probe)) -matchReceivedProbeHash db vr user@User {userId} from (ProbeHash probeHash) = do +matchReceivedProbeHash :: DB.Connection -> StoreCxt -> User -> ContactOrMember -> ProbeHash -> IO (Maybe (ContactOrMember, Probe)) +matchReceivedProbeHash db cxt user@User {userId} from (ProbeHash probeHash) = do probeIds <- maybeFirstRow id $ DB.query @@ -2753,11 +2753,11 @@ matchReceivedProbeHash db vr user@User {userId} from (ProbeHash probeHash) = do db "INSERT INTO received_probes (contact_id, group_member_id, probe_hash, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?)" (ctId, gmId, Binary probeHash, userId, currentTs, currentTs) - pure probeIds $>>= \(Only probe :. cgmIds) -> (,Probe probe) <$$> getContactOrMember_ db vr user cgmIds + pure probeIds $>>= \(Only probe :. cgmIds) -> (,Probe probe) <$$> getContactOrMember_ db cxt user cgmIds -matchSentProbe :: DB.Connection -> VersionRangeChat -> User -> ContactOrMember -> Probe -> IO (Maybe ContactOrMember) -matchSentProbe db vr user@User {userId} _from (Probe probe) = do - cgmIds $>>= getContactOrMember_ db vr user +matchSentProbe :: DB.Connection -> StoreCxt -> User -> ContactOrMember -> Probe -> IO (Maybe ContactOrMember) +matchSentProbe db cxt user@User {userId} _from (Probe probe) = do + cgmIds $>>= getContactOrMember_ db cxt user where (ctId, gmId) = contactOrMemberIds _from cgmIds = @@ -2776,11 +2776,11 @@ matchSentProbe db vr user@User {userId} _from (Probe probe) = do |] (userId, Binary probe, ctId, gmId) -getContactOrMember_ :: DB.Connection -> VersionRangeChat -> User -> (Maybe ContactId, Maybe GroupId, Maybe GroupMemberId) -> IO (Maybe ContactOrMember) -getContactOrMember_ db vr user ids = +getContactOrMember_ :: DB.Connection -> StoreCxt -> User -> (Maybe ContactId, Maybe GroupId, Maybe GroupMemberId) -> IO (Maybe ContactOrMember) +getContactOrMember_ db cxt user ids = fmap eitherToMaybe . runExceptT $ case ids of - (Just ctId, _, _) -> COMContact <$> getContact db vr user ctId - (_, Just gId, Just gmId) -> COMGroupMember <$> getGroupMember db vr user gId gmId + (Just ctId, _, _) -> COMContact <$> getContact db cxt user ctId + (_, Just gId, Just gmId) -> COMGroupMember <$> getGroupMember db cxt user gId gmId _ -> throwError $ SEInternalError "" associateMemberWithContactRecord :: DB.Connection -> User -> Contact -> GroupMember -> IO () @@ -2801,10 +2801,10 @@ associateMemberWithContactRecord when (memProfileId /= profileId) $ deleteUnusedProfile_ db userId memProfileId when (memLDN /= localDisplayName) $ deleteUnusedDisplayName_ db userId memLDN -associateContactWithMemberRecord :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> Contact -> ExceptT StoreError IO Contact +associateContactWithMemberRecord :: DB.Connection -> StoreCxt -> User -> GroupMember -> Contact -> ExceptT StoreError IO Contact associateContactWithMemberRecord db - vr + cxt user@User {userId} GroupMember {groupId, groupMemberId, localDisplayName = memLDN, memberProfile = LocalProfile {profileId = memProfileId}} Contact {contactId, localDisplayName, profile = LocalProfile {profileId}} = do @@ -2828,7 +2828,7 @@ associateContactWithMemberRecord (memLDN, memProfileId, currentTs, userId, contactId) when (profileId /= memProfileId) $ deleteUnusedProfile_ db userId profileId when (localDisplayName /= memLDN) $ deleteUnusedDisplayName_ db userId localDisplayName - getContact db vr user contactId + getContact db cxt user contactId deleteUnusedDisplayName_ :: DB.Connection -> UserId -> ContactName -> IO () deleteUnusedDisplayName_ db userId localDisplayName = @@ -2985,15 +2985,15 @@ createMemberContact mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, preparedContact = Nothing, contactRequestId = Nothing, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, groupDirectInv = Nothing, chatTags = [], chatItemTTL = Nothing, uiThemes = Nothing, chatDeleted = False, customData = Nothing, simplexName = Nothing, simplexNameVerifiedAt = Nothing} -getMemberContact :: DB.Connection -> VersionRangeChat -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation) -getMemberContact db vr user contactId = do - ct <- getContact db vr user contactId +getMemberContact :: DB.Connection -> StoreCxt -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation) +getMemberContact db cxt user contactId = do + ct <- getContact db cxt user contactId let Contact {contactGroupMemberId, activeConn} = ct case (activeConn, contactGroupMemberId) of (Just Connection {connId}, Just groupMemberId) -> do cReq <- getConnReqInv db connId - m@GroupMember {groupId} <- getGroupMemberById db vr user groupMemberId - g <- getGroupInfo db vr user groupId + m@GroupMember {groupId} <- getGroupMemberById db cxt user groupMemberId + g <- getGroupInfo db cxt user groupId pure (g, m, ct, cReq) _ -> throwError $ SEMemberContactGroupMemberNotFound contactId @@ -3102,13 +3102,13 @@ createMemberContactConn forM_ cmdId_ $ \cmdId -> setCommandConnId db user cmdId connId pure connId -getMemberContactInvited :: DB.Connection -> VersionRangeChat -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, Connection, Contact, GroupDirectInvitation) -getMemberContactInvited db vr user contactId = do - ct@Contact {groupDirectInv = groupDirectInv_} <- getContact db vr user contactId +getMemberContactInvited :: DB.Connection -> StoreCxt -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, Connection, Contact, GroupDirectInvitation) +getMemberContactInvited db cxt user contactId = do + ct@Contact {groupDirectInv = groupDirectInv_} <- getContact db cxt user contactId case groupDirectInv_ of Just groupDirectInv@GroupDirectInvitation {fromGroupId_ = Just groupId, fromGroupMemberId_ = Just _gmId, fromGroupMemberConnId_ = Just mConnId} -> do - g <- getGroupInfo db vr user groupId - mConn <- getConnectionById db vr user mConnId + g <- getGroupInfo db cxt user groupId + mConn <- getConnectionById db cxt user mConnId pure (g, mConn, ct, groupDirectInv) _ -> throwError $ SEMemberContactGroupMemberNotFound contactId @@ -3190,8 +3190,8 @@ setXGrpLinkMemReceived db mId xGrpLinkMemReceived = do "UPDATE group_members SET xgrplinkmem_received = ?, updated_at = ? WHERE group_member_id = ?" (BI xGrpLinkMemReceived, currentTs, mId) -createNewUnknownGroupMember :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> MemberId -> Text -> GroupMemberRole -> ExceptT StoreError IO GroupMember -createNewUnknownGroupMember db vr user@User {userId, userContactId} GroupInfo {groupId} memberId memberName unknownMemberRole = do +createNewUnknownGroupMember :: DB.Connection -> StoreCxt -> User -> GroupInfo -> MemberId -> Text -> GroupMemberRole -> ExceptT StoreError IO GroupMember +createNewUnknownGroupMember db cxt user@User {userId, userContactId} GroupInfo {groupId} memberId memberName unknownMemberRole = do currentTs <- liftIO getCurrentTime let memberProfile = profileFromName memberName (localDisplayName, profileId) <- createNewMemberProfile_ db user memberProfile currentTs @@ -3211,12 +3211,12 @@ createNewUnknownGroupMember db vr user@User {userId, userContactId} GroupInfo {g :. (minV, maxV) ) groupMemberId <- liftIO $ insertedRowId db - getGroupMemberById db vr user groupMemberId + getGroupMemberById db cxt user groupMemberId where - VersionRange minV maxV = vr + VersionRange minV maxV = vr cxt -createLinkOwnerMember :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> Maybe ContactId -> MemberId -> C.PublicKeyEd25519 -> ExceptT StoreError IO GroupMember -createLinkOwnerMember db vr user@User {userId, userContactId} GroupInfo {groupId} contactId_ memberId ownerKey = do +createLinkOwnerMember :: DB.Connection -> StoreCxt -> User -> GroupInfo -> Maybe ContactId -> MemberId -> C.PublicKeyEd25519 -> ExceptT StoreError IO GroupMember +createLinkOwnerMember db cxt user@User {userId, userContactId} GroupInfo {groupId} contactId_ memberId ownerKey = do currentTs <- liftIO getCurrentTime let memberProfile = profileFromName $ nameFromMemberId memberId (localDisplayName, profileId) <- createNewMemberProfile_ db user memberProfile currentTs @@ -3236,15 +3236,15 @@ createLinkOwnerMember db vr user@User {userId, userContactId} GroupInfo {groupId :. (minV, maxV) ) groupMemberId <- liftIO $ insertedRowId db - getGroupMemberById db vr user groupMemberId + getGroupMemberById db cxt user groupMemberId where - VersionRange minV maxV = vr + VersionRange minV maxV = vr cxt -- member_pub_key is not updated here — introduced members are owners -- whose keys are loaded from link data (trusted out-of-band). -- Updating from an in-band message would allow a compromised relay to substitute keys. -updatePreparedChannelMember :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> MemberInfo -> ExceptT StoreError IO GroupMember -updatePreparedChannelMember db vr user@User {userId} member@GroupMember {groupMemberId, memberChatVRange} MemberInfo {memberRole, v, profile} = do +updatePreparedChannelMember :: DB.Connection -> StoreCxt -> User -> GroupMember -> MemberInfo -> ExceptT StoreError IO GroupMember +updatePreparedChannelMember db cxt user@User {userId} member@GroupMember {groupMemberId, memberChatVRange} MemberInfo {memberRole, v, profile} = do _ <- updateMemberProfile db user member profile currentTs <- liftIO getCurrentTime liftIO $ @@ -3260,12 +3260,12 @@ updatePreparedChannelMember db vr user@User {userId} member@GroupMember {groupMe WHERE user_id = ? AND group_member_id = ? |] (memberRole, GSMemIntroduced, minV, maxV, currentTs, userId, groupMemberId) - getGroupMemberById db vr user groupMemberId + getGroupMemberById db cxt user groupMemberId where VersionRange minV maxV = maybe memberChatVRange fromChatVRange v -updateUnknownMemberAnnounced :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> GroupMember -> MemberInfo -> GroupMemberStatus -> ExceptT StoreError IO GroupMember -updateUnknownMemberAnnounced db vr user@User {userId} invitingMember unknownMember@GroupMember {groupMemberId, memberChatVRange} MemberInfo {memberRole, v, profile, memberKey} status = do +updateUnknownMemberAnnounced :: DB.Connection -> StoreCxt -> User -> GroupMember -> GroupMember -> MemberInfo -> GroupMemberStatus -> ExceptT StoreError IO GroupMember +updateUnknownMemberAnnounced db cxt user@User {userId} invitingMember unknownMember@GroupMember {groupMemberId, memberChatVRange} MemberInfo {memberRole, v, profile, memberKey} status = do _ <- updateMemberProfile db user unknownMember profile currentTs <- liftIO getCurrentTime liftIO $ @@ -3286,7 +3286,7 @@ updateUnknownMemberAnnounced db vr user@User {userId} invitingMember unknownMemb ( (memberRole, GCPostMember, status, groupMemberId' invitingMember) :. (minV, maxV, memberPubKey_, currentTs, userId, groupMemberId) ) - getGroupMemberById db vr user groupMemberId + getGroupMemberById db cxt user groupMemberId where VersionRange minV maxV = maybe memberChatVRange fromChatVRange v memberPubKey_ = (\(MemberKey k) -> k) <$> memberKey diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 20657a96a7..ba20f1164c 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -406,8 +406,8 @@ data MemberAttention | MAReset deriving (Show) -updateChatTsStats :: DB.Connection -> VersionRangeChat -> User -> ChatDirection c d -> UTCTime -> Maybe (Int, MemberAttention, Int) -> IO (ChatInfo c) -updateChatTsStats db vr user@User {userId} chatDirection chatTs chatStats_ = case toChatInfo chatDirection of +updateChatTsStats :: DB.Connection -> StoreCxt -> User -> ChatDirection c d -> UTCTime -> Maybe (Int, MemberAttention, Int) -> IO (ChatInfo c) +updateChatTsStats db cxt user@User {userId} chatDirection chatTs chatStats_ = case toChatInfo chatDirection of DirectChat ct@Contact {contactId} -> do DB.execute db @@ -516,7 +516,7 @@ updateChatTsStats db vr user@User {userId} chatDirection chatTs chatStats_ = cas WHERE group_member_id = ? |] (chatTs, unread, mentions, groupMemberId) - m_ <- runExceptT $ getGroupMemberById db vr user groupMemberId + m_ <- runExceptT $ getGroupMemberById db cxt user groupMemberId pure $ either (const m) id m_ -- Left shouldn't happen, but types require it LocalChat nf@NoteFolder {noteFolderId} -> do DB.execute @@ -530,8 +530,8 @@ setSupportChatTs :: DB.Connection -> GroupMemberId -> UTCTime -> IO () setSupportChatTs db groupMemberId chatTs = DB.execute db "UPDATE group_members SET support_chat_ts = ? WHERE group_member_id = ?" (chatTs, groupMemberId) -setSupportChatMemberAttention :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupMember -> Int64 -> IO (GroupInfo, GroupMember) -setSupportChatMemberAttention db vr user g m memberAttention = do +setSupportChatMemberAttention :: DB.Connection -> StoreCxt -> User -> GroupInfo -> GroupMember -> Int64 -> IO (GroupInfo, GroupMember) +setSupportChatMemberAttention db cxt user g m memberAttention = do m' <- updateGMAttention g' <- updateGroupMembersRequireAttention db user g m m' pure (g', m') @@ -542,7 +542,7 @@ setSupportChatMemberAttention db vr user g m memberAttention = do db "UPDATE group_members SET support_chat_items_member_attention = ?, updated_at = ? WHERE group_member_id = ?" (memberAttention, currentTs, groupMemberId' m) - m_ <- runExceptT $ getGroupMemberById db vr user (groupMemberId' m) + m_ <- runExceptT $ getGroupMemberById db cxt user (groupMemberId' m) pure $ either (const m) id m_ -- Left shouldn't happen, but types require it createNewSndChatItem :: DB.Connection -> User -> ChatDirection c 'MDSnd -> ShowGroupAsSender -> SndMessage -> CIContent 'MDSnd -> Maybe (CIQuote c) -> Maybe CIForwardedFrom -> Maybe CITimed -> Bool -> Bool -> UTCTime -> IO ChatItemId @@ -733,8 +733,8 @@ getChatItemQuote_ db User {userId, userContactId} chatDirection QuotedMsg {msgRe ciQuoteGroup [] = ciQuote Nothing $ CIQGroupRcv Nothing ciQuoteGroup ((Only itemId :. memberRow) : _) = ciQuote itemId . CIQGroupRcv . Just $ toGroupMember userContactId memberRow -getChatPreviews :: DB.Connection -> VersionRangeChat -> User -> Bool -> PaginationByTime -> ChatListQuery -> IO [Either StoreError AChat] -getChatPreviews db vr user withPCC pagination query = do +getChatPreviews :: DB.Connection -> StoreCxt -> User -> Bool -> PaginationByTime -> ChatListQuery -> IO [Either StoreError AChat] +getChatPreviews db cxt user withPCC pagination query = do directChats <- findDirectChatPreviews_ db user pagination query groupChats <- findGroupChatPreviews_ db user pagination query localChats <- findLocalChatPreviews_ db user pagination query @@ -756,8 +756,8 @@ getChatPreviews db vr user withPCC pagination query = do PTBefore _ count -> take count . sortBy (comparing $ Down . ts) getChatPreview :: AChatPreviewData -> ExceptT StoreError IO AChat getChatPreview (ACPD cType cpd) = case cType of - SCTDirect -> getDirectChatPreview_ db vr user cpd - SCTGroup -> getGroupChatPreview_ db vr user cpd + SCTDirect -> getDirectChatPreview_ db cxt user cpd + SCTGroup -> getGroupChatPreview_ db cxt user cpd SCTLocal -> getLocalChatPreview_ db user cpd SCTContactRequest -> let (ContactRequestPD _ chat) = cpd in pure chat SCTContactConnection -> let (ContactConnectionPD _ chat) = cpd in pure chat @@ -874,9 +874,9 @@ findDirectChatPreviews_ db User {userId} pagination clq = PTAfter ts count -> DB.query db (query <> " AND ct.chat_ts > ? ORDER BY ct.chat_ts ASC LIMIT ?") (params :. (ts, count)) PTBefore ts count -> DB.query db (query <> " AND ct.chat_ts < ? ORDER BY ct.chat_ts DESC LIMIT ?") (params :. (ts, count)) -getDirectChatPreview_ :: DB.Connection -> VersionRangeChat -> User -> ChatPreviewData 'CTDirect -> ExceptT StoreError IO AChat -getDirectChatPreview_ db vr user (DirectChatPD _ contactId lastItemId_ stats) = do - contact <- getContact db vr user contactId +getDirectChatPreview_ :: DB.Connection -> StoreCxt -> User -> ChatPreviewData 'CTDirect -> ExceptT StoreError IO AChat +getDirectChatPreview_ db cxt user (DirectChatPD _ contactId lastItemId_ stats) = do + contact <- getContact db cxt user contactId ts <- liftIO getCurrentTime lastItem <- case lastItemId_ of Just lastItemId -> do @@ -985,9 +985,9 @@ findGroupChatPreviews_ db User {userId} pagination clq = PTAfter ts count -> DB.query db (query <> " AND g.chat_ts > ? ORDER BY g.chat_ts ASC LIMIT ?") (params :. (ts, count)) PTBefore ts count -> DB.query db (query <> " AND g.chat_ts < ? ORDER BY g.chat_ts DESC LIMIT ?") (params :. (ts, count)) -getGroupChatPreview_ :: DB.Connection -> VersionRangeChat -> User -> ChatPreviewData 'CTGroup -> ExceptT StoreError IO AChat -getGroupChatPreview_ db vr user (GroupChatPD _ groupId lastItemId_ stats) = do - groupInfo <- getGroupInfo db vr user groupId +getGroupChatPreview_ :: DB.Connection -> StoreCxt -> User -> ChatPreviewData 'CTGroup -> ExceptT StoreError IO AChat +getGroupChatPreview_ db cxt user (GroupChatPD _ groupId lastItemId_ stats) = do + groupInfo <- getGroupInfo db cxt user groupId ts <- liftIO getCurrentTime lastItem <- case lastItemId_ of Just lastItemId -> do @@ -1223,10 +1223,10 @@ getChatContentTypes db User {userId} (ChatRef cType chatId chatScope_) = case cT ("SELECT DISTINCT msg_content_tag FROM chat_items WHERE user_id = ? AND " <> cond <> " AND msg_content_tag IS NOT NULL ORDER BY msg_content_tag") ((userId, chatId) :. params) -getDirectChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> Maybe MsgContentTag -> ChatPagination -> Maybe Text -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo) -getDirectChat db vr user contactId contentFilter pagination search_ = do +getDirectChat :: DB.Connection -> StoreCxt -> User -> Int64 -> Maybe MsgContentTag -> ChatPagination -> Maybe Text -> ExceptT StoreError IO (Chat 'CTDirect, Maybe NavigationInfo) +getDirectChat db cxt user contactId contentFilter pagination search_ = do let search = fromMaybe "" search_ - ct <- getContact db vr user contactId + ct <- getContact db cxt user contactId case pagination of CPLast count -> (,Nothing) <$> getDirectChatLast_ db user ct contentFilter count search CPAfter afterId count -> (,Nothing) <$> getDirectChatAfter_ db user ct contentFilter afterId count search @@ -1443,11 +1443,11 @@ getContactNavInfo_ db User {userId} Contact {contactId} afterCI = do :. (userId, contactId, ciCreatedAt afterCI, cChatItemId afterCI) ) -getGroupChat :: DB.Connection -> VersionRangeChat -> User -> Int64 -> Maybe GroupChatScope -> Maybe MsgContentTag -> ChatPagination -> Maybe Text -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) -getGroupChat db vr user groupId scope_ contentFilter pagination search_ = do +getGroupChat :: DB.Connection -> StoreCxt -> User -> Int64 -> Maybe GroupChatScope -> Maybe MsgContentTag -> ChatPagination -> Maybe Text -> ExceptT StoreError IO (Chat 'CTGroup, Maybe NavigationInfo) +getGroupChat db cxt user groupId scope_ contentFilter pagination search_ = do let search = fromMaybe "" search_ - g <- getGroupInfo db vr user groupId - scopeInfo <- mapM (getCreateGroupChatScopeInfo db vr user g) scope_ + g <- getGroupInfo db cxt user groupId + scopeInfo <- mapM (getCreateGroupChatScopeInfo db cxt user g) scope_ case pagination of CPLast count -> (,Nothing) <$> getGroupChatLast_ db user g scopeInfo contentFilter count search emptyChatStats CPAfter afterId count -> (,Nothing) <$> getGroupChatAfter_ db user g scopeInfo contentFilter afterId count search @@ -1457,31 +1457,31 @@ getGroupChat db vr user groupId scope_ contentFilter pagination search_ = do unless (T.null search) $ throwError $ SEInternalError "initial chat pagination doesn't support search" getGroupChatInitial_ db user g scopeInfo contentFilter count -getCreateGroupChatScopeInfo :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupChatScope -> ExceptT StoreError IO GroupChatScopeInfo -getCreateGroupChatScopeInfo db vr user GroupInfo {membership} = \case +getCreateGroupChatScopeInfo :: DB.Connection -> StoreCxt -> User -> GroupInfo -> GroupChatScope -> ExceptT StoreError IO GroupChatScopeInfo +getCreateGroupChatScopeInfo db cxt user GroupInfo {membership} = \case GCSMemberSupport Nothing -> do when (isNothing $ supportChat membership) $ do ts <- liftIO getCurrentTime liftIO $ setSupportChatTs db (groupMemberId' membership) ts pure $ GCSIMemberSupport {groupMember_ = Nothing} GCSMemberSupport (Just gmId) -> do - m <- getGroupMemberById db vr user gmId + m <- getGroupMemberById db cxt user gmId when (isNothing $ supportChat m) $ do ts <- liftIO getCurrentTime liftIO $ setSupportChatTs db gmId ts pure GCSIMemberSupport {groupMember_ = Just m} -getGroupChatScopeInfoForItem :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> ChatItemId -> ExceptT StoreError IO (Maybe GroupChatScopeInfo) -getGroupChatScopeInfoForItem db vr user g itemId = - getGroupChatScopeForItem_ db itemId >>= mapM (getGroupChatScopeInfo db vr user g) +getGroupChatScopeInfoForItem :: DB.Connection -> StoreCxt -> User -> GroupInfo -> ChatItemId -> ExceptT StoreError IO (Maybe GroupChatScopeInfo) +getGroupChatScopeInfoForItem db cxt user g itemId = + getGroupChatScopeForItem_ db itemId >>= mapM (getGroupChatScopeInfo db cxt user g) -getGroupChatScopeInfo :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupChatScope -> ExceptT StoreError IO GroupChatScopeInfo -getGroupChatScopeInfo db vr user GroupInfo {membership} = \case +getGroupChatScopeInfo :: DB.Connection -> StoreCxt -> User -> GroupInfo -> GroupChatScope -> ExceptT StoreError IO GroupChatScopeInfo +getGroupChatScopeInfo db cxt user GroupInfo {membership} = \case GCSMemberSupport Nothing -> case supportChat membership of Nothing -> throwError $ SEInternalError "no moderators support chat" Just _supportChat -> pure $ GCSIMemberSupport {groupMember_ = Nothing} GCSMemberSupport (Just gmId) -> do - m <- getGroupMemberById db vr user gmId + m <- getGroupMemberById db cxt user gmId case supportChat m of Nothing -> throwError $ SEInternalError "no support chat" Just _supportChat -> pure GCSIMemberSupport {groupMember_ = Just m} @@ -2087,8 +2087,8 @@ updateGroupChatItemsRead db User {userId} GroupInfo {groupId} = do |] (CISRcvRead, currentTs, userId, groupId, CISRcvNew) -updateSupportChatItemsRead :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupChatScopeInfo -> IO (GroupInfo, GroupMember) -updateSupportChatItemsRead db vr user@User {userId} g@GroupInfo {groupId, membership} scopeInfo = do +updateSupportChatItemsRead :: DB.Connection -> StoreCxt -> User -> GroupInfo -> GroupChatScopeInfo -> IO (GroupInfo, GroupMember) +updateSupportChatItemsRead db cxt user@User {userId} g@GroupInfo {groupId, membership} scopeInfo = do currentTs <- getCurrentTime case scopeInfo of GCSIMemberSupport {groupMember_} -> do @@ -2126,7 +2126,7 @@ updateSupportChatItemsRead db vr user@User {userId} g@GroupInfo {groupId, member WHERE group_member_id = ? |] (currentTs, groupMemberId) - m_ <- runExceptT $ getGroupMemberById db vr user groupMemberId + m_ <- runExceptT $ getGroupMemberById db cxt user groupMemberId pure $ either (const m) id m_ -- Left shouldn't happen, but types require it getGroupUnreadTimedItems :: DB.Connection -> User -> GroupId -> Maybe GroupChatScope -> IO [(ChatItemId, Int)] @@ -2154,8 +2154,8 @@ getGroupUnreadTimedItems db User {userId} groupId scope = |] (userId, groupId, GCSTMemberSupport_, groupMemberId_, CISRcvNew) -updateGroupChatItemsReadList :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> NonEmpty ChatItemId -> ExceptT StoreError IO ([(ChatItemId, Int)], GroupInfo) -updateGroupChatItemsReadList db vr user@User {userId} g@GroupInfo {groupId} scopeInfo_ itemIds = do +updateGroupChatItemsReadList :: DB.Connection -> StoreCxt -> User -> GroupInfo -> Maybe GroupChatScopeInfo -> NonEmpty ChatItemId -> ExceptT StoreError IO ([(ChatItemId, Int)], GroupInfo) +updateGroupChatItemsReadList db cxt user@User {userId} g@GroupInfo {groupId} scopeInfo_ itemIds = do currentTs <- liftIO getCurrentTime -- Possible improvement is to differentiate retrieval queries for each scope, -- but we rely on UI to not pass item IDs from incorrect scope. @@ -2164,7 +2164,7 @@ updateGroupChatItemsReadList db vr user@User {userId} g@GroupInfo {groupId} scop Nothing -> pure g Just scopeInfo@GCSIMemberSupport {groupMember_} -> do let decStats = countReadItems groupMember_ readItemsData - liftIO $ updateGroupScopeUnreadStats db vr user g scopeInfo decStats + liftIO $ updateGroupScopeUnreadStats db cxt user g scopeInfo decStats pure (timedItems readItemsData, g') where getUpdateGroupItem :: UTCTime -> ChatItemId -> IO (Maybe (ChatItemId, Maybe Int, Maybe UTCTime, Maybe GroupMemberId, Maybe BoolInt)) @@ -2199,8 +2199,8 @@ updateGroupChatItemsReadList db vr user@User {userId} g@GroupInfo {groupId} scop addTimedItem acc (itemId, Just ttl, Nothing, _, _) = (itemId, ttl) : acc addTimedItem acc _ = acc -updateGroupScopeUnreadStats :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> GroupChatScopeInfo -> (Int, Int, Int) -> IO GroupInfo -updateGroupScopeUnreadStats db vr user g@GroupInfo {membership} scopeInfo (unread, unanswered, mentions) = +updateGroupScopeUnreadStats :: DB.Connection -> StoreCxt -> User -> GroupInfo -> GroupChatScopeInfo -> (Int, Int, Int) -> IO GroupInfo +updateGroupScopeUnreadStats db cxt user g@GroupInfo {membership} scopeInfo (unread, unanswered, mentions) = case scopeInfo of GCSIMemberSupport {groupMember_} -> case groupMember_ of Nothing -> do @@ -2238,7 +2238,7 @@ updateGroupScopeUnreadStats db vr user g@GroupInfo {membership} scopeInfo (unrea |] #endif (unread, unanswered, mentions, currentTs, groupMemberId) - m_ <- runExceptT $ getGroupMemberById db vr user groupMemberId + m_ <- runExceptT $ getGroupMemberById db cxt user groupMemberId pure $ either (const m) id m_ -- Left shouldn't happen, but types require it setGroupChatItemsDeleteAt :: DB.Connection -> User -> GroupId -> [(ChatItemId, Int)] -> UTCTime -> IO [(ChatItemId, UTCTime)] @@ -2413,8 +2413,8 @@ toGroupChatItem ciTimed :: Maybe CITimed ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt} -getAllChatItems :: DB.Connection -> VersionRangeChat -> User -> ChatPagination -> Maybe Text -> ExceptT StoreError IO [AChatItem] -getAllChatItems db vr user@User {userId} pagination search_ = do +getAllChatItems :: DB.Connection -> StoreCxt -> User -> ChatPagination -> Maybe Text -> ExceptT StoreError IO [AChatItem] +getAllChatItems db cxt user@User {userId} pagination search_ = do itemRefs <- rights . map toChatItemRef <$> case pagination of CPLast count -> liftIO $ getAllChatItemsLast_ count @@ -2426,12 +2426,12 @@ getAllChatItems db vr user@User {userId} pagination search_ = do liftIO getFirstUnreadItemId_ >>= \case Just itemId -> liftIO . getAllChatItemsAround_ itemId count . aChatItemTs =<< getAChatItem_ itemId Nothing -> liftIO $ getAllChatItemsLast_ count - mapM (uncurry (getAChatItem db vr user)) itemRefs + mapM (uncurry (getAChatItem db cxt user)) itemRefs where search = fromMaybe "" search_ getAChatItem_ itemId = do chatRef <- getChatRefViaItemId db user itemId - getAChatItem db vr user chatRef itemId + getAChatItem db cxt user chatRef itemId getAllChatItemsLast_ count = reverse <$> DB.query @@ -3239,8 +3239,8 @@ deleteLocalChatItem db User {userId} NoteFolder {noteFolderId} ci = do |] (userId, noteFolderId, itemId) -getChatItemByFileId :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO AChatItem -getChatItemByFileId db vr user@User {userId} fileId = do +getChatItemByFileId :: DB.Connection -> StoreCxt -> User -> Int64 -> ExceptT StoreError IO AChatItem +getChatItemByFileId db cxt user@User {userId} fileId = do (chatRef, itemId) <- ExceptT . firstRow' toChatItemRef (SEChatItemNotFoundByFileId fileId) $ DB.query @@ -3253,16 +3253,16 @@ getChatItemByFileId db vr user@User {userId} fileId = do LIMIT 1 |] (userId, fileId) - getAChatItem db vr user chatRef itemId + getAChatItem db cxt user chatRef itemId -lookupChatItemByFileId :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO (Maybe AChatItem) -lookupChatItemByFileId db vr user fileId = do - fmap Just (getChatItemByFileId db vr user fileId) `catchError` \case +lookupChatItemByFileId :: DB.Connection -> StoreCxt -> User -> Int64 -> ExceptT StoreError IO (Maybe AChatItem) +lookupChatItemByFileId db cxt user fileId = do + fmap Just (getChatItemByFileId db cxt user fileId) `catchError` \case SEChatItemNotFoundByFileId {} -> pure Nothing e -> throwError e -getChatItemByGroupId :: DB.Connection -> VersionRangeChat -> User -> GroupId -> ExceptT StoreError IO AChatItem -getChatItemByGroupId db vr user@User {userId} groupId = do +getChatItemByGroupId :: DB.Connection -> StoreCxt -> User -> GroupId -> ExceptT StoreError IO AChatItem +getChatItemByGroupId db cxt user@User {userId} groupId = do (chatRef, itemId) <- ExceptT . firstRow' toChatItemRef (SEChatItemNotFoundByGroupId groupId) $ DB.query @@ -3275,7 +3275,7 @@ getChatItemByGroupId db vr user@User {userId} groupId = do LIMIT 1 |] (userId, groupId) - getAChatItem db vr user chatRef itemId + getAChatItem db cxt user chatRef itemId getChatRefViaItemId :: DB.Connection -> User -> ChatItemId -> ExceptT StoreError IO ChatRef getChatRefViaItemId db User {userId} itemId = do @@ -3288,17 +3288,17 @@ getChatRefViaItemId db User {userId} itemId = do (Nothing, Just groupId) -> Right $ ChatRef CTGroup groupId Nothing (_, _) -> Left $ SEBadChatItem itemId Nothing -getAChatItem :: DB.Connection -> VersionRangeChat -> User -> ChatRef -> ChatItemId -> ExceptT StoreError IO AChatItem -getAChatItem db vr user (ChatRef cType chatId scope) itemId = do +getAChatItem :: DB.Connection -> StoreCxt -> User -> ChatRef -> ChatItemId -> ExceptT StoreError IO AChatItem +getAChatItem db cxt user (ChatRef cType chatId scope) itemId = do aci <- case cType of CTDirect -> do - ct <- getContact db vr user chatId + ct <- getContact db cxt user chatId (CChatItem msgDir ci) <- getDirectChatItem db user chatId itemId pure $ AChatItem SCTDirect msgDir (DirectChat ct) ci CTGroup -> do - gInfo <- getGroupInfo db vr user chatId + gInfo <- getGroupInfo db cxt user chatId (CChatItem msgDir ci) <- getGroupChatItem db user chatId itemId - scopeInfo <- mapM (getGroupChatScopeInfo db vr user gInfo) scope + scopeInfo <- mapM (getGroupChatScopeInfo db cxt user gInfo) scope pure $ AChatItem SCTGroup msgDir (GroupChat gInfo scopeInfo) ci CTLocal -> do nf <- getNoteFolder db user chatId @@ -3474,8 +3474,8 @@ setGroupReaction db GroupInfo {groupId} m itemMemberId itemSharedMId sent reacti |] (groupId, groupMemberId' m, itemSharedMId, itemMemberId, BI sent, reaction) -getReactionMembers :: DB.Connection -> VersionRangeChat -> User -> GroupId -> SharedMsgId -> MsgReaction -> IO [MemberReaction] -getReactionMembers db vr user groupId itemSharedMId reaction = do +getReactionMembers :: DB.Connection -> StoreCxt -> User -> GroupId -> SharedMsgId -> MsgReaction -> IO [MemberReaction] +getReactionMembers db cxt user groupId itemSharedMId reaction = do reactions <- DB.query db @@ -3489,7 +3489,7 @@ getReactionMembers db vr user groupId itemSharedMId reaction = do where toMemberReaction :: (GroupMemberId, UTCTime) -> ExceptT StoreError IO MemberReaction toMemberReaction (groupMemberId, reactionTs) = do - groupMember <- getGroupMemberById db vr user groupMemberId + groupMember <- getGroupMemberById db cxt user groupMemberId pure MemberReaction {groupMember, reactionTs} getTimedItems :: DB.Connection -> User -> UTCTime -> IO [((ChatRef, ChatItemId), UTCTime)] @@ -3587,9 +3587,9 @@ createCIModeration db GroupInfo {groupId} moderatorMember itemMemberId itemShare |] (groupId, groupMemberId' moderatorMember, itemMemberId, itemSharedMId, msgId, moderatedAtTs) -getCIModeration :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> MemberId -> Maybe SharedMsgId -> IO (Maybe CIModeration) +getCIModeration :: DB.Connection -> StoreCxt -> User -> GroupInfo -> MemberId -> Maybe SharedMsgId -> IO (Maybe CIModeration) getCIModeration _ _ _ _ _ Nothing = pure Nothing -getCIModeration db vr user GroupInfo {groupId} itemMemberId (Just sharedMsgId) = do +getCIModeration db cxt user GroupInfo {groupId} itemMemberId (Just sharedMsgId) = do r_ <- maybeFirstRow id $ DB.query @@ -3603,7 +3603,7 @@ getCIModeration db vr user GroupInfo {groupId} itemMemberId (Just sharedMsgId) = (groupId, itemMemberId, sharedMsgId) case r_ of Just (moderationId, moderatorId, createdByMsgId, moderatedAt) -> do - runExceptT (getGroupMember db vr user groupId moderatorId) >>= \case + runExceptT (getGroupMember db cxt user groupId moderatorId) >>= \case Right moderatorMember -> pure (Just CIModeration {moderationId, moderatorMember, createdByMsgId, moderatedAt}) _ -> pure Nothing _ -> pure Nothing diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 3ba25e700b..749f87b059 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -394,9 +394,9 @@ createUserContactLink db User {userId} agentConnId (CCLink cReq shortLink) subMo userContactLinkId <- insertedRowId db void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId ConnNew initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff Nothing -getUserAddressConnection :: DB.Connection -> VersionRangeChat -> User -> ExceptT StoreError IO Connection -getUserAddressConnection db vr User {userId} = do - ExceptT . firstRow (toConnection vr) SEUserContactLinkNotFound $ +getUserAddressConnection :: DB.Connection -> StoreCxt -> User -> ExceptT StoreError IO Connection +getUserAddressConnection db cxt User {userId} = do + ExceptT . firstRow (toConnection cxt) SEUserContactLinkNotFound $ DB.query db [sql| @@ -539,8 +539,8 @@ setUserContactLinkShortLink db userContactLinkId shortLink = |] (shortLink, BI True, BI True, BI False, userContactLinkId) -getContactWithoutConnViaAddress :: DB.Connection -> VersionRangeChat -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe Contact) -getContactWithoutConnViaAddress db vr user@User {userId} (cReqSchema1, cReqSchema2) = do +getContactWithoutConnViaAddress :: DB.Connection -> StoreCxt -> User -> (ConnReqContact, ConnReqContact) -> IO (Maybe Contact) +getContactWithoutConnViaAddress db cxt user@User {userId} (cReqSchema1, cReqSchema2) = do ctId_ <- maybeFirstRow fromOnly $ DB.query @@ -553,10 +553,10 @@ getContactWithoutConnViaAddress db vr user@User {userId} (cReqSchema1, cReqSchem WHERE cp.user_id = ? AND cp.contact_link IN (?,?) AND c.connection_id IS NULL |] (userId, cReqSchema1, cReqSchema2) - maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getContact db vr user) ctId_ + maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getContact db cxt user) ctId_ -getContactWithoutConnViaShortAddress :: DB.Connection -> VersionRangeChat -> User -> ShortLinkContact -> IO (Maybe Contact) -getContactWithoutConnViaShortAddress db vr user@User {userId} shortLink = do +getContactWithoutConnViaShortAddress :: DB.Connection -> StoreCxt -> User -> ShortLinkContact -> IO (Maybe Contact) +getContactWithoutConnViaShortAddress db cxt user@User {userId} shortLink = do ctId_ <- maybeFirstRow fromOnly $ DB.query @@ -569,7 +569,7 @@ getContactWithoutConnViaShortAddress db vr user@User {userId} shortLink = do WHERE cp.user_id = ? AND cp.contact_link = ? AND c.connection_id IS NULL |] (userId, shortLink) - maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getContact db vr user) ctId_ + maybe (pure Nothing) (fmap eitherToMaybe . runExceptT . getContact db cxt user) ctId_ updateUserAddressSettings :: DB.Connection -> Int64 -> AddressSettings -> IO () updateUserAddressSettings db userContactLinkId AddressSettings {businessAddress, autoAccept, autoReply} = diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index ae3f3bc0d2..cbfb80f741 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -236,12 +236,12 @@ type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Ma decodeSimplexName :: Maybe Text -> Maybe SimplexNameInfo decodeSimplexName = (>>= eitherToMaybe . strDecode . encodeUtf8) -toConnection :: VersionRangeChat -> ConnectionRow -> Connection -toConnection vr ((connId, acId, connLevel, viaContact, viaUserContactLink, BI viaGroupLink, groupLinkId, xContactId) :. (customUserProfileId, connStatus, connType, BI contactConnInitiated, localAlias) :. (contactId, groupMemberId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter, quotaErrCounter, chatV, minVer, maxVer) :. Only simplexNameRaw) = +toConnection :: StoreCxt -> ConnectionRow -> Connection +toConnection cxt ((connId, acId, connLevel, viaContact, viaUserContactLink, BI viaGroupLink, groupLinkId, xContactId) :. (customUserProfileId, connStatus, connType, BI contactConnInitiated, localAlias) :. (contactId, groupMemberId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter, quotaErrCounter, chatV, minVer, maxVer) :. Only simplexNameRaw) = Connection { connId, agentConnId = AgentConnId acId, - connChatVersion = fromMaybe (vr `peerConnChatVersion` peerChatVRange) chatV, + connChatVersion = fromMaybe (vr cxt `peerConnChatVersion` peerChatVRange) chatV, peerChatVRange = peerChatVRange, connLevel, viaContact, @@ -272,9 +272,9 @@ toConnection vr ((connId, acId, connLevel, viaContact, viaUserContactLink, BI vi entityId_ ConnMember = groupMemberId entityId_ ConnUserContact = userContactLinkId -toMaybeConnection :: VersionRangeChat -> MaybeConnectionRow -> Maybe Connection -toMaybeConnection vr ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, xContactId) :. (customUserProfileId, Just connStatus, Just connType, Just contactConnInitiated, Just localAlias) :. (contactId, groupMemberId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just pqSupport, Just pqEncryption, pqSndEnabled_, pqRcvEnabled_, Just authErrCounter, Just quotaErrCounter, connChatVersion, Just minVer, Just maxVer) :. Only simplexNameRaw) = - Just $ toConnection vr ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, xContactId) :. (customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled_, pqRcvEnabled_, authErrCounter, quotaErrCounter, connChatVersion, minVer, maxVer) :. Only simplexNameRaw) +toMaybeConnection :: StoreCxt -> MaybeConnectionRow -> Maybe Connection +toMaybeConnection cxt ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, xContactId) :. (customUserProfileId, Just connStatus, Just connType, Just contactConnInitiated, Just localAlias) :. (contactId, groupMemberId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just pqSupport, Just pqEncryption, pqSndEnabled_, pqRcvEnabled_, Just authErrCounter, Just quotaErrCounter, connChatVersion, Just minVer, Just maxVer) :. Only simplexNameRaw) = + Just $ toConnection cxt ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, xContactId) :. (customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled_, pqRcvEnabled_, authErrCounter, quotaErrCounter, connChatVersion, minVer, maxVer) :. Only simplexNameRaw) toMaybeConnection _ _ = Nothing -- | Creates a new connection row. The @simplexName@ argument is a TRANSIENT @@ -555,11 +555,11 @@ type ContactRow = Only ContactId :. ContactRow' -- ct.simplex_name -> Contact.simplexName (user's locally-known label) -- cp.simplex_name -> LocalProfile.simplexName (peer's broadcast claim) -toContact :: VersionRangeChat -> User -> [ChatTagId] -> ContactRow :. MaybeConnectionRow -> Contact -toContact vr user chatTags ((Only contactId :. (profileId, localDisplayName, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL, ctSimplexNameRaw, cpSimplexNameRaw, simplexNameVerifiedAt)) :. connRow) = +toContact :: StoreCxt -> User -> [ChatTagId] -> ContactRow :. MaybeConnectionRow -> Contact +toContact cxt user chatTags ((Only contactId :. (profileId, localDisplayName, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, BI contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, BI favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. preparedContactRow :. (contactRequestId, contactGroupMemberId, BI contactGrpInvSent) :. groupDirectInvRow :. (uiThemes, BI chatDeleted, customData, chatItemTTL, ctSimplexNameRaw, cpSimplexNameRaw, simplexNameVerifiedAt)) :. connRow) = let simplexName = decodeSimplexName ctSimplexNameRaw profile = LocalProfile {profileId, displayName, fullName, shortDescr, image, contactLink, simplexName = decodeSimplexName cpSimplexNameRaw, peerType, preferences, localAlias} - activeConn = toMaybeConnection vr connRow + activeConn = toMaybeConnection cxt connRow chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} incognito = maybe False connIncognito activeConn mergedPreferences = contactUserPreferences user userPreferences preferences incognito @@ -741,9 +741,9 @@ type GroupMemberRow = (GroupMemberId, GroupId, Int64, MemberId, VersionChat, Ver type ProfileRow = (ProfileId, ContactName, Text, Maybe Text, Maybe ImageData, Maybe ConnLinkContact, Maybe ChatPeerType, LocalAlias, Maybe Preferences, Maybe Text) -toGroupInfo :: VersionRangeChat -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo -toGroupInfo vr userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, publicMemberCount, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri) :. groupKeysRow :. (gSimplexNameRaw, gpSimplexNameRaw, simplexNameVerifiedAt) :. userMemberRow) = - let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr} +toGroupInfo :: StoreCxt -> Int64 -> [ChatTagId] -> GroupInfoRow -> GroupInfo +toGroupInfo cxt userContactId chatTags ((groupId, localDisplayName, displayName, fullName, shortDescr, localAlias, description, image, groupType_, groupLink_, publicGroupId_) :. accessRow :. (enableNtfs_, sendRcpts, BI favorite, groupPreferences, memberAdmission) :. (createdAt, updatedAt, chatTs, userMemberProfileSentAt) :. preparedGroupRow :. businessRow :. (BI useRelays, relayOwnStatus, uiThemes, currentMembers, publicMemberCount, customData, chatItemTTL, membersRequireAttention, viaGroupLinkUri) :. groupKeysRow :. (gSimplexNameRaw, gpSimplexNameRaw, simplexNameVerifiedAt) :. userMemberRow) = + let membership = (toGroupMember userContactId userMemberRow) {memberChatVRange = vr cxt} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts = unBI <$> sendRcpts, favorite} fullGroupPreferences = mergeGroupPreferences groupPreferences publicGroup = toPublicGroupProfile groupType_ groupLink_ publicGroupId_ (toPublicGroupAccess accessRow) @@ -829,9 +829,9 @@ groupMemberQuery = LEFT JOIN connections c ON c.group_member_id = m.group_member_id |] -toContactMember :: VersionRangeChat -> User -> (GroupMemberRow :. MaybeConnectionRow) -> GroupMember -toContactMember vr User {userContactId} (memberRow :. connRow) = - (toGroupMember userContactId memberRow) {activeConn = toMaybeConnection vr connRow} +toContactMember :: StoreCxt -> User -> (GroupMemberRow :. MaybeConnectionRow) -> GroupMember +toContactMember cxt User {userContactId} (memberRow :. connRow) = + (toGroupMember userContactId memberRow) {activeConn = toMaybeConnection cxt connRow} rowToLocalProfile :: ProfileRow -> LocalProfile rowToLocalProfile (profileId, displayName, fullName, shortDescr, image, contactLink, peerType, localAlias, preferences, simplexNameRaw) = @@ -950,10 +950,10 @@ addGroupChatTags db g@GroupInfo {groupId} = do chatTags <- getGroupChatTags db groupId pure (g :: GroupInfo) {chatTags} -getGroupInfo :: DB.Connection -> VersionRangeChat -> User -> Int64 -> ExceptT StoreError IO GroupInfo -getGroupInfo db vr User {userId, userContactId} groupId = ExceptT $ do +getGroupInfo :: DB.Connection -> StoreCxt -> User -> Int64 -> ExceptT StoreError IO GroupInfo +getGroupInfo db cxt User {userId, userContactId} groupId = ExceptT $ do chatTags <- getGroupChatTags db groupId - firstRow (toGroupInfo vr userContactId chatTags) (SEGroupNotFound groupId) $ + firstRow (toGroupInfo cxt userContactId chatTags) (SEGroupNotFound groupId) $ DB.query db (groupInfoQuery <> " WHERE g.group_id = ? AND g.user_id = ? AND mu.contact_id = ?") diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index d5c2106876..a1f470a872 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -2049,6 +2049,10 @@ type VersionChat = Version ChatVersion type VersionRangeChat = VersionRange ChatVersion +-- | Store-wide context passed to store functions in place of the bare `vr` +-- parameter. Built from config by mkStoreCxt; more fields are added here over time. +newtype StoreCxt = StoreCxt {vr :: VersionRangeChat} + pattern VersionChat :: Word16 -> VersionChat pattern VersionChat v = Version v diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index 6025ad859e..7f03c5c2c0 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -23,7 +23,7 @@ import Data.List (isPrefixOf, isSuffixOf) import Data.Maybe (fromMaybe) import Data.String import qualified Data.Text as T -import Simplex.Chat.Controller (ChatConfig (..), ChatController (..)) +import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), mkStoreCxt) import Simplex.Chat.Markdown (viewName) import Simplex.Chat.Messages.CIContent (e2eInfoNoPQText, e2eInfoPQText) import Simplex.Chat.Protocol @@ -702,10 +702,10 @@ getCtConn cc contactId = getTestCCContact cc contactId >>= maybe (fail "no conne getTestCCContact :: TestCC -> ContactId -> IO Contact getTestCCContact cc contactId = do - let TestCC {chatController = ChatController {config = ChatConfig {chatVRange = vr}}} = cc + let TestCC {chatController = ChatController {config}} = cc withCCTransaction cc $ \db -> withCCUser cc $ \user -> - runExceptT (getContact db vr user contactId) >>= either (fail . show) pure + runExceptT (getContact db (mkStoreCxt config) user contactId) >>= either (fail . show) pure lastItemId :: HasCallStack => TestCC -> IO String lastItemId cc = do