Merge remote-tracking branch 'origin/master' into sh/namespace

# Conflicts:
#	src/Simplex/Chat/Library/Commands.hs
#	src/Simplex/Chat/Library/Subscriber.hs
#	src/Simplex/Chat/Store/Direct.hs
#	src/Simplex/Chat/Store/Groups.hs
#	src/Simplex/Chat/Store/Shared.hs
This commit is contained in:
shum
2026-06-08 16:06:27 +00:00
107 changed files with 2612 additions and 1684 deletions
@@ -577,7 +577,7 @@ struct ChatInfoView: View {
private func clearChatAlert() -> Alert {
Alert(
title: Text("Clear conversation?"),
message: Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."),
message: Text(chat.chatInfo.displayName + "\n\n") + Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."),
primaryButton: .destructive(Text("Clear")) {
Task {
await clearChat(chat)
@@ -1185,6 +1185,7 @@ private func deleteContactOrConversationDialog(
showActionSheet(SomeActionSheet(
actionSheet: ActionSheet(
title: Text("Delete contact?"),
message: Text(contact.displayName),
buttons: [
.destructive(Text("Only delete conversation")) {
deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: .messages, dismissToChatList, showAlert)
@@ -1331,6 +1332,7 @@ private func deleteContactWithoutConversation(
showActionSheet(SomeActionSheet(
actionSheet: ActionSheet(
title: Text("Confirm contact deletion?"),
message: Text(contact.displayName),
buttons: [
.destructive(Text("Delete and notify contact")) {
deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: .full(notify: true), dismissToChatList, showAlert)
@@ -1355,6 +1357,7 @@ private func deleteNotReadyContact(
showActionSheet(SomeActionSheet(
actionSheet: ActionSheet(
title: Text("Confirm contact deletion?"),
message: Text(contact.displayName),
buttons: [
.destructive(Text("Confirm")) {
deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: .full(notify: false), dismissToChatList, showAlert)
@@ -845,7 +845,7 @@ struct GroupChatInfoView: View {
let label: LocalizedStringKey = groupInfo.useRelays ? "Delete channel?" : groupInfo.businessChat == nil ? "Delete group?" : "Delete chat?"
return Alert(
title: Text(label),
message: deleteGroupAlertMessage(groupInfo),
message: Text(chat.chatInfo.displayName + "\n\n") + deleteGroupAlertMessage(groupInfo),
primaryButton: .destructive(Text("Delete")) {
Task {
do {
@@ -867,7 +867,7 @@ struct GroupChatInfoView: View {
private func clearChatAlert() -> Alert {
Alert(
title: Text("Clear conversation?"),
message: Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."),
message: Text(chat.chatInfo.displayName + "\n\n") + Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."),
primaryButton: .destructive(Text("Clear")) {
Task {
await clearChat(chat)
@@ -889,7 +889,7 @@ struct GroupChatInfoView: View {
)
return Alert(
title: Text(titleLabel),
message: Text(messageLabel),
message: Text(chat.chatInfo.displayName + "\n\n") + Text(messageLabel),
primaryButton: .destructive(Text("Leave")) {
Task {
await leaveGroup(chat.chatInfo.apiId)
@@ -568,7 +568,7 @@ struct ChatListNavLink: View {
let label: LocalizedStringKey = groupInfo.useRelays ? "Delete channel?" : groupInfo.businessChat == nil ? "Delete group?" : "Delete chat?"
return Alert(
title: Text(label),
message: deleteGroupAlertMessage(groupInfo),
message: Text(chat.chatInfo.displayName + "\n\n") + deleteGroupAlertMessage(groupInfo),
primaryButton: .destructive(Text("Delete")) {
Task { await deleteChat(chat) }
},
@@ -600,7 +600,7 @@ struct ChatListNavLink: View {
private func clearChatAlert() -> Alert {
Alert(
title: Text("Clear conversation?"),
message: Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."),
message: Text(chat.chatInfo.displayName + "\n\n") + Text("All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you."),
primaryButton: .destructive(Text("Clear")) {
Task { await clearChat(chat) }
},
@@ -630,7 +630,7 @@ struct ChatListNavLink: View {
)
return Alert(
title: Text(titleLabel),
message: Text(messageLabel),
message: Text(chat.chatInfo.displayName + "\n\n") + Text(messageLabel),
primaryButton: .destructive(Text("Leave")) {
Task { await leaveGroup(groupInfo.groupId) }
},
@@ -701,10 +701,10 @@ func rejectContactRequestAlert(_ contactRequestId: Int64) -> Alert {
func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, showError: @escaping (ErrorAlert) -> Void, success: @escaping () -> Void = {}) -> Alert {
Alert(
title: Text("Delete pending connection?"),
message:
contactConnection.initiated
? Text("The contact you shared this link with will NOT be able to connect!")
: Text("The connection you accepted will be cancelled!"),
message: Text(contactConnection.displayName + "\n\n")
+ (contactConnection.initiated
? Text("The contact you shared this link with will NOT be able to connect!")
: Text("The connection you accepted will be cancelled!")),
primaryButton: .destructive(Text("Delete")) {
Task {
do {
@@ -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>
+1 -1
View File
@@ -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 \"设置中允许共享。";
+1 -1
View File
@@ -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)
+3 -3
View File
@@ -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." = "Можете да опитате още веднъж.";
+3 -3
View File
@@ -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.";
+3 -3
View File
@@ -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.";
+3 -3
View File
@@ -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.";
+3 -3
View File
@@ -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.";
+3 -3
View File
@@ -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.";
+3 -3
View File
@@ -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.";
+3 -3
View File
@@ -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.";
+3 -3
View File
@@ -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." = "ユーザープロファイルを右にスワイプすると、非表示またはミュートにすることができます。";
+3 -3
View File
@@ -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.";
+3 -3
View File
@@ -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.";
+1 -1
View File
@@ -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 -4
View File
@@ -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
+3 -3
View File
@@ -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." = "Вы можете попробовать ещё раз.";
+3 -3
View File
@@ -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." = "คุณสามารถซ่อนหรือปิดเสียงโปรไฟล์ผู้ใช้ - ปัดไปทางขวา";
+3 -3
View File
@@ -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.";
+3 -3
View File
@@ -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." = "Ви можете спробувати ще раз.";
+3 -3
View File
@@ -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." = "你可以再试一次。";
@@ -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)
}
}
}
)
}
@@ -248,6 +248,8 @@ fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? =
private fun deleteContactOrConversationDialog(chat: Chat, contact: Contact, chatModel: ChatModel, close: (() -> Unit)?) {
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(MR.strings.delete_contact_question),
text = contact.displayName,
parseHtml = false,
buttons = {
Column {
// Only delete conversation
@@ -306,7 +308,8 @@ private fun deleteActiveContactDialog(chat: Chat, contact: Contact, chatModel: C
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(MR.strings.delete_contact_question),
text = generalGetString(MR.strings.delete_contact_cannot_undo_warning),
text = "${contact.displayName}\n\n${generalGetString(MR.strings.delete_contact_cannot_undo_warning)}",
parseHtml = false,
buttons = {
Column {
// Keep conversation toggle
@@ -361,7 +364,8 @@ private fun deleteActiveContactDialog(chat: Chat, contact: Contact, chatModel: C
private fun deleteContactWithoutConversation(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?) {
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(MR.strings.confirm_delete_contact_question),
text = generalGetString(MR.strings.delete_contact_cannot_undo_warning),
text = "${chat.chatInfo.displayName}\n\n${generalGetString(MR.strings.delete_contact_cannot_undo_warning)}",
parseHtml = false,
buttons = {
Column {
// Delete and notify contact
@@ -417,7 +421,8 @@ private fun deleteContactWithoutConversation(chat: Chat, chatModel: ChatModel, c
private fun deleteNotReadyContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?) {
AlertManager.shared.showAlertDialogButtonsColumn(
title = generalGetString(MR.strings.confirm_delete_contact_question),
text = generalGetString(MR.strings.delete_contact_cannot_undo_warning),
text = "${chat.chatInfo.displayName}\n\n${generalGetString(MR.strings.delete_contact_cannot_undo_warning)}",
parseHtml = false,
buttons = {
// Confirm
SectionItemView({
@@ -492,7 +497,8 @@ fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, chatDe
fun clearChatDialog(chat: Chat, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.clear_chat_question),
text = generalGetString(MR.strings.clear_chat_warning),
text = "${chat.chatInfo.displayName}\n\n${generalGetString(MR.strings.clear_chat_warning)}",
parseHtml = false,
confirmText = generalGetString(MR.strings.clear_verb),
onConfirm = { controller.clearChat(chat, close) },
destructive = true,
@@ -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)
@@ -199,7 +199,8 @@ fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, cl
}
AlertManager.shared.showAlertDialog(
title = generalGetString(titleId),
text = generalGetString(messageId),
text = "${groupInfo.displayName}\n\n${generalGetString(messageId)}",
parseHtml = false,
confirmText = generalGetString(MR.strings.delete_verb),
onConfirm = {
withBGApi {
@@ -233,7 +234,8 @@ fun leaveGroupDialog(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl
MR.strings.you_will_stop_receiving_messages_from_this_chat_chat_history_will_be_preserved
AlertManager.shared.showAlertDialog(
title = generalGetString(titleId),
text = generalGetString(messageId),
text = "${groupInfo.displayName}\n\n${generalGetString(messageId)}",
parseHtml = false,
confirmText = generalGetString(MR.strings.leave_group_button),
onConfirm = {
withLongRunningApi(60_000) {
@@ -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, _ ->
@@ -772,10 +772,11 @@ fun rejectContactRequest(rhId: Long?, contactRequestId: Long, chatModel: ChatMod
fun deleteContactConnectionAlert(rhId: Long?, connection: PendingContactConnection, chatModel: ChatModel, onSuccess: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.delete_pending_connection__question),
text = generalGetString(
text = "${connection.displayName}\n\n" + generalGetString(
if (connection.initiated) MR.strings.contact_you_shared_link_with_wont_be_able_to_connect
else MR.strings.connection_you_accepted_will_be_cancelled
),
parseHtml = false,
confirmText = generalGetString(MR.strings.delete_verb),
onConfirm = {
withBGApi {
@@ -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 = {},
)
}
}
@@ -75,6 +75,8 @@ class AlertManager {
onDismissRequest: (() -> Unit)? = null,
hostDevice: Pair<Long?, String>? = null,
belowTextContent: @Composable (() -> Unit) = {},
// When false, [text] is rendered as literal text — use for user-controlled content.
parseHtml: Boolean = true,
buttons: @Composable () -> Unit,
) {
showAlert {
@@ -82,8 +84,14 @@ class AlertManager {
onDismissRequest = { onDismissRequest?.invoke(); if (dismissible) hideAlert() },
title = alertTitle(title),
buttons = {
AlertContent(text, hostDevice, extraPadding = true, textAlign = textAlign, belowTextContent = belowTextContent) {
buttons()
if (parseHtml) {
AlertContent(text, hostDevice, extraPadding = true, textAlign = textAlign, belowTextContent = belowTextContent) {
buttons()
}
} else {
AlertContent(text?.let { AnnotatedString(it) }, hostDevice, extraPadding = true) {
buttons()
}
}
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))
@@ -122,13 +130,15 @@ class AlertManager {
onDismissRequest: (() -> Unit)? = null,
destructive: Boolean = false,
hostDevice: Pair<Long?, String>? = null,
// When false, [text] is rendered as literal text — use for user-controlled content.
parseHtml: Boolean = true,
) {
showAlert {
AlertDialog(
onDismissRequest = { onDismissRequest?.invoke(); hideAlert() },
title = alertTitle(title),
buttons = {
AlertContent(text, hostDevice, true) {
val buttonRow: @Composable () -> Unit = {
Row(
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING),
horizontalArrangement = Arrangement.SpaceBetween
@@ -149,6 +159,11 @@ class AlertManager {
}, Modifier.focusRequester(focusRequester)) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
}
}
if (parseHtml) {
AlertContent(text, hostDevice, true, content = buttonRow)
} else {
AlertContent(text?.let { AnnotatedString(it) }, hostDevice, true, content = buttonRow)
}
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))
)
@@ -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)
@@ -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))
@@ -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()
}
@@ -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 = { {} },
@@ -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 &amp; 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 &amp; 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 -->
@@ -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() {}
@@ -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