mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-27 22:34:51 +00:00
Merge branch 'master' into f/public-groups
This commit is contained in:
@@ -325,6 +325,15 @@ struct GroupChatInfoView: View {
|
||||
.lineLimit(4)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
if let webPage = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupWebPage,
|
||||
let url = URL(string: webPage) {
|
||||
Link(destination: url) {
|
||||
Text(webPage)
|
||||
.font(.subheadline)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
}
|
||||
if groupInfo.useRelays,
|
||||
let count = groupInfo.groupSummary.publicMemberCount,
|
||||
count > 0 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -1157,8 +1157,8 @@
|
||||
<target state="translated">يطور</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve" approved="no">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve" approved="no">
|
||||
<source>Developer</source>
|
||||
<target state="translated">أدوات المطور</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
||||
@@ -3014,8 +3014,8 @@ alert button</note>
|
||||
<source>Developer options</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve">
|
||||
<source>Developer</source>
|
||||
<target>Инструменти за разработчици</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
||||
@@ -1223,8 +1223,8 @@
|
||||
<source>Develop</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve">
|
||||
<source>Developer</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Device" xml:space="preserve">
|
||||
|
||||
@@ -2904,8 +2904,8 @@ alert button</note>
|
||||
<source>Developer options</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve">
|
||||
<source>Developer</source>
|
||||
<target>Nástroje pro vývojáře</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
||||
@@ -3130,8 +3130,8 @@ alert button</note>
|
||||
<target>Optionen für Entwickler</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve">
|
||||
<source>Developer</source>
|
||||
<target>Entwicklertools</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
||||
@@ -1100,8 +1100,8 @@ Available in v5.1</source>
|
||||
<source>Develop</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve">
|
||||
<source>Developer</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Device" xml:space="preserve">
|
||||
|
||||
@@ -3140,9 +3140,9 @@ alert button</note>
|
||||
<target>Developer options</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
<source>Developer tools</source>
|
||||
<target>Developer tools</target>
|
||||
<trans-unit id="Developer" xml:space="preserve">
|
||||
<source>Developer</source>
|
||||
<target>Developer</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Device" xml:space="preserve">
|
||||
|
||||
@@ -3130,8 +3130,8 @@ alert button</note>
|
||||
<target>Opciones desarrollador</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve">
|
||||
<source>Developer</source>
|
||||
<target>Herramientas desarrollo</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
||||
@@ -2791,8 +2791,8 @@ alert button</note>
|
||||
<source>Developer options</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve">
|
||||
<source>Developer</source>
|
||||
<target>Kehittäjätyökalut</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
||||
@@ -3039,8 +3039,8 @@ alert button</note>
|
||||
<target>Options pour les développeurs</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve">
|
||||
<source>Developer</source>
|
||||
<target>Outils du développeur</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
||||
@@ -1356,8 +1356,8 @@ Available in v5.1</source>
|
||||
<target state="translated">לְפַתֵחַ</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve" approved="no">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve" approved="no">
|
||||
<source>Developer</source>
|
||||
<target state="translated">כלי מפתחים</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
||||
@@ -1012,8 +1012,8 @@
|
||||
<source>Develop</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve">
|
||||
<source>Developer</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Device" xml:space="preserve">
|
||||
|
||||
@@ -3130,8 +3130,8 @@ alert button</note>
|
||||
<target>Fejlesztői beállítások</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve">
|
||||
<source>Developer</source>
|
||||
<target>Fejlesztői eszközök</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
||||
@@ -3130,8 +3130,8 @@ alert button</note>
|
||||
<target>Opzioni sviluppatore</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve">
|
||||
<source>Developer</source>
|
||||
<target>Strumenti di sviluppo</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
||||
@@ -2891,8 +2891,8 @@ alert button</note>
|
||||
<target>開発者向けの設定</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve">
|
||||
<source>Developer</source>
|
||||
<target>開発ツール</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
||||
@@ -1141,8 +1141,8 @@
|
||||
<source>Develop</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve">
|
||||
<source>Developer</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Device" xml:space="preserve">
|
||||
|
||||
@@ -1005,8 +1005,8 @@
|
||||
<source>Develop</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve">
|
||||
<source>Developer</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Device" xml:space="preserve">
|
||||
|
||||
@@ -3040,8 +3040,8 @@ alert button</note>
|
||||
<target>Ontwikkelaars opties</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve">
|
||||
<source>Developer</source>
|
||||
<target>Ontwikkel gereedschap</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
||||
@@ -3063,8 +3063,8 @@ alert button</note>
|
||||
<target>Opcje deweloperskie</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve">
|
||||
<source>Developer</source>
|
||||
<target>Narzędzia deweloperskie</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
||||
@@ -1179,8 +1179,8 @@
|
||||
<target state="translated">Desenvolver</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve" approved="no">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve" approved="no">
|
||||
<source>Developer</source>
|
||||
<target state="translated">Ferramentas de desenvolvimento</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
||||
@@ -1203,8 +1203,8 @@ Available in v5.1</source>
|
||||
<source>Develop</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve">
|
||||
<source>Developer</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Device" xml:space="preserve">
|
||||
|
||||
@@ -3130,8 +3130,8 @@ alert button</note>
|
||||
<target>Опции разработчика</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve">
|
||||
<source>Developer</source>
|
||||
<target>Инструменты разработчика</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
||||
@@ -2779,8 +2779,8 @@ alert button</note>
|
||||
<source>Developer options</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve">
|
||||
<source>Developer</source>
|
||||
<target>เครื่องมือสำหรับนักพัฒนา</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
||||
@@ -3066,8 +3066,8 @@ alert button</note>
|
||||
<target>Geliştirici seçenekleri</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve">
|
||||
<source>Developer</source>
|
||||
<target>Geliştirici araçları</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
||||
@@ -3050,8 +3050,8 @@ alert button</note>
|
||||
<target>Можливості для розробників</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve">
|
||||
<source>Developer</source>
|
||||
<target>Інструменти для розробників</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
||||
@@ -3060,8 +3060,8 @@ alert button</note>
|
||||
<target>开发者选项</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve">
|
||||
<source>Developer</source>
|
||||
<target>开发者工具</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
||||
@@ -1148,8 +1148,8 @@
|
||||
<target state="translated">開發</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Developer tools" xml:space="preserve" approved="no">
|
||||
<source>Developer tools</source>
|
||||
<trans-unit id="Developer" xml:space="preserve" approved="no">
|
||||
<source>Developer</source>
|
||||
<target state="translated">開發者工具</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.";
|
||||
|
||||
|
||||
@@ -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.";
|
||||
|
||||
|
||||
@@ -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.";
|
||||
|
||||
|
||||
@@ -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.";
|
||||
|
||||
|
||||
@@ -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.";
|
||||
|
||||
|
||||
@@ -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.";
|
||||
|
||||
|
||||
@@ -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.";
|
||||
|
||||
|
||||
@@ -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.";
|
||||
|
||||
|
||||
@@ -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.";
|
||||
|
||||
|
||||
@@ -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.";
|
||||
|
||||
|
||||
@@ -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 \"设置中允许共享。";
|
||||
|
||||
|
||||
@@ -2073,7 +2073,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 333;
|
||||
CURRENT_PROJECT_VERSION = 334;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -2098,7 +2098,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES_THIN;
|
||||
MARKETING_VERSION = 6.5.3;
|
||||
MARKETING_VERSION = 6.5.4;
|
||||
OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
@@ -2123,7 +2123,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 333;
|
||||
CURRENT_PROJECT_VERSION = 334;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -2148,7 +2148,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.5.3;
|
||||
MARKETING_VERSION = 6.5.4;
|
||||
OTHER_LDFLAGS = "-Wl,-stack_size,0x1000000";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
@@ -2165,11 +2165,11 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 333;
|
||||
CURRENT_PROJECT_VERSION = 334;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 6.5.3;
|
||||
MARKETING_VERSION = 6.5.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2185,11 +2185,11 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 333;
|
||||
CURRENT_PROJECT_VERSION = 334;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 6.5.3;
|
||||
MARKETING_VERSION = 6.5.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2210,7 +2210,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 333;
|
||||
CURRENT_PROJECT_VERSION = 334;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
@@ -2225,7 +2225,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.5.3;
|
||||
MARKETING_VERSION = 6.5.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -2247,7 +2247,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 333;
|
||||
CURRENT_PROJECT_VERSION = 334;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_CODE_COVERAGE = NO;
|
||||
@@ -2262,7 +2262,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.5.3;
|
||||
MARKETING_VERSION = 6.5.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -2284,7 +2284,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 333;
|
||||
CURRENT_PROJECT_VERSION = 334;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2310,7 +2310,7 @@
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.5.3;
|
||||
MARKETING_VERSION = 6.5.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2335,7 +2335,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 333;
|
||||
CURRENT_PROJECT_VERSION = 334;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2362,7 +2362,7 @@
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.5.3;
|
||||
MARKETING_VERSION = 6.5.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2389,7 +2389,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 333;
|
||||
CURRENT_PROJECT_VERSION = 334;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -2404,7 +2404,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 6.5.3;
|
||||
MARKETING_VERSION = 6.5.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2423,7 +2423,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 333;
|
||||
CURRENT_PROJECT_VERSION = 334;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -2438,7 +2438,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 6.5.3;
|
||||
MARKETING_VERSION = 6.5.4;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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." = "Можете да опитате още веднъж.";
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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." = "ユーザープロファイルを右にスワイプすると、非表示またはミュートにすることができます。";
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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." = "Вы можете попробовать ещё раз.";
|
||||
|
||||
@@ -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." = "คุณสามารถซ่อนหรือปิดเสียงโปรไฟล์ผู้ใช้ - ปัดไปทางขวา";
|
||||
|
||||
@@ -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.";
|
||||
|
||||
@@ -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." = "Ви можете спробувати ще раз.";
|
||||
|
||||
@@ -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." = "你可以再试一次。";
|
||||
|
||||
+36
-11
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+10
-4
@@ -288,16 +288,22 @@ expect fun AttachmentSelection(
|
||||
)
|
||||
|
||||
fun MutableState<ComposeState>.onFilesAttached(uris: List<URI>) {
|
||||
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<ComposeState>.processPickedFile(uri: URI?, text: String?) {
|
||||
if (uri != null) {
|
||||
val fileSize = getFileSize(uri)
|
||||
|
||||
+14
@@ -13,6 +13,7 @@ import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.*
|
||||
@@ -27,6 +28,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import dev.icerock.moko.resources.compose.painterResource
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
@@ -930,6 +932,18 @@ private fun GroupChatInfoHeader(cInfo: ChatInfo, groupInfo: GroupInfo) {
|
||||
modifier = Modifier.combinedClickable(onClick = copyDisplayName, onLongClick = copyDisplayName).onRightClick(copyDisplayName)
|
||||
)
|
||||
ChatInfoDescription(cInfo, displayName, copyNameToClipboard)
|
||||
val webPage = groupInfo.groupProfile.publicGroup?.publicGroupAccess?.groupWebPage
|
||||
if (webPage != null) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
Text(
|
||||
webPage,
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.primary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.clickable { uriHandler.openUriCatching(webPage) }
|
||||
)
|
||||
}
|
||||
if (groupInfo.useRelays) {
|
||||
val count = groupInfo.groupSummary.publicMemberCount
|
||||
if (count != null && count > 0) {
|
||||
|
||||
+20
-5
@@ -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, _ ->
|
||||
|
||||
+158
-124
@@ -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<String?>(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<Boolean>,
|
||||
importArchiveLauncher: FileChooserLauncher,
|
||||
appFilesCountAndSize: MutableState<Pair<Int, Long>>,
|
||||
chatItemTTL: MutableState<ChatItemTTL>,
|
||||
currentUser: User?,
|
||||
users: List<UserInfo>,
|
||||
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<String?>(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<ChatItemTTL>,
|
||||
progressIndicator: MutableState<Boolean>,
|
||||
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
+4
-27
@@ -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<NotificationsMode>,
|
||||
notificationPreviewMode: State<NotificationPreviewMode>,
|
||||
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))
|
||||
|
||||
+102
-55
@@ -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()
|
||||
}
|
||||
|
||||
+52
-39
@@ -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<NotificationsMode>,
|
||||
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 = { {} },
|
||||
|
||||
+38
-16
@@ -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<CoreVersionInfo?>(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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1555,6 +1555,13 @@
|
||||
<string name="settings_section_title_files">Files</string>
|
||||
<string name="settings_section_title_delivery_receipts">Send delivery receipts to</string>
|
||||
<string name="settings_section_title_contact_requests_from_groups">Contact requests from groups</string>
|
||||
<string name="settings_section_title_about">About</string>
|
||||
<string name="settings_section_title_contact">Contact</string>
|
||||
<string name="settings_section_title_support_project">Support the project</string>
|
||||
<string name="chat_data">Chat data</string>
|
||||
<string name="help_and_support">Help & support</string>
|
||||
<string name="more_privacy">More privacy</string>
|
||||
<string name="advanced_settings">Advanced settings</string>
|
||||
<string name="settings_restart_app">Restart</string>
|
||||
<string name="settings_shutdown">Shutdown</string>
|
||||
<string name="settings_developer_tools">Developer tools</string>
|
||||
@@ -2681,7 +2688,7 @@
|
||||
<string name="dont_enable_receipts">Don\'t enable</string>
|
||||
<string name="you_can_enable_delivery_receipts_later">You can enable later via Settings</string>
|
||||
<string name="delivery_receipts_are_disabled">Delivery receipts are disabled!</string>
|
||||
<string name="you_can_enable_delivery_receipts_later_alert">You can enable them later via app Privacy & Security settings.</string>
|
||||
<string name="you_can_enable_delivery_receipts_later_alert">You can enable them later via app Your privacy settings.</string>
|
||||
<string name="error_enabling_delivery_receipts">Error enabling delivery receipts!</string>
|
||||
|
||||
<!-- Remote access -->
|
||||
|
||||
+8
-12
@@ -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() {}
|
||||
|
||||
@@ -24,13 +24,13 @@ android.nonTransitiveRClass=true
|
||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
kotlin.jvm.target=11
|
||||
|
||||
android.version_name=6.5.3
|
||||
android.version_code=351
|
||||
android.version_name=6.5.4
|
||||
android.version_code=353
|
||||
|
||||
android.bundle=false
|
||||
|
||||
desktop.version_name=6.5.3
|
||||
desktop.version_code=144
|
||||
desktop.version_name=6.5.4
|
||||
desktop.version_code=145
|
||||
|
||||
kotlin.version=2.1.20
|
||||
gradle.plugin.version=8.7.0
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@simplex-chat/types",
|
||||
"version": "0.7.0",
|
||||
"version": "0.8.0",
|
||||
"description": "TypeScript types for SimpleX Chat bot libraries",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "simplex-chat",
|
||||
"version": "6.5.2",
|
||||
"version": "6.5.4",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
@@ -24,7 +24,7 @@
|
||||
"docs": "typedoc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@simplex-chat/types": "^0.7.0",
|
||||
"@simplex-chat/types": "^0.8.0",
|
||||
"extract-zip": "^2.0.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"node-addon-api": "^8.5.0"
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -5,5 +5,5 @@ Bump both together for normal releases. For wrapper-only fixes use a PEP 440
|
||||
post-release: __version__ = "6.5.2.post1", LIBS_VERSION unchanged.
|
||||
"""
|
||||
|
||||
__version__ = "6.5.2" # PEP 440 — read by hatchling for wheel metadata
|
||||
LIBS_VERSION = "6.5.2" # simplex-chat-libs release tag (no 'v' prefix)
|
||||
__version__ = "6.5.4" # PEP 440 — read by hatchling for wheel metadata
|
||||
LIBS_VERSION = "6.5.4" # simplex-chat-libs release tag (no 'v' prefix)
|
||||
|
||||
@@ -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.
|
||||
@@ -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<ComposeState>.onFilesAttached(uris: List<URI>) {
|
||||
- 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.
|
||||
@@ -38,14 +38,10 @@
|
||||
</description>
|
||||
|
||||
<releases>
|
||||
<release version="6.5.3" date="2026-05-23">
|
||||
<release version="6.5.4" date="2026-06-02">
|
||||
<url type="details">https://simplex.chat/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html</url>
|
||||
<description>
|
||||
<p>New in v6.5.3:</p>
|
||||
<ul>
|
||||
<li>relays reject groups that were removed, can be manually re-allowed</li>
|
||||
</ul>
|
||||
<p>New in v6.5:</p>
|
||||
<p>New in v6.5.4:</p>
|
||||
<p>Public channels - speak freely!</p>
|
||||
<ul>
|
||||
<li>Reliability: many relays per channel.</li>
|
||||
|
||||
@@ -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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -473,12 +473,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
|
||||
@@ -687,7 +687,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
|
||||
@@ -696,7 +696,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
|
||||
@@ -707,10 +707,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
|
||||
@@ -721,12 +721,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
|
||||
@@ -802,13 +802,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
|
||||
@@ -826,11 +826,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
|
||||
@@ -881,8 +881,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
|
||||
@@ -891,7 +891,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
|
||||
@@ -917,15 +917,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 -> Maybe GroupMember -> CM GroupMember
|
||||
acceptGroupJoinRequestAsync
|
||||
@@ -968,12 +968,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
|
||||
@@ -998,12 +998,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
|
||||
@@ -1012,7 +1012,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
|
||||
@@ -1031,7 +1031,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
|
||||
@@ -1055,28 +1055,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
|
||||
@@ -1090,15 +1090,15 @@ businessGroupProfile :: Profile -> GroupPreferences -> GroupProfile
|
||||
businessGroupProfile Profile {displayName, fullName, shortDescr, image} groupPreferences =
|
||||
GroupProfile {displayName, fullName, description = Nothing, shortDescr, image, publicGroup = 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
|
||||
@@ -1108,15 +1108,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
|
||||
|
||||
@@ -1171,12 +1171,12 @@ memberIntroEvt gInfo reMember =
|
||||
-- sent it, so the recipient verifies the owner signature.
|
||||
forwardGroupRoster :: User -> GroupInfo -> GroupMember -> CM ()
|
||||
forwardGroupRoster user gInfo subscriber = do
|
||||
vr <- chatVersionRange
|
||||
cxt <- chatStoreCxt
|
||||
withStore' (\db -> getGroupRoster db gInfo) >>= \case
|
||||
Nothing -> pure ()
|
||||
Just (ownerGMId, brokerTs, sm@SignedMsg {signedBody}) ->
|
||||
forM_ (eitherToMaybe (J.eitherDecodeStrict' signedBody) :: Maybe (ChatMessage 'Json)) $ \chatMsg ->
|
||||
withStore' (\db -> runExceptT $ getGroupMemberById db vr user ownerGMId) >>= \case
|
||||
withStore' (\db -> runExceptT $ getGroupMemberById db cxt user ownerGMId) >>= \case
|
||||
Right owner -> do
|
||||
let fwd = GrpMsgForward {fwdSender = FwdMember (memberId' owner) (memberShortenedName owner), fwdBrokerTs = brokerTs}
|
||||
sendFwdMemberMessage subscriber fwd (VMSigned MSSVerified sm chatMsg)
|
||||
@@ -1185,11 +1185,11 @@ forwardGroupRoster user gInfo subscriber = do
|
||||
-- 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
|
||||
introduceInChannel cxt user gInfo subscriber@GroupMember {activeConn = Just conn, indexInGroup = subscriberIdx} = do
|
||||
(owners, rosterMems) <- withStore' $ \db ->
|
||||
(,) <$> getGroupOwners db vr user gInfo <*> getGroupRosterMembers db vr user gInfo
|
||||
(,) <$> getGroupOwners db cxt user gInfo <*> getGroupRosterMembers db cxt user gInfo
|
||||
let modMs = owners <> rosterMems
|
||||
void $ sendGroupMessage' user gInfo modMs $ XGrpMemNew (memberInfo gInfo subscriber) Nothing
|
||||
withStore' $ \db ->
|
||||
@@ -1379,9 +1379,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))
|
||||
@@ -1389,17 +1389,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
|
||||
@@ -1410,9 +1410,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
|
||||
@@ -1422,12 +1422,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)
|
||||
@@ -1506,14 +1506,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 ()
|
||||
@@ -1548,16 +1548,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"
|
||||
@@ -1677,25 +1677,25 @@ parseChatMessage' conn s =
|
||||
where
|
||||
errType = CEInvalidChatMessage conn Nothing (safeDecodeUtf8 s)
|
||||
|
||||
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'
|
||||
@@ -1705,7 +1705,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
|
||||
@@ -1731,8 +1731,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)
|
||||
@@ -1747,7 +1747,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)
|
||||
@@ -1861,8 +1861,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"
|
||||
|
||||
@@ -2061,13 +2061,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
|
||||
@@ -2174,11 +2174,11 @@ sendGroupMessage' user gInfo members chatMsgEvent =
|
||||
-- TODO after restoring from a stale backup (relays accept only strictly-greater versions)
|
||||
bumpAndBroadcastRoster :: User -> GroupInfo -> CM ()
|
||||
bumpAndBroadcastRoster user gInfo = do
|
||||
vr <- chatVersionRange
|
||||
cxt <- chatStoreCxt
|
||||
let rosterVer = maybe (VersionRoster 0) (\(VersionRoster n) -> VersionRoster (n + 1)) (rosterVersion gInfo)
|
||||
(relays, roster) <- withStore' $ \db -> do
|
||||
relays <- getGroupRelayMembers db vr user gInfo
|
||||
mods <- getGroupRosterMembers db vr user gInfo
|
||||
relays <- getGroupRelayMembers db cxt user gInfo
|
||||
mods <- getGroupRosterMembers db cxt user gInfo
|
||||
setGroupRosterVersion db gInfo rosterVer
|
||||
pure (relays, buildGroupRoster rosterVer mods)
|
||||
forM_ (L.nonEmpty relays) $ \relays' ->
|
||||
@@ -2188,8 +2188,8 @@ bumpAndBroadcastRoster user gInfo = do
|
||||
sendGroupRosterToRelay :: User -> GroupInfo -> GroupMember -> CM ()
|
||||
sendGroupRosterToRelay user gInfo relayMember =
|
||||
forM_ (rosterVersion gInfo) $ \rosterVer -> do
|
||||
vr <- chatVersionRange
|
||||
mods <- withStore' $ \db -> getGroupRosterMembers db vr user gInfo
|
||||
cxt <- chatStoreCxt
|
||||
mods <- withStore' $ \db -> getGroupRosterMembers db cxt user gInfo
|
||||
void $ sendGroupMessage' user gInfo [relayMember] (XGrpRoster (buildGroupRoster rosterVer mods))
|
||||
|
||||
sendGroupMessages :: MsgEncodingI e => User -> GroupInfo -> Maybe GroupChatScope -> ShowGroupAsSender -> [GroupMember] -> NonEmpty (ChatMsgEvent e) -> CM (NonEmpty (Either ChatError SndMessage), GroupSndResult)
|
||||
@@ -2423,8 +2423,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
|
||||
@@ -2444,8 +2444,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
|
||||
@@ -2487,9 +2487,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))
|
||||
@@ -2515,14 +2515,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_
|
||||
@@ -2815,13 +2815,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)
|
||||
@@ -2860,8 +2860,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
|
||||
@@ -2911,6 +2911,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 #-}
|
||||
|
||||
@@ -124,10 +124,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.
|
||||
@@ -189,27 +189,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
|
||||
@@ -242,13 +242,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
|
||||
@@ -261,10 +261,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
|
||||
@@ -289,12 +289,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
|
||||
|
||||
@@ -329,13 +329,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
|
||||
@@ -347,13 +347,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
|
||||
@@ -364,20 +364,20 @@ 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.
|
||||
-- SEDBException will be re-trown as CRITICAL as it is likely to indicate a temporary database condition
|
||||
-- that will be resolved with app restart.
|
||||
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
|
||||
@@ -580,7 +580,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)
|
||||
@@ -632,7 +632,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
|
||||
@@ -743,7 +743,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
|
||||
@@ -811,7 +811,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)
|
||||
@@ -819,7 +819,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"
|
||||
@@ -893,7 +893,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
|
||||
@@ -929,13 +929,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
|
||||
@@ -958,12 +958,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"
|
||||
@@ -1224,7 +1224,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
confId <- withStore $ \db -> do
|
||||
confId <- getRelayConfId db m
|
||||
liftIO $ updateGroupMemberStatus db userId m GSMemAccepted
|
||||
void $ setRelayKey db vr user m (MemberKey relayKey) relayProfile
|
||||
void $ setRelayKey db cxt user m (MemberKey relayKey) relayProfile
|
||||
pure confId
|
||||
allowAgentConnectionAsync user conn confId XOk
|
||||
else
|
||||
@@ -1313,7 +1313,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
|
||||
@@ -1336,7 +1336,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 ()
|
||||
@@ -1361,7 +1361,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, newlyActiveGMIds) <- 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, newlyActiveLinks, newlyActiveGMIds) <- liftIO $ foldrM (updateRelay db) ([], False, [], []) relays
|
||||
@@ -1374,7 +1374,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} ->
|
||||
@@ -1385,7 +1385,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
void $ sendGroupMessages user gInfo Nothing False recipients events
|
||||
-- send the current roster to relays that just became active so they can serve joiners
|
||||
forM_ newlyActiveGMIds $ \gmId ->
|
||||
(withStore (\db -> getGroupMemberById db vr user gmId) >>= sendGroupRosterToRelay user gInfo) `catchAllErrors` eToView
|
||||
(withStore (\db -> getGroupMemberById db cxt user gmId) >>= sendGroupRosterToRelay user gInfo) `catchAllErrors` eToView
|
||||
where
|
||||
updateRelay :: DB.Connection -> GroupRelay -> ([GroupRelay], Bool, [ShortLinkContact], [GroupMemberId]) -> IO ([GroupRelay], Bool, [ShortLinkContact], [GroupMemberId])
|
||||
updateRelay db relay@GroupRelay {groupMemberId, relayLink, relayStatus} (acc, changed, newlyActiveLinks, newlyActiveGMIds) =
|
||||
@@ -1427,7 +1427,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
|
||||
@@ -1559,7 +1559,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
|
||||
@@ -1585,10 +1585,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
|
||||
@@ -1603,7 +1603,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
|
||||
memberJoinRequestViaRelay :: InvitationId -> VersionRangeChat -> Maybe SignedMsg -> Profile -> MemberId -> MemberKey -> Maybe MemberId -> CM ()
|
||||
@@ -1611,8 +1611,8 @@ 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
|
||||
existing_ <- withStore' $ \db -> eitherToMaybe <$> runExceptT (getGroupMemberByMemberId db vr user gInfo joiningMemberId)
|
||||
gInfo <- withStore $ \db -> getGroupInfo db cxt user groupId
|
||||
existing_ <- withStore' $ \db -> eitherToMaybe <$> runExceptT (getGroupMemberByMemberId db cxt user gInfo joiningMemberId)
|
||||
case existing_ of
|
||||
Just rosterMem
|
||||
-- a privileged memberId's key is owner-authoritative (the roster); the joiner must prove
|
||||
@@ -1800,7 +1800,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
|
||||
@@ -1816,7 +1816,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
|
||||
@@ -1889,7 +1889,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
|
||||
|
||||
@@ -1897,7 +1897,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}
|
||||
@@ -2058,7 +2058,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
|
||||
@@ -2096,13 +2096,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_
|
||||
@@ -2192,7 +2192,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
|
||||
@@ -2223,7 +2223,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')
|
||||
@@ -2335,7 +2335,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_
|
||||
@@ -2403,14 +2403,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
|
||||
@@ -2419,7 +2419,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
|
||||
@@ -2447,7 +2447,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
|
||||
@@ -2485,7 +2485,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
|
||||
@@ -2501,7 +2501,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)
|
||||
@@ -2510,7 +2510,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
|
||||
@@ -2536,7 +2536,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'
|
||||
@@ -2577,7 +2577,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}
|
||||
@@ -2586,7 +2586,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
|
||||
@@ -2655,7 +2655,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
|
||||
@@ -2699,7 +2699,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 ()
|
||||
@@ -2721,7 +2721,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') <- withStore $ \db -> updateContactMemberProfile db user m mCt p'
|
||||
@@ -2769,7 +2769,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
|
||||
@@ -2785,7 +2785,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
|
||||
@@ -2815,7 +2815,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
|
||||
@@ -2964,14 +2964,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'
|
||||
|
||||
@@ -2981,15 +2981,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
conn' <- updatePeerChatVRange activeConn chatVRange
|
||||
case chatMsgEvent of
|
||||
XInfo p -> do
|
||||
ct <- withStore $ \db -> createDirectContact db vr user conn' p
|
||||
ct <- withStore $ \db -> createDirectContact db cxt user conn' p
|
||||
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)
|
||||
@@ -3003,7 +3003,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
if sameMemberId memId (membership gInfo)
|
||||
then pure Nothing
|
||||
else
|
||||
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}
|
||||
-- roster-established privileged member: the relay may update the profile only,
|
||||
-- never the role or key (those are owner-authoritative via the roster, and
|
||||
@@ -3016,7 +3016,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
-- TODO [relays] member: surface relay-key-mismatch as a dedicated event / chat item / relay state
|
||||
when (assertedKey /= memberPubKey unknownMember) $
|
||||
messageWarning $ "x.grp.mem.new: relay asserted key differs from roster-established key, keeping roster key, memberId=" <> safeDecodeUtf8 (strEncode memId)
|
||||
updatedMember <- withStore $ \db -> updateRosterMemberAnnounced db vr user m unknownMember memInfo initialStatus
|
||||
updatedMember <- withStore $ \db -> updateRosterMemberAnnounced db cxt user m unknownMember memInfo initialStatus
|
||||
-- roster members can't be pending, so no members-require-attention update
|
||||
gInfo' <- updatePublicGroupData user gInfo
|
||||
toView $ CEvtUnknownMemberAnnounced user gInfo' m unknownMember updatedMember
|
||||
@@ -3027,7 +3027,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
messageError "x.grp.mem.new: privileged role not established by roster" $> Nothing
|
||||
| otherwise -> 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
|
||||
@@ -3083,10 +3083,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"
|
||||
@@ -3107,7 +3107,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
|
||||
@@ -3118,7 +3118,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
|
||||
@@ -3127,7 +3127,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"
|
||||
@@ -3138,7 +3138,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.
|
||||
@@ -3162,7 +3162,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)
|
||||
@@ -3171,7 +3171,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
|
||||
-- in relay groups the roster delivers the chat item for previously-unknown privileged members
|
||||
Left _
|
||||
@@ -3242,7 +3242,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
let rosterIds = map (\RosterMember {memberId} -> memberId) entries
|
||||
acc <- foldrM applyRosterEntry ([], []) entries
|
||||
-- absent privileged members revert to the joiner default
|
||||
currentPriv <- liftIO $ getGroupRosterMembers db vr user gInfo
|
||||
currentPriv <- liftIO $ getGroupRosterMembers db cxt user gInfo
|
||||
liftIO $ forM_ currentPriv $ \m ->
|
||||
when (memberId' m `notElem` rosterIds) $
|
||||
updateGroupMemberRole db user m defaultRole
|
||||
@@ -3253,7 +3253,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
|
||||
apply `catchAllErrors` \_ -> pure (cs, as)
|
||||
where
|
||||
applied m = (cs, ((m :: GroupMember) {memberRole = role}, memberRole' m) : as)
|
||||
apply = getCreateUnknownGMByMemberId db vr user gInfo memberId name defaultRole True >>= \case
|
||||
apply = getCreateUnknownGMByMemberId db cxt user gInfo memberId name defaultRole True >>= \case
|
||||
Nothing -> pure (cs, as)
|
||||
Just (m, _) -> case memberPubKey m of
|
||||
Just k
|
||||
@@ -3311,7 +3311,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
|
||||
@@ -3335,7 +3335,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
|
||||
@@ -3357,7 +3357,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}}
|
||||
@@ -3507,7 +3507,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
|
||||
@@ -3534,7 +3534,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
|
||||
@@ -3542,7 +3542,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
|
||||
@@ -3553,7 +3553,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'
|
||||
@@ -3562,7 +3562,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
|
||||
@@ -3593,7 +3593,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)
|
||||
@@ -3650,8 +3650,12 @@ 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 ->
|
||||
signed MSSVerified <$ guard (verifyGroupSig pubKey publicGroupId memberId signatures signedBody)
|
||||
CBGroup
|
||||
| Just GroupKeys {publicGroupId} <- groupKeys gInfo ->
|
||||
signed MSSVerified <$ guard (verifyGroupSig pubKey publicGroupId memberId signatures signedBody)
|
||||
| otherwise ->
|
||||
let prefix = smpEncode chatBinding <> 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)
|
||||
where
|
||||
@@ -3723,7 +3727,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
|
||||
@@ -3747,14 +3751,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
|
||||
@@ -3774,20 +3778,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
|
||||
@@ -3803,7 +3807,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
|
||||
@@ -3862,19 +3866,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
|
||||
@@ -3914,7 +3918,7 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do
|
||||
senders <- withStore' $ \db ->
|
||||
fmap catMaybes . forM senderGMIds $ \sId ->
|
||||
fmap (join . eitherToMaybe) . runExceptT $ do
|
||||
sender <- getNonRemovedMemberById db vr user sId
|
||||
sender <- getNonRemovedMemberById db cxt user sId
|
||||
-- owners are already known to every member (group link + owner-intro in introduceInChannel),
|
||||
-- so we never disseminate their profile (redundant, and races with joins re-announcing the owner)
|
||||
if memberRole' sender == GROwner
|
||||
@@ -3932,7 +3936,7 @@ runDeliveryJobWorker a deliveryKey Worker {doWork} = do
|
||||
then pure (body, [], [], [])
|
||||
else do
|
||||
-- all members' profiles disseminate; privileged key/role come from the roster, not here
|
||||
let (encoderErrs, validLabeled) = partitionEithers [(\bs -> (s, bs)) <$> encodeMemberNew vr gInfo s | (s, _) <- senders]
|
||||
let (encoderErrs, validLabeled) = partitionEithers [(\bs -> (s, bs)) <$> encodeMemberNew (vr cxt) gInfo s | (s, _) <- senders]
|
||||
(extBody', inBody, overflowLabeled, large1) = batchProfilesWithBody maxEncodedMsgLength body validLabeled
|
||||
(overflowBatches', large2) = batchProfiles maxEncodedMsgLength overflowLabeled
|
||||
packerErrs = [ChatError (CEInternalError $ "oversized profile element for member " <> show (groupMemberId' s)) | s <- large1 <> large2]
|
||||
@@ -3950,7 +3954,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)
|
||||
@@ -3995,7 +3999,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
|
||||
@@ -4005,14 +4009,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
|
||||
@@ -4022,14 +4026,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
|
||||
@@ -4080,7 +4084,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
|
||||
@@ -4088,10 +4092,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
|
||||
@@ -4140,7 +4144,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)
|
||||
@@ -4168,7 +4172,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 ()
|
||||
@@ -4200,5 +4204,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
|
||||
|
||||
@@ -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|
|
||||
@@ -172,7 +172,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
|
||||
@@ -191,17 +191,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") $
|
||||
@@ -222,8 +222,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
|
||||
@@ -240,7 +240,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 =
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -243,8 +243,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
|
||||
@@ -261,7 +261,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
|
||||
@@ -285,13 +285,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|
|
||||
@@ -305,10 +305,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|
|
||||
@@ -394,18 +394,18 @@ createIncognitoProfile db User {userId} p = do
|
||||
createdAt <- getCurrentTime
|
||||
createIncognitoProfile_ db userId createdAt p
|
||||
|
||||
createPreparedContact :: DB.Connection -> VersionRangeChat -> User -> Profile -> ACreatedConnLink -> Maybe SharedMsgId -> ExceptT StoreError IO Contact
|
||||
createPreparedContact db vr user p connLinkToConnect welcomeSharedMsgId = do
|
||||
createPreparedContact :: DB.Connection -> StoreCxt -> User -> Profile -> ACreatedConnLink -> Maybe SharedMsgId -> ExceptT StoreError IO Contact
|
||||
createPreparedContact db cxt user p connLinkToConnect welcomeSharedMsgId = do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
let prepared = Just (connLinkToConnect, welcomeSharedMsgId)
|
||||
ctUserPreferences = newContactUserPrefs user p
|
||||
contactId <- createContact_ db user p ctUserPreferences prepared "" currentTs
|
||||
getContact db vr user contactId
|
||||
getContact db cxt user contactId
|
||||
|
||||
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
|
||||
@@ -438,15 +438,15 @@ updatePreparedContactUser
|
||||
|]
|
||||
(newUserId, currentTs, contactId)
|
||||
safeDeleteLDN db user oldLDN
|
||||
getContact db vr newUser contactId
|
||||
getContact db cxt newUser contactId
|
||||
|
||||
createDirectContact :: DB.Connection -> VersionRangeChat -> User -> Connection -> Profile -> ExceptT StoreError IO Contact
|
||||
createDirectContact db vr user Connection {connId, localAlias} p = do
|
||||
createDirectContact :: DB.Connection -> StoreCxt -> User -> Connection -> Profile -> ExceptT StoreError IO Contact
|
||||
createDirectContact db cxt user Connection {connId, localAlias} p = do
|
||||
currentTs <- liftIO getCurrentTime
|
||||
let ctUserPreferences = newContactUserPrefs user p
|
||||
contactId <- createContact_ db user p ctUserPreferences Nothing localAlias currentTs
|
||||
liftIO $ DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, currentTs, connId)
|
||||
getContact db vr user contactId
|
||||
getContact db cxt user contactId
|
||||
|
||||
deleteContactConnections :: DB.Connection -> User -> Contact -> IO ()
|
||||
deleteContactConnections db User {userId} Contact {contactId} = do
|
||||
@@ -500,13 +500,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 =
|
||||
@@ -756,15 +756,15 @@ 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
|
||||
|
||||
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)
|
||||
@@ -890,22 +890,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|
|
||||
@@ -932,8 +932,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_ =
|
||||
@@ -950,11 +950,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|
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user